From b6ffb57263a4175868449ad72c13e57226446f2e Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 25 Jan 2013 09:30:31 +0000 Subject: [PATCH 001/197] Fixing GP_EP --- GPy/examples/classification.py | 3 +- GPy/examples/ep_fix.py | 39 +++ GPy/inference/EP.py | 314 +++++++++++++++++++++++ GPy/inference/Expectation_Propagation.py | 2 +- GPy/inference/likelihoods.py | 2 +- GPy/models/GP_EP.py | 2 +- GPy/models/GP_EP2.py | 280 ++++++++++++++++++++ GPy/models/__init__.py | 1 + 8 files changed, 638 insertions(+), 5 deletions(-) create mode 100644 GPy/examples/ep_fix.py create mode 100644 GPy/inference/EP.py create mode 100644 GPy/models/GP_EP2.py diff --git a/GPy/examples/classification.py b/GPy/examples/classification.py index 989ed08a..fb14139d 100644 --- a/GPy/examples/classification.py +++ b/GPy/examples/classification.py @@ -76,11 +76,10 @@ def toy_linear_1d_classification(model_type='Full', inducing=4, seed=default_see # create simple GP model if model_type=='Full': - m = GPy.models.simple_GP_EP(data['X'],likelihood) + m = GPy.models.GP_EP(data['X'],likelihood) else: # create sparse GP EP model m = GPy.models.sparse_GP_EP(data['X'],likelihood=likelihood,inducing=inducing,ep_proxy=model_type) - m.constrain_positive('var') m.constrain_positive('len') diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py new file mode 100644 index 00000000..e4999f30 --- /dev/null +++ b/GPy/examples/ep_fix.py @@ -0,0 +1,39 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +""" +Simple Gaussian Processes classification +""" +import pylab as pb +import numpy as np +import GPy +pb.ion() + +default_seed=10000 + +model_type='Full' +inducing=4 +seed=default_seed +"""Simple 1D classification example. +:param model_type: type of model to fit ['Full', 'FITC', 'DTC']. +:param seed : seed value for data generation (default is 4). +:type seed: int +:param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). +:type inducing: int +""" +data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) +likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) + +m = GPy.models.GP_EP2(data['X'],likelihood) + +#m.constrain_positive('var') +#m.constrain_positive('len') +#m.tie_param('lengthscale') +m.approximate_likelihood() +# Optimize and plot +#m.optimize() +#m.em(plot_all=False) # EM algorithm +m.plot() + +print(m) diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py new file mode 100644 index 00000000..fa691961 --- /dev/null +++ b/GPy/inference/EP.py @@ -0,0 +1,314 @@ +import numpy as np +import random +import pylab as pb #TODO erase me +from scipy import stats, linalg +from .likelihoods import likelihood +from ..core import model +from ..util.linalg import pdinv,mdot,jitchol +from ..util.plot import gpplot +from .. import kern + +class EP: + def __init__(self,covariance,likelihood,Kmn=None,Knn_diag=None,epsilon=1e-3,powerep=[1.,1.]): + """ + Expectation Propagation + + Arguments + --------- + X : input observations + likelihood : Output's likelihood (likelihood class) + kernel : a GPy kernel (kern class) + inducing : Either an array specifying the inducing points location or a sacalar defining their number. None value for using a non-sparse model is used. + powerep : Power-EP parameters (eta,delta) - 2x1 numpy array (floats) + epsilon : Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) + """ + self.likelihood = likelihood + assert covariance.shape[0] == covariance.shape[1] + if Kmn is not None: + self.Kmm = covariance + self.Kmn = Kmn + self.M = self.Kmn.shape[0] + self.N = self.Kmn.shape[1] + assert self.M < self.N, 'The number of inducing inputs must be smaller than the number of observations' + else: + self.K = covariance + self.N = self.K.shape[0] + if Knn_diag is not None: + self.Knn_diag = Knn_diag + assert len(Knn_diag) == self.N, 'Knn_diagonal has size different from N' + + self.epsilon = epsilon + self.eta, self.delta = powerep + self.jitter = 1e-12 + + """ + Initial values - Likelihood approximation parameters: + p(y|f) = t(f|tau_tilde,v_tilde) + """ + self.tau_tilde = np.zeros(self.N) + self.v_tilde = np.zeros(self.N) + + def restart_EP(self): + """ + Set the EP approximation to initial state + """ + self.tau_tilde = np.zeros(self.N) + self.v_tilde = np.zeros(self.N) + self.mu = np.zeros(self.N) + +class Full(EP): + def fit_EP(self): + """ + The expectation-propagation algorithm. + For nomenclature see Rasmussen & Williams 2006 (pag. 52-60) + """ + #Prior distribution parameters: p(f|X) = N(f|0,K) + #self.K = self.kernel.K(self.X,self.X) + + #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) + self.mu=np.zeros(self.N) + self.Sigma=self.K.copy() + + """ + Initial values - Cavity distribution parameters: + q_(f|mu_,sigma2_) = Product{q_i(f|mu_i,sigma2_i)} + sigma_ = 1./tau_ + mu_ = v_/tau_ + """ + self.tau_ = np.empty(self.N,dtype=float) + self.v_ = np.empty(self.N,dtype=float) + + #Initial values - Marginal moments + z = np.empty(self.N,dtype=float) + self.Z_hat = np.empty(self.N,dtype=float) + phi = np.empty(self.N,dtype=float) + mu_hat = np.empty(self.N,dtype=float) + sigma2_hat = np.empty(self.N,dtype=float) + self.mu_hat = mu_hat #TODO erase me + self.sigma2_hat = sigma2_hat #TODO erase me + + #Approximation + epsilon_np1 = self.epsilon + 1. + epsilon_np2 = self.epsilon + 1. + self.iterations = 0 + self.np1 = [self.tau_tilde.copy()] + self.np2 = [self.v_tilde.copy()] + while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: + update_order = np.arange(self.N) + #random.shuffle(update_order) #TODO uncomment + for i in update_order: + #Cavity distribution parameters + self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] + self.v_[i] = self.mu[i]/self.Sigma[i,i] - self.eta*self.v_tilde[i] + #Marginal moments + self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + self.mu_hat[i] = mu_hat[i] #TODO erase me + self.sigma2_hat[i] = sigma2_hat[i] #TODO erase me + #if i == 3: + # a = b + #Site parameters update + Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma[i,i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma[i,i]) + print Delta_tau + self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau + self.v_tilde[i] = self.v_tilde[i] + Delta_v + #Posterior distribution parameters update + si=self.Sigma[:,i].reshape(self.N,1) + self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) + self.mu = np.dot(self.Sigma,self.v_tilde) + self.iterations += 1 + #Sigma recomptutation with Cholesky decompositon + Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*(self.K) + B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K + L = jitchol(B) + V,info = linalg.flapack.dtrtrs(L,Sroot_tilde_K,lower=1) + self.Sigma = self.K - np.dot(V.T,V) + self.mu = np.dot(self.Sigma,self.v_tilde) + epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N + epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N + self.np1.append(self.tau_tilde.copy()) + self.np2.append(self.v_tilde.copy()) + +class DTC(EP): + def fit_EP(self): + """ + The expectation-propagation algorithm with sparse pseudo-input. + For nomenclature see ... 2013. + """ + + """ + Prior approximation parameters: + q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) + Sigma0 = Qnn = Knm*Kmmi*Kmn + """ + self.Kmmi, self.Kmm_hld = pdinv(self.Kmm) + self.KmnKnm = np.dot(self.Kmn, self.Kmn.T) + self.KmmiKmn = np.dot(self.Kmmi,self.Kmn) + self.Qnn_diag = np.sum(self.Kmn*self.KmmiKmn,-2) + self.LLT0 = self.Kmm.copy() + + """ + Posterior approximation: q(f|y) = N(f| mu, Sigma) + Sigma = Diag + P*R.T*R*P.T + K + mu = w + P*gamma + """ + self.mu = np.zeros(self.N) + self.LLT = self.Kmm.copy() + self.Sigma_diag = self.Qnn_diag.copy() + + """ + Initial values - Cavity distribution parameters: + q_(g|mu_,sigma2_) = Product{q_i(g|mu_i,sigma2_i)} + sigma_ = 1./tau_ + mu_ = v_/tau_ + """ + self.tau_ = np.empty(self.N,dtype=float) + self.v_ = np.empty(self.N,dtype=float) + + #Initial values - Marginal moments + z = np.empty(self.N,dtype=float) + self.Z_hat = np.empty(self.N,dtype=float) + phi = np.empty(self.N,dtype=float) + mu_hat = np.empty(self.N,dtype=float) + sigma2_hat = np.empty(self.N,dtype=float) + + #Approximation + epsilon_np1 = 1 + epsilon_np2 = 1 + self.iterations = 0 + self.np1 = [self.tau_tilde.copy()] + self.np2 = [self.v_tilde.copy()] + while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: + update_order = np.arange(self.N) + random.shuffle(update_order) + for i in update_order: + #Cavity distribution parameters + self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] + self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] + #Marginal moments + self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + #Site parameters update + Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) + self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau + self.v_tilde[i] = self.v_tilde[i] + Delta_v + #Posterior distribution parameters update + self.LLT = self.LLT + np.outer(self.Kmn[:,i],self.Kmn[:,i])*Delta_tau + L = jitchol(self.LLT) + V,info = linalg.flapack.dtrtrs(L,self.Kmn,lower=1) + self.Sigma_diag = np.sum(V*V,-2) + si = np.sum(V.T*V[:,i],-1) + self.mu = self.mu + (Delta_v-Delta_tau*self.mu[i])*si + self.iterations += 1 + #Sigma recomputation with Cholesky decompositon + self.LLT0 = self.LLT0 + np.dot(self.Kmn*self.tau_tilde[None,:],self.Kmn.T) + self.L = jitchol(self.LLT) + V,info = linalg.flapack.dtrtrs(L,self.Kmn,lower=1) + V2,info = linalg.flapack.dtrtrs(L.T,V,lower=0) + self.Sigma_diag = np.sum(V*V,-2) + Knmv_tilde = np.dot(self.Kmn,self.v_tilde) + self.mu = np.dot(V2.T,Knmv_tilde) + epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N + epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N + self.np1.append(self.tau_tilde.copy()) + self.np2.append(self.v_tilde.copy()) + +class FITC(EP): + def fit_EP(self): + """ + The expectation-propagation algorithm with sparse pseudo-input. + For nomenclature see Naish-Guzman and Holden, 2008. + """ + + """ + Prior approximation parameters: + q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) + Sigma0 = diag(Knn-Qnn) + Qnn, Qnn = Knm*Kmmi*Kmn + """ + self.Kmmi, self.Kmm_hld = pdinv(self.Kmm) + self.P0 = self.Kmn.T + self.KmnKnm = np.dot(self.P0.T, self.P0) + self.KmmiKmn = np.dot(self.Kmmi,self.P0.T) + self.Qnn_diag = np.sum(self.P0.T*self.KmmiKmn,-2) + self.Diag0 = self.Knn_diag - self.Qnn_diag + self.R0 = jitchol(self.Kmmi).T + + """ + Posterior approximation: q(f|y) = N(f| mu, Sigma) + Sigma = Diag + P*R.T*R*P.T + K + mu = w + P*gamma + """ + self.w = np.zeros(self.N) + self.gamma = np.zeros(self.M) + self.mu = np.zeros(self.N) + self.P = self.P0.copy() + self.R = self.R0.copy() + self.Diag = self.Diag0.copy() + self.Sigma_diag = self.Knn_diag + + """ + Initial values - Cavity distribution parameters: + q_(g|mu_,sigma2_) = Product{q_i(g|mu_i,sigma2_i)} + sigma_ = 1./tau_ + mu_ = v_/tau_ + """ + self.tau_ = np.empty(self.N,dtype=float) + self.v_ = np.empty(self.N,dtype=float) + + #Initial values - Marginal moments + z = np.empty(self.N,dtype=float) + self.Z_hat = np.empty(self.N,dtype=float) + phi = np.empty(self.N,dtype=float) + mu_hat = np.empty(self.N,dtype=float) + sigma2_hat = np.empty(self.N,dtype=float) + + #Approximation + epsilon_np1 = 1 + epsilon_np2 = 1 + self.iterations = 0 + self.np1 = [self.tau_tilde.copy()] + self.np2 = [self.v_tilde.copy()] + while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: + update_order = np.arange(self.N) + random.shuffle(update_order) + for i in update_order: + #Cavity distribution parameters + self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] + self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] + #Marginal moments + self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + #Site parameters update + Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) + self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau + self.v_tilde[i] = self.v_tilde[i] + Delta_v + #Posterior distribution parameters update + dtd1 = Delta_tau*self.Diag[i] + 1. + dii = self.Diag[i] + self.Diag[i] = dii - (Delta_tau * dii**2.)/dtd1 + pi_ = self.P[i,:].reshape(1,self.M) + self.P[i,:] = pi_ - (Delta_tau*dii)/dtd1 * pi_ + Rp_i = np.dot(self.R,pi_.T) + RTR = np.dot(self.R.T,np.dot(np.eye(self.M) - Delta_tau/(1.+Delta_tau*self.Sigma_diag[i]) * np.dot(Rp_i,Rp_i.T),self.R)) + self.R = jitchol(RTR).T + self.w[i] = self.w[i] + (Delta_v - Delta_tau*self.w[i])*dii/dtd1 + self.gamma = self.gamma + (Delta_v - Delta_tau*self.mu[i])*np.dot(RTR,self.P[i,:].T) + self.RPT = np.dot(self.R,self.P.T) + self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) + self.mu = self.w + np.dot(self.P,self.gamma) + self.iterations += 1 + #Sigma recomptutation with Cholesky decompositon + self.Diag = self.Diag0/(1.+ self.Diag0 * self.tau_tilde) + self.P = (self.Diag / self.Diag0)[:,None] * self.P0 + self.RPT0 = np.dot(self.R0,self.P0.T) + L = jitchol(np.eye(self.M) + np.dot(self.RPT0,(1./self.Diag0 - self.Diag/(self.Diag0**2))[:,None]*self.RPT0.T)) + self.R,info = linalg.flapack.dtrtrs(L,self.R0,lower=1) + self.RPT = np.dot(self.R,self.P.T) + self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) + self.w = self.Diag * self.v_tilde + self.gamma = np.dot(self.R.T, np.dot(self.RPT,self.v_tilde)) + self.mu = self.w + np.dot(self.P,self.gamma) + epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N + epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N + self.np1.append(self.tau_tilde.copy()) + self.np2.append(self.v_tilde.copy()) diff --git a/GPy/inference/Expectation_Propagation.py b/GPy/inference/Expectation_Propagation.py index 05453f1d..520fc607 100644 --- a/GPy/inference/Expectation_Propagation.py +++ b/GPy/inference/Expectation_Propagation.py @@ -116,7 +116,7 @@ class Full(EP_base): self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) if messages: - print "EP iteration %i, epsiolon %d"%(self.iterations,epsilon_np1) + print "EP iteration %i, epsilon %d"%(self.iterations,epsilon_np1) class FITC(EP_base): """ diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index 5f0eb7ff..ff4770f6 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -32,7 +32,7 @@ class likelihood: """ assert X_new.shape[1] == 1, 'Number of dimensions must be 1' gpplot(X_new,Mean_new,Var_new) - pb.errorbar(X_u,Mean_u,2*np.sqrt(Var_u),fmt='r+') + pb.errorbar(X_u.flatten(),Mean_u.flatten(),2*np.sqrt(Var_u.flatten()),fmt='r+') pb.plot(X_u,Mean_u,'ro') def plot2D(self,X,X_new,F_new,U=None): diff --git a/GPy/models/GP_EP.py b/GPy/models/GP_EP.py index 51d69d0a..302ff366 100644 --- a/GPy/models/GP_EP.py +++ b/GPy/models/GP_EP.py @@ -57,7 +57,7 @@ class GP_EP(model): def posterior_param(self): self.K = self.kernel.K(self.X) self.Sroot_tilde_K = np.sqrt(self.ep_approx.tau_tilde)[:,None]*self.K - B = np.eye(self.N) + np.sqrt(self.ep_approx.tau_tilde)[None,:]*self.Sroot_tilde_K + B = np.eye(self.N) + np.sqrt(self.ep_approx.tau_tilde)*self.Sroot_tilde_K #self.L = np.linalg.cholesky(B) self.L = jitchol(B) V,info = linalg.flapack.dtrtrs(self.L,self.Sroot_tilde_K,lower=1) diff --git a/GPy/models/GP_EP2.py b/GPy/models/GP_EP2.py new file mode 100644 index 00000000..c68e7b70 --- /dev/null +++ b/GPy/models/GP_EP2.py @@ -0,0 +1,280 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + +import numpy as np +import pylab as pb +from scipy import stats, linalg +from .. import kern +from ..inference.EP import Full +from ..inference.likelihoods import likelihood,probit,poisson,gaussian +from ..core import model +from ..util.linalg import pdinv,mdot #,jitchol +from ..util.plot import gpplot, Tango + +class GP_EP2(model): + def __init__(self,X,likelihood,kernel=None,normalize_X=False,Xslices=None,epsilon_ep=1e-3,epsion_em=.1,powerep=[1.,1.]): + """ + Simple Gaussian Process with Non-Gaussian likelihood + + Arguments + --------- + :param X: input observations (NxD numpy.darray) + :param likelihood: a GPy likelihood (likelihood class) + :param kernel: a GPy kernel, defaults to rbf+white + :param normalize_X: whether to normalize the input data before computing (predictions will be in original scales) + :type normalize_X: False|True + :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 1e-3 + :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] (list) + :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) + :rtype: model object. + """ + #.. Note:: Multiple independent outputs are allowed using columns of Y #TODO add this note? + if kernel is None: + kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) + + # parse arguments + self.Xslices = Xslices + assert isinstance(kernel, kern.kern) + self.likelihood = likelihood + #self.Y = self.likelihood.Y #we might not need this + self.kern = kernel + self.X = X + assert len(self.X.shape)==2 + #assert len(self.Y.shape)==2 + #assert self.X.shape[0] == self.Y.shape[0] + #self.N, self.D = self.Y.shape + self.D = 1 + self.N, self.Q = self.X.shape + + #here's some simple normalisation + if normalize_X: + self._Xmean = X.mean(0)[None,:] + self._Xstd = X.std(0)[None,:] + self.X = (X.copy() - self._Xmean) / self._Xstd + if hasattr(self,'Z'): + self.Z = (self.Z - self._Xmean) / self._Xstd + else: + self._Xmean = np.zeros((1,self.X.shape[1])) + self._Xstd = np.ones((1,self.X.shape[1])) + + #THIS PART IS NOT NEEDED + """ + if normalize_Y: + self._Ymean = Y.mean(0)[None,:] + self._Ystd = Y.std(0)[None,:] + self.Y = (Y.copy()- self._Ymean) / self._Ystd + else: + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + + if self.D > self.N: + # then it's more efficient to store YYT + self.YYT = np.dot(self.Y, self.Y.T) + else: + self.YYT = None + """ + self.eta,self.delta = powerep + self.epsilon_ep = epsilon_ep + self.tau_tilde = np.zeros([self.N,self.D]) + self.v_tilde = np.zeros([self.N,self.D]) + model.__init__(self) + + def _set_params(self,p): + self.kern._set_params_transformed(p) + self.K = self.kern.K(self.X,slices1=self.Xslices) + self.posterior_params() + + def _get_params(self): + return self.kern._get_params_transformed() + + def _get_param_names(self): + return self.kern._get_param_names_transformed() + + def approximate_likelihood(self): + self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) + self.ep_approx.fit_EP() + self.tau_tilde = self.ep_approx.tau_tilde[:,None] + self.v_tilde = self.ep_approx.tau_tilde[:,None] + self.posterior_params() + self.Y = self.v_tilde/self.tau_tilde + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + #self.YYT = np.dot(self.Y, self.Y.T) + + def posterior_params(self): + self.Sroot_tilde_K = np.sqrt(self.tau_tilde.flatten())[:,None]*self.K + B = np.eye(self.N) + np.sqrt(self.tau_tilde.flatten())[None,:]*self.Sroot_tilde_K + self.Bi,self.L,self.Li,B_logdet = pdinv(B) + V = np.dot(self.Li,self.Sroot_tilde_K) + #V,info = linalg.flapack.dtrtrs(self.L,self.Sroot_tilde_K,lower=1) + self.Sigma = self.K - np.dot(V.T,V) + self.mu = np.dot(self.Sigma,self.v_tilde.flatten()) + + + #def _model_fit_term(self): + # """ + # Computes the model fit using YYT if it's available + # """ + # if self.YYT is None: + # return -0.5*np.sum(np.square(np.dot(self.Li,self.Y))) + # else: + # return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) + + def log_likelihood(self): + mu_ = self.ep_approx.v_/self.ep_approx.tau_ + L1 =.5*sum(np.log(1+self.ep_approx.tau_tilde*1./self.ep_approx.tau_))-sum(np.log(np.diag(self.L))) + L2A =.5*np.sum((self.Sigma-np.diag(1./(self.ep_approx.tau_+self.ep_approx.tau_tilde))) * np.dot(self.ep_approx.v_tilde[:,None],self.ep_approx.v_tilde[None,:])) + L2B = .5*np.dot(mu_*(self.ep_approx.tau_/(self.ep_approx.tau_tilde+self.ep_approx.tau_)),self.ep_approx.tau_tilde*mu_ - 2*self.ep_approx.v_tilde) + L3 = sum(np.log(self.ep_approx.Z_hat)) + return L1 + L2A + L2B + L3 + + def dL_dK(self): #FIXME + if self.YYT is None: + alpha = np.dot(self.Ki,self.Y) + dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) + else: + dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) + + return dL_dK + + def _log_likelihood_gradients(self): #FIXME + return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) + + def predict(self,Xnew, slices=None, full_cov=False): + """ + + Predict the function(s) at the new point(s) Xnew. + + Arguments + --------- + :param Xnew: The points at which to make a prediction + :type Xnew: np.ndarray, Nnew x self.Q + :param slices: specifies which outputs kernel(s) the Xnew correspond to (see below) + :type slices: (None, list of slice objects, list of ints) + :param full_cov: whether to return the folll covariance matrix, or just the diagonal + :type full_cov: bool + :rtype: posterior mean, a Numpy array, Nnew x self.D + :rtype: posterior variance, a Numpy array, Nnew x Nnew x (self.D) + + .. Note:: "slices" specifies how the the points X_new co-vary wich the training points. + + - If None, the new points covary throigh every kernel part (default) + - If a list of slices, the i^th slice specifies which data are affected by the i^th kernel part + - If a list of booleans, specifying which kernel parts are active + + If full_cov and self.D > 1, the return shape of var is Nnew x Nnew x self.D. If self.D == 1, the return shape is Nnew x Nnew. + This is to allow for different normalisations of the output dimensions. + + + """ + + #normalise X values + Xnew = (Xnew.copy() - self._Xmean) / self._Xstd + mu, var, phi = self._raw_predict(Xnew, slices, full_cov) + + #un-normalise + mu = mu*self._Ystd + self._Ymean + if full_cov: + if self.D==1: + var *= np.square(self._Ystd) + else: + var = var[:,:,None] * np.square(self._Ystd) + else: + if self.D==1: + var *= np.square(np.squeeze(self._Ystd)) + else: + var = var[:,None] * np.square(self._Ystd) + + return mu,var,phi + + def _raw_predict(self,_Xnew,slices, full_cov=False): + """Internal helper function for making predictions, does not account for normalisation""" + """ + Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) + mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) + KiKx = np.dot(self.Ki,Kx) + if full_cov: + Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) + var = Kxx - np.dot(KiKx.T,Kx) + else: + Kxx = self.kern.Kdiag(_Xnew, slices=slices) + var = Kxx - np.sum(np.multiply(KiKx,Kx),0) + return mu, var + """ + K_x = self.kern.K(self.X,_Xnew) + Kxx = self.kern.K(_Xnew) + #aux1,info = linalg.flapack.dtrtrs(self.L,np.dot(self.Sroot_tilde_K,self.ep_approx.v_tilde),lower=1) + #aux2,info = linalg.flapack.dtrtrs(self.L.T, aux1,lower=0) + #aux2 = mdot(self.Li.T,self.Li,self.Sroot_tilde_K,self.ep_approx.v_tilde) + aux2 = mdot(self.Bi,self.Sroot_tilde_K,self.ep_approx.v_tilde) + zeta = np.sqrt(self.ep_approx.tau_tilde)*aux2 + f = np.dot(K_x.T,self.ep_approx.v_tilde-zeta) + #v,info = linalg.flapack.dtrtrs(self.L,np.sqrt(self.ep_approx.tau_tilde)[:,None]*K_x,lower=1) + v = mdot(self.Li,np.sqrt(self.ep_approx.tau_tilde)[:,None]*K_x) + variance = Kxx - np.dot(v.T,v) + vdiag = np.diag(variance) + y=self.likelihood.predictive_mean(f,vdiag) + return f,vdiag,y + + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): + """ + :param samples: the number of a posteriori samples to plot + :param which_data: which if the training data to plot (default all) + :type which_data: 'all' or a slice object to slice self.X, self.Y + :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits + :param which_functions: which of the kernel functions to plot (additively) + :type which_functions: list of bools + :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D + + Plot the posterior of the GP. + - In one dimension, the function is plotted with a shaded region identifying two standard deviations. + - In two dimsensions, a contour-plot shows the mean predicted function + - In higher dimensions, we've no implemented this yet !TODO! + + Can plot only part of the data and part of the posterior functions using which_data and which_functions + """ + if which_functions=='all': + which_functions = [True]*self.kern.Nparts + if which_data=='all': + which_data = slice(None) + + X = self.X[which_data,:] + Y = self.Y[which_data,:] + + Xorig = X*self._Xstd + self._Xmean + Yorig = Y*self._Ystd + self._Ymean + if plot_limits is None: + xmin,xmax = Xorig.min(0),Xorig.max(0) + xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) + elif len(plot_limits)==2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + + if self.X.shape[1]==1: + Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] + #m,v,phi = self.predict(Xnew,slices=which_functions) + #gpplot(Xnew,m,v) + mu_f, var_f, phi_f = self.predict(Xnew,slices=which_functions) + pb.subplot(211) + self.likelihood.plot1Da(X_new=Xnew,Mean_new=mu_f,Var_new=var_f,X_u=self.X,Mean_u=self.mu,Var_u=np.diag(self.Sigma)) + if samples: + s = np.random.multivariate_normal(m.flatten(),v,samples) + pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) + pb.xlim(xmin,xmax) + pb.subplot(212) + self.likelihood.plot1Db(self.X,Xnew,phi_f) + + elif self.X.shape[1]==2: + resolution = 50 or resolution + xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + Xtest = np.vstack((xx.flatten(),yy.flatten())).T + zz,vv = self.predict(Xtest,slices=which_functions) + zz = zz.reshape(resolution,resolution) + pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + pb.xlim(xmin[0],xmax[0]) + pb.ylim(xmin[1],xmax[1]) + + else: + raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index ab7ff5b4..5f824f2b 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -7,6 +7,7 @@ from sparse_GP_regression import sparse_GP_regression from GPLVM import GPLVM from warped_GP import warpedGP from GP_EP import GP_EP +from GP_EP2 import GP_EP2 from generalized_FITC import generalized_FITC from sparse_GPLVM import sparse_GPLVM from uncollapsed_sparse_GP import uncollapsed_sparse_GP From 6a2e0a1fe554dfe00036b6fdef82c9d437bff3f0 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 25 Jan 2013 18:14:28 +0000 Subject: [PATCH 002/197] fixing EP and merging it with GP_regression --- GPy/examples/ep_fix.py | 11 +- GPy/inference/EP.py | 12 +- GPy/inference/likelihoods.py | 31 ++-- GPy/models/GP.py | 312 +++++++++++++++++++++++++++++++++++ GPy/models/GP_EP.py | 2 +- GPy/models/GP_EP2.py | 127 +++++++------- GPy/models/__init__.py | 1 + 7 files changed, 403 insertions(+), 93 deletions(-) create mode 100644 GPy/models/GP.py diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index e4999f30..2da94335 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -25,14 +25,15 @@ seed=default_seed data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) -m = GPy.models.GP_EP2(data['X'],likelihood) +m = GPy.models.GP(data['X'],likelihood=likelihood) -#m.constrain_positive('var') -#m.constrain_positive('len') -#m.tie_param('lengthscale') +m.constrain_positive('var') +m.constrain_positive('len') +m.tie_param('lengthscale') m.approximate_likelihood() +print m.checkgrad() # Optimize and plot -#m.optimize() +m.optimize() #m.em(plot_all=False) # EM algorithm m.plot() diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py index fa691961..f7c163b1 100644 --- a/GPy/inference/EP.py +++ b/GPy/inference/EP.py @@ -60,7 +60,7 @@ class Full(EP): def fit_EP(self): """ The expectation-propagation algorithm. - For nomenclature see Rasmussen & Williams 2006 (pag. 52-60) + For nomenclature see Rasmussen & Williams 2006. """ #Prior distribution parameters: p(f|X) = N(f|0,K) #self.K = self.kernel.K(self.X,self.X) @@ -84,8 +84,6 @@ class Full(EP): phi = np.empty(self.N,dtype=float) mu_hat = np.empty(self.N,dtype=float) sigma2_hat = np.empty(self.N,dtype=float) - self.mu_hat = mu_hat #TODO erase me - self.sigma2_hat = sigma2_hat #TODO erase me #Approximation epsilon_np1 = self.epsilon + 1. @@ -95,21 +93,16 @@ class Full(EP): self.np2 = [self.v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: update_order = np.arange(self.N) - #random.shuffle(update_order) #TODO uncomment + random.shuffle(update_order) for i in update_order: #Cavity distribution parameters self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] self.v_[i] = self.mu[i]/self.Sigma[i,i] - self.eta*self.v_tilde[i] #Marginal moments self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) - self.mu_hat[i] = mu_hat[i] #TODO erase me - self.sigma2_hat[i] = sigma2_hat[i] #TODO erase me - #if i == 3: - # a = b #Site parameters update Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma[i,i]) Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma[i,i]) - print Delta_tau self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau self.v_tilde[i] = self.v_tilde[i] + Delta_v #Posterior distribution parameters update @@ -128,6 +121,7 @@ class Full(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) + return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] class DTC(EP): def fit_EP(self): diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index ff4770f6..29e194e0 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -19,7 +19,7 @@ class likelihood: self.Y = Y self.N = self.Y.shape[0] - def plot1Da(self,X_new,Mean_new,Var_new,X_u,Mean_u,Var_u): + def plot1Da(self,X,mean,var,Z=None,mean_Z=None,var_Z=None): """ Plot the predictive distribution of the GP model for 1-dimensional inputs @@ -30,10 +30,18 @@ class likelihood: :param Mean_u: mean values at X_u :param Var_new: variance values at X_u """ - assert X_new.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X_new,Mean_new,Var_new) - pb.errorbar(X_u.flatten(),Mean_u.flatten(),2*np.sqrt(Var_u.flatten()),fmt='r+') - pb.plot(X_u,Mean_u,'ro') + assert X.shape[1] == 1, 'Number of dimensions must be 1' + gpplot(X,mean,var.flatten()) + pb.errorbar(Z.flatten(),mean_Z.flatten(),2*np.sqrt(var_Z.flatten()),fmt='r+') + pb.plot(Z,mean_Z,'ro') + + def plot1Db(self,X_obs,X,phi,Z=None): + assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' + gpplot(X,phi,np.zeros(X.shape[0])) + pb.plot(X_obs,(self.Y+1)/2,'kx',mew=1.5) + pb.ylim(-0.2,1.2) + if Z is not None: + pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) def plot2D(self,X,X_new,F_new,U=None): """ @@ -88,16 +96,11 @@ class probit(likelihood): sigma2_hat = 1./tau_i - (phi/((tau_i**2+tau_i)*Z_hat))*(z+phi/Z_hat) return Z_hat, mu_hat, sigma2_hat - def plot1Db(self,X,X_new,F_new,U=None): - assert X.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X_new,F_new,np.zeros(X_new.shape[0])) - pb.plot(X,(self.Y+1)/2,'kx',mew=1.5) - pb.ylim(-0.2,1.2) - if U is not None: - pb.plot(U,U*0+.5,'r|',mew=1.5,markersize=12) - def predictive_mean(self,mu,variance): - return stats.norm.cdf(mu/np.sqrt(1+variance)) + def predictive_mean(self,mu,var): + mu = mu.flatten() + var = var.flatten() + return stats.norm.cdf(mu/np.sqrt(1+var)) def _log_likelihood_gradients(): raise NotImplementedError diff --git a/GPy/models/GP.py b/GPy/models/GP.py new file mode 100644 index 00000000..4a8d23e9 --- /dev/null +++ b/GPy/models/GP.py @@ -0,0 +1,312 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +import numpy as np +import pylab as pb +from .. import kern +from ..core import model +from ..util.linalg import pdinv,mdot +from ..util.plot import gpplot, Tango +from ..inference.EP import Full +from ..inference.likelihoods import likelihood,probit,poisson,gaussian + +class GP(model): + """ + Gaussian Process model for regression + + :param X: input observations + :param Y: observed values + :param kernel: a GPy kernel, defaults to rbf+white + :param normalize_X: whether to normalize the input data before computing (predictions will be in original scales) + :type normalize_X: False|True + :param normalize_Y: whether to normalize the input data before computing (predictions will be in original scales) + :type normalize_Y: False|True + :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) + :rtype: model object + + .. Note:: Multiple independent outputs are allowed using columns of Y + + """ + + def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsion_em=.1,powerep=[1.,1.]): + #TODO: specify beta parameter explicitely + + # parse arguments + self.Xslices = Xslices + self.X = X + self.N, self.Q = self.X.shape + assert len(self.X.shape)==2 + if kernel is None: + kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) + else: + assert isinstance(kernel, kern.kern) + self.kern = kernel + + #here's some simple normalisation + if normalize_X: + self._Xmean = X.mean(0)[None,:] + self._Xstd = X.std(0)[None,:] + self.X = (X.copy() - self._Xmean) / self._Xstd + if hasattr(self,'Z'): + self.Z = (self.Z - self._Xmean) / self._Xstd + else: + self._Xmean = np.zeros((1,self.X.shape[1])) + self._Xstd = np.ones((1,self.X.shape[1])) + + + # Y - likelihood related variables, these might change whether using EP or not + if likelihood is None: + assert Y is not None, "Either Y or likelihood must be defined" + self.likelihood = gaussian(Y) + else: + self.likelihood = likelihood + assert len(self.likelihood.Y.shape)==2 + assert self.X.shape[0] == self.likelihood.Y.shape[0] + self.N, self.D = self.likelihood.Y.shape + + if isinstance(self.likelihood,gaussian): + self.EP = False + self.Y = Y + + #here's some simple normalisation + if normalize_Y: + self._Ymean = Y.mean(0)[None,:] + self._Ystd = Y.std(0)[None,:] + self.Y = (Y.copy()- self._Ymean) / self._Ystd + else: + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + + if self.D > self.N: + # then it's more efficient to store YYT + self.YYT = np.dot(self.Y, self.Y.T) + else: + self.YYT = None + + else: + # Y is defined after approximating the likelihood + self.EP = True + self.eta,self.delta = powerep + self.epsilon_ep = epsilon_ep + self.tau_tilde = np.ones([self.N,self.D]) + self.v_tilde = np.zeros([self.N,self.D]) + self.tau_ = np.ones([self.N,self.D]) + self.v_ = np.zeros([self.N,self.D]) + self.Z_hat = np.ones([self.N,self.D]) + + model.__init__(self) + + def _set_params(self,p): + # TODO: remove beta when using EP + self.kern._set_params_transformed(p) + if not self.EP: + self.K = self.kern.K(self.X,slices1=self.Xslices) + self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) + else: + self._ep_covariance() + + def _get_params(self): + # TODO: remove beta when using EP + return self.kern._get_params_transformed() + + def _get_param_names(self): + # TODO: remove beta when using EP + return self.kern._get_param_names_transformed() + + def approximate_likelihood(self): + assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" + self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) + self.tau_tilde, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() + # Y: EP likelihood is defined as a regression model for mu_tilde + self.Y = self.v_tilde/self.tau_tilde + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + if self.D > self.N: + # then it's more efficient to store YYT + self.YYT = np.dot(self.Y, self.Y.T) + else: + self.YYT = None + self.mu_ = self.v_/self.tau_ + self._ep_covariance() + + def _ep_covariance(self): + # Kernel plus noise variance term + self.K = self.kern.K(self.X,slices1=self.Xslices) + np.diag(1./self.tau_tilde.flatten()) + self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) + + def _model_fit_term(self): + """ + Computes the model fit using YYT if it's available + """ + if self.YYT is None: + return -0.5*np.sum(np.square(np.dot(self.Li,self.Y))) + else: + return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) + + def _normalization_term(self): + """ + Computes the marginal likelihood normalization constants + """ + sigma_sum = 1./self.tau_ + 1./self.tau_tilde + mu_diff_2 = (self.mu_ - self.Y)**2 + penalty_term = np.sum(np.log(self.Z_hat)) + return penalty_term + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) + + def log_likelihood(self): + """ + The log marginal likelihood for an EP model can be written as the log likelihood of + a regression model for a new variable Y* = v_tilde/tau_tilde, with a covariance + matrix K* = K + diag(1./tau_tilde) plus a normalization term. + """ + complexity_term = -0.5*self.D*self.Kplus_logdet + normalization_term = 0 if self.EP == False else self.normalization_term() + return complexity_term + normalization_term + self._model_fit_term() + + + def log_likelihood(self): + complexity_term = -0.5*self.N*self.D*np.log(2.*np.pi) - 0.5*self.D*self.K_logdet + return complexity_term + self._model_fit_term() + + def dL_dK(self): + if self.YYT is None: + alpha = np.dot(self.Ki,self.Y) + dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) + else: + dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) + + return dL_dK + + def _log_likelihood_gradients(self): + return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) + + def predict(self,Xnew, slices=None, full_cov=False): + """ + + Predict the function(s) at the new point(s) Xnew. + + Arguments + --------- + :param Xnew: The points at which to make a prediction + :type Xnew: np.ndarray, Nnew x self.Q + :param slices: specifies which outputs kernel(s) the Xnew correspond to (see below) + :type slices: (None, list of slice objects, list of ints) + :param full_cov: whether to return the folll covariance matrix, or just the diagonal + :type full_cov: bool + :rtype: posterior mean, a Numpy array, Nnew x self.D + :rtype: posterior variance, a Numpy array, Nnew x Nnew x (self.D) + + .. Note:: "slices" specifies how the the points X_new co-vary wich the training points. + + - If None, the new points covary throigh every kernel part (default) + - If a list of slices, the i^th slice specifies which data are affected by the i^th kernel part + - If a list of booleans, specifying which kernel parts are active + + If full_cov and self.D > 1, the return shape of var is Nnew x Nnew x self.D. If self.D == 1, the return shape is Nnew x Nnew. + This is to allow for different normalisations of the output dimensions. + + + """ + + #normalise X values + Xnew = (Xnew.copy() - self._Xmean) / self._Xstd + mu, var, phi = self._raw_predict(Xnew, slices, full_cov) + + #un-normalise + mu = mu*self._Ystd + self._Ymean + if full_cov: + if self.D==1: + var *= np.square(self._Ystd) + else: + var = var[:,:,None] * np.square(self._Ystd) + else: + if self.D==1: + var *= np.square(np.squeeze(self._Ystd)) + else: + var = var[:,None] * np.square(self._Ystd) + + return mu,var,phi + + def _raw_predict(self,_Xnew,slices, full_cov=False): + """Internal helper function for making predictions, does not account for normalisation""" + Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) + mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) + KiKx = np.dot(self.Ki,Kx) + if full_cov: + Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) + var = Kxx - np.dot(KiKx.T,Kx) + else: + Kxx = self.kern.Kdiag(_Xnew, slices=slices) + var = Kxx - np.sum(np.multiply(KiKx,Kx),0) + phi = None if not self.EP else self.likelihood.predictive_mean(mu,var) + return mu, var, phi + + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): + """ + :param samples: the number of a posteriori samples to plot + :param which_data: which if the training data to plot (default all) + :type which_data: 'all' or a slice object to slice self.X, self.Y + :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits + :param which_functions: which of the kernel functions to plot (additively) + :type which_functions: list of bools + :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D + + Plot the posterior of the GP. + - In one dimension, the function is plotted with a shaded region identifying two standard deviations. + - In two dimsensions, a contour-plot shows the mean predicted function + - In higher dimensions, we've no implemented this yet !TODO! + + Can plot only part of the data and part of the posterior functions using which_data and which_functions + """ + if which_functions=='all': + which_functions = [True]*self.kern.Nparts + if which_data=='all': + which_data = slice(None) + + X = self.X[which_data,:] + Y = self.Y[which_data,:] + + Xorig = X*self._Xstd + self._Xmean + Yorig = Y*self._Ystd + self._Ymean if not self.EP else self.likelihood.Y + + if plot_limits is None: + xmin,xmax = Xorig.min(0),Xorig.max(0) + xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) + elif len(plot_limits)==2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + + if self.X.shape[1]==1: + Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] + m,v,phi = self.predict(Xnew,slices=which_functions) + if self.EP: + pb.subplot(211) + + gpplot(Xnew,m,v) + if samples: + s = np.random.multivariate_normal(m.flatten(),v,samples) + pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) + + if not self.EP: + pb.plot(Xorig,Yorig,'kx',mew=1.5) + pb.xlim(xmin,xmax) + else: + pb.xlim(xmin,xmax) + pb.subplot(212) + self.likelihood.plot1Db(self.X,Xnew,phi) + pb.xlim(xmin,xmax) + + elif self.X.shape[1]==2: + resolution = 50 or resolution + xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + Xtest = np.vstack((xx.flatten(),yy.flatten())).T + zz,vv,phi = self.predict(Xtest,slices=which_functions) + zz = zz.reshape(resolution,resolution) + pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + pb.xlim(xmin[0],xmax[0]) + pb.ylim(xmin[1],xmax[1]) + + else: + raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" diff --git a/GPy/models/GP_EP.py b/GPy/models/GP_EP.py index 302ff366..1c0b9cf6 100644 --- a/GPy/models/GP_EP.py +++ b/GPy/models/GP_EP.py @@ -62,7 +62,7 @@ class GP_EP(model): self.L = jitchol(B) V,info = linalg.flapack.dtrtrs(self.L,self.Sroot_tilde_K,lower=1) self.Sigma = self.K - np.dot(V.T,V) - self.mu = np.dot(self.Sigma,self.ep_approx.v_tilde) + self.mu = np.dot(self.Sigma,self.ep_approx.v_tilde) * self.Z_hat def log_likelihood(self): """ diff --git a/GPy/models/GP_EP2.py b/GPy/models/GP_EP2.py index c68e7b70..ce869951 100644 --- a/GPy/models/GP_EP2.py +++ b/GPy/models/GP_EP2.py @@ -36,14 +36,11 @@ class GP_EP2(model): self.Xslices = Xslices assert isinstance(kernel, kern.kern) self.likelihood = likelihood - #self.Y = self.likelihood.Y #we might not need this self.kern = kernel self.X = X assert len(self.X.shape)==2 - #assert len(self.Y.shape)==2 - #assert self.X.shape[0] == self.Y.shape[0] - #self.N, self.D = self.Y.shape - self.D = 1 + assert self.X.shape[0] == self.likelihood.Y.shape[0] + self.D = self.likelihood.Y.shape[1] self.N, self.Q = self.X.shape #here's some simple normalisation @@ -75,14 +72,17 @@ class GP_EP2(model): """ self.eta,self.delta = powerep self.epsilon_ep = epsilon_ep - self.tau_tilde = np.zeros([self.N,self.D]) + self.tau_tilde = np.ones([self.N,self.D]) self.v_tilde = np.zeros([self.N,self.D]) + self.tau_ = np.ones([self.N,self.D]) + self.v_ = np.zeros([self.N,self.D]) + self.Z_hat = np.ones([self.N,self.D]) model.__init__(self) def _set_params(self,p): self.kern._set_params_transformed(p) self.K = self.kern.K(self.X,slices1=self.Xslices) - self.posterior_params() + self._ep_params() def _get_params(self): return self.kern._get_params_transformed() @@ -92,52 +92,63 @@ class GP_EP2(model): def approximate_likelihood(self): self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) - self.ep_approx.fit_EP() - self.tau_tilde = self.ep_approx.tau_tilde[:,None] - self.v_tilde = self.ep_approx.tau_tilde[:,None] - self.posterior_params() - self.Y = self.v_tilde/self.tau_tilde - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - #self.YYT = np.dot(self.Y, self.Y.T) + self.tau_tilde, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() + self._ep_params() - def posterior_params(self): - self.Sroot_tilde_K = np.sqrt(self.tau_tilde.flatten())[:,None]*self.K + def _ep_params(self): + # Posterior mean and Variance computation + self.Sroot_tilde_K = np.sqrt(self.tau_tilde)*self.K B = np.eye(self.N) + np.sqrt(self.tau_tilde.flatten())[None,:]*self.Sroot_tilde_K self.Bi,self.L,self.Li,B_logdet = pdinv(B) V = np.dot(self.Li,self.Sroot_tilde_K) - #V,info = linalg.flapack.dtrtrs(self.L,self.Sroot_tilde_K,lower=1) - self.Sigma = self.K - np.dot(V.T,V) - self.mu = np.dot(self.Sigma,self.v_tilde.flatten()) + self.Sigma = self.K - np.dot(V.T,V) #posterior variance + self.mu = np.dot(self.Sigma,self.v_tilde) #posterior mean + # Kernel plus noise variance term + self.Kplus = self.K + np.diag(1./self.tau_tilde.flatten()) + self.Kplusi,self.Lplus,self.Lplusi,self.Kplus_logdet = pdinv(self.Kplus) + # Y: EP likelihood is defined as a regression model for mu_tilde + self.Y = self.v_tilde/self.tau_tilde + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + self.YYT = None #np.dot(self.Y, self.Y.T) + self.mu_ = self.v_/self.tau_ + def _model_fit_term(self): + """ + Computes the model fit using YYT if it's available + """ + if self.YYT is None: + return -0.5*np.sum(np.square(np.dot(self.Lplusi,self.Y))) + else: + return -0.5*np.sum(np.multiply(self.Kplusi, self.YYT)) - #def _model_fit_term(self): - # """ - # Computes the model fit using YYT if it's available - # """ - # if self.YYT is None: - # return -0.5*np.sum(np.square(np.dot(self.Li,self.Y))) - # else: - # return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) + def _normalization_term(self): + """ + Computes the marginal likelihood normalization constants + """ + sigma_sum = 1./self.tau_ + 1./self.tau_tilde + mu_diff_2 = (self.mu_ - self.Y)**2 + penalty_term = np.sum(np.log(self.Z_hat)) + return penalty_term + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) def log_likelihood(self): - mu_ = self.ep_approx.v_/self.ep_approx.tau_ - L1 =.5*sum(np.log(1+self.ep_approx.tau_tilde*1./self.ep_approx.tau_))-sum(np.log(np.diag(self.L))) - L2A =.5*np.sum((self.Sigma-np.diag(1./(self.ep_approx.tau_+self.ep_approx.tau_tilde))) * np.dot(self.ep_approx.v_tilde[:,None],self.ep_approx.v_tilde[None,:])) - L2B = .5*np.dot(mu_*(self.ep_approx.tau_/(self.ep_approx.tau_tilde+self.ep_approx.tau_)),self.ep_approx.tau_tilde*mu_ - 2*self.ep_approx.v_tilde) - L3 = sum(np.log(self.ep_approx.Z_hat)) - return L1 + L2A + L2B + L3 + """ + The log marginal likelihood for an EP model can be written as the log likelihood of + a regression model for a new variable Y* = v_tilde/tau_tilde, with a covariance + matrix K* = K + diag(1./tau_tilde) plus a normalization term. + """ + complexity_term = -0.5*self.D*self.Kplus_logdet + return complexity_term + self._model_fit_term() + self._normalization_term() - def dL_dK(self): #FIXME + def dL_dK(self): if self.YYT is None: - alpha = np.dot(self.Ki,self.Y) - dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) + alpha = np.dot(self.Kplusi,self.Y) + dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Kplusi) else: - dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) - + dL_dK = 0.5*(mdot(self.Kplusi, self.YYT, self.Kplusi) - self.D*self.Kplusi) return dL_dK - def _log_likelihood_gradients(self): #FIXME + def _log_likelihood_gradients(self): return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) def predict(self,Xnew, slices=None, full_cov=False): @@ -189,32 +200,20 @@ class GP_EP2(model): def _raw_predict(self,_Xnew,slices, full_cov=False): """Internal helper function for making predictions, does not account for normalisation""" - """ - Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) - mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) - KiKx = np.dot(self.Ki,Kx) + K_x = self.kern.K(self.X,_Xnew,slices1=self.Xslices,slices2=slices) + aux2 = mdot(self.Bi,self.Sroot_tilde_K,self.v_tilde) + zeta = np.sqrt(self.tau_tilde)*aux2 + f = np.dot(K_x.T,self.v_tilde-zeta) + v = mdot(self.Li,np.sqrt(self.tau_tilde)*K_x) if full_cov: - Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) - var = Kxx - np.dot(KiKx.T,Kx) + Kxx = self.kern.K(_Xnew,slices1=slices,slices2=slices) + var = Kxx - np.dot(v.T,v) + var_diag = np.diag(var)[:,None] else: Kxx = self.kern.Kdiag(_Xnew, slices=slices) - var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - return mu, var - """ - K_x = self.kern.K(self.X,_Xnew) - Kxx = self.kern.K(_Xnew) - #aux1,info = linalg.flapack.dtrtrs(self.L,np.dot(self.Sroot_tilde_K,self.ep_approx.v_tilde),lower=1) - #aux2,info = linalg.flapack.dtrtrs(self.L.T, aux1,lower=0) - #aux2 = mdot(self.Li.T,self.Li,self.Sroot_tilde_K,self.ep_approx.v_tilde) - aux2 = mdot(self.Bi,self.Sroot_tilde_K,self.ep_approx.v_tilde) - zeta = np.sqrt(self.ep_approx.tau_tilde)*aux2 - f = np.dot(K_x.T,self.ep_approx.v_tilde-zeta) - #v,info = linalg.flapack.dtrtrs(self.L,np.sqrt(self.ep_approx.tau_tilde)[:,None]*K_x,lower=1) - v = mdot(self.Li,np.sqrt(self.ep_approx.tau_tilde)[:,None]*K_x) - variance = Kxx - np.dot(v.T,v) - vdiag = np.diag(variance) - y=self.likelihood.predictive_mean(f,vdiag) - return f,vdiag,y + var_diag = (Kxx - np.sum(v**2,-2))[:,None] + phi = self.likelihood.predictive_mean(f,var_diag) + return f, var_diag, phi def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): """ @@ -257,7 +256,7 @@ class GP_EP2(model): #gpplot(Xnew,m,v) mu_f, var_f, phi_f = self.predict(Xnew,slices=which_functions) pb.subplot(211) - self.likelihood.plot1Da(X_new=Xnew,Mean_new=mu_f,Var_new=var_f,X_u=self.X,Mean_u=self.mu,Var_u=np.diag(self.Sigma)) + self.likelihood.plot1Da(X=Xnew,mean=mu_f,var=var_f,Z=self.X,mean_Z=self.mu,var_Z=np.diag(self.Sigma)) if samples: s = np.random.multivariate_normal(m.flatten(),v,samples) pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index 5f824f2b..ca44aab1 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -11,3 +11,4 @@ from GP_EP2 import GP_EP2 from generalized_FITC import generalized_FITC from sparse_GPLVM import sparse_GPLVM from uncollapsed_sparse_GP import uncollapsed_sparse_GP +from GP import GP From 738ca78dac64b0806eeea7bd247849db751e565b Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 25 Jan 2013 18:24:10 +0000 Subject: [PATCH 003/197] No more GP_EP stuff --- GPy/inference/Expectation_Propagation.py | 240 ------------------- GPy/models/GP_EP.py | 160 ------------- GPy/models/GP_EP2.py | 279 ----------------------- GPy/models/__init__.py | 2 - GPy/models/generalized_FITC.py | 3 +- 5 files changed, 2 insertions(+), 682 deletions(-) delete mode 100644 GPy/inference/Expectation_Propagation.py delete mode 100644 GPy/models/GP_EP.py delete mode 100644 GPy/models/GP_EP2.py diff --git a/GPy/inference/Expectation_Propagation.py b/GPy/inference/Expectation_Propagation.py deleted file mode 100644 index 520fc607..00000000 --- a/GPy/inference/Expectation_Propagation.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - - -import numpy as np -import random -from scipy import stats, linalg -from .likelihoods import likelihood -from ..core import model -from ..util.linalg import pdinv,mdot,jitchol -from ..util.plot import gpplot -from .. import kern - -class EP_base: - """ - Expectation Propagation. - - This is just the base class for expectation propagation. We'll extend it for full and sparse EP. - """ - def __init__(self,likelihood,epsilon=1e-3,powerep=[1.,1.]): - self.likelihood = likelihood - self.epsilon = epsilon - self.eta, self.delta = powerep - self.jitter = 1e-12 - - #Initial values - Likelihood approximation parameters: - #p(y|f) = t(f|tau_tilde,v_tilde) - self.restart_EP() - - def restart_EP(self): - """ - Set the EP approximation to initial state - """ - self.tau_tilde = np.zeros(self.N) - self.v_tilde = np.zeros(self.N) - self.mu = np.zeros(self.N) - -class Full(EP_base): - """ - :param likelihood: Output's likelihood (e.g. probit) - :type likelihood: GPy.inference.likelihood instance - :param K: prior covariance matrix - :type K: np.ndarray (N x N) - :param likelihood: Output's likelihood (e.g. probit) - :type likelihood: GPy.inference.likelihood instance - :param epsilon: Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) - :param powerep: Power-EP parameters (eta,delta) - 2x1 numpy array (floats) - """ - def __init__(self,K,likelihood,*args,**kwargs): - assert K.shape[0] == K.shape[1] - self.K = K - self.N = self.K.shape[0] - EP_base.__init__(self,likelihood,*args,**kwargs) - def fit_EP(self,messages=False): - """ - The expectation-propagation algorithm. - For nomenclature see Rasmussen & Williams 2006 (pag. 52-60) - """ - #Prior distribution parameters: p(f|X) = N(f|0,K) - #self.K = self.kernel.K(self.X,self.X) - - #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) - self.mu=np.zeros(self.N) - self.Sigma=self.K.copy() - - """ - Initial values - Cavity distribution parameters: - q_(f|mu_,sigma2_) = Product{q_i(f|mu_i,sigma2_i)} - sigma_ = 1./tau_ - mu_ = v_/tau_ - """ - - self.tau_ = np.empty(self.N,dtype=np.float64) - self.v_ = np.empty(self.N,dtype=np.float64) - - #Initial values - Marginal moments - z = np.empty(self.N,dtype=np.float64) - self.Z_hat = np.empty(self.N,dtype=np.float64) - phi = np.empty(self.N,dtype=np.float64) - mu_hat = np.empty(self.N,dtype=np.float64) - sigma2_hat = np.empty(self.N,dtype=np.float64) - - #Approximation - epsilon_np1 = self.epsilon + 1. - epsilon_np2 = self.epsilon + 1. - self.iterations = 0 - self.np1 = [self.tau_tilde.copy()] - self.np2 = [self.v_tilde.copy()] - while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.random.permutation(self.N) - for i in update_order: - #Cavity distribution parameters - self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] - self.v_[i] = self.mu[i]/self.Sigma[i,i] - self.eta*self.v_tilde[i] - #Marginal moments - self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) - #Site parameters update - Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma[i,i]) - Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma[i,i]) - self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau - self.v_tilde[i] = self.v_tilde[i] + Delta_v - #Posterior distribution parameters update - si=self.Sigma[:,i].reshape(self.N,1) - self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) - self.mu = np.dot(self.Sigma,self.v_tilde) - self.iterations += 1 - #Sigma recomptutation with Cholesky decompositon - Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*(self.K) - B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K - L = jitchol(B) - V,info = linalg.flapack.dtrtrs(L,Sroot_tilde_K,lower=1) - self.Sigma = self.K - np.dot(V.T,V) - self.mu = np.dot(self.Sigma,self.v_tilde) - epsilon_np1 = np.mean(self.tau_tilde-self.np1[-1]**2) - epsilon_np2 = np.mean(self.v_tilde-self.np2[-1]**2) - self.np1.append(self.tau_tilde.copy()) - self.np2.append(self.v_tilde.copy()) - if messages: - print "EP iteration %i, epsilon %d"%(self.iterations,epsilon_np1) - -class FITC(EP_base): - """ - :param likelihood: Output's likelihood (e.g. probit) - :type likelihood: GPy.inference.likelihood instance - :param Knn_diag: The diagonal elements of Knn is a 1D vector - :param Kmn: The 'cross' variance between inducing inputs and data - :param Kmm: the covariance matrix of the inducing inputs - :param likelihood: Output's likelihood (e.g. probit) - :type likelihood: GPy.inference.likelihood instance - :param epsilon: Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) - :param powerep: Power-EP parameters (eta,delta) - 2x1 numpy array (floats) - """ - def __init__(self,likelihood,Knn_diag,Kmn,Kmm,*args,**kwargs): - self.Knn_diag = Knn_diag - self.Kmn = Kmn - self.Kmm = Kmm - self.M = self.Kmn.shape[0] - self.N = self.Kmn.shape[1] - assert self.M <= self.N, 'The number of inducing inputs must be smaller than the number of observations' - assert len(Knn_diag) == self.N, 'Knn_diagonal has size different from N' - EP_base.__init__(self,likelihood,*args,**kwargs) - - def fit_EP(self): - """ - The expectation-propagation algorithm with sparse pseudo-input. - For nomenclature see Naish-Guzman and Holden, 2008. - """ - - """ - Prior approximation parameters: - q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) - Sigma0 = diag(Knn-Qnn) + Qnn, Qnn = Knm*Kmmi*Kmn - """ - self.Kmmi, self.Kmm_hld = pdinv(self.Kmm) - self.P0 = self.Kmn.T - self.KmnKnm = np.dot(self.P0.T, self.P0) - self.KmmiKmn = np.dot(self.Kmmi,self.P0.T) - self.Qnn_diag = np.sum(self.P0.T*self.KmmiKmn,-2) - self.Diag0 = self.Knn_diag - self.Qnn_diag - self.R0 = jitchol(self.Kmmi).T - - """ - Posterior approximation: q(f|y) = N(f| mu, Sigma) - Sigma = Diag + P*R.T*R*P.T + K - mu = w + P*gamma - """ - self.w = np.zeros(self.N) - self.gamma = np.zeros(self.M) - self.mu = np.zeros(self.N) - self.P = self.P0.copy() - self.R = self.R0.copy() - self.Diag = self.Diag0.copy() - self.Sigma_diag = self.Knn_diag - - """ - Initial values - Cavity distribution parameters: - q_(g|mu_,sigma2_) = Product{q_i(g|mu_i,sigma2_i)} - sigma_ = 1./tau_ - mu_ = v_/tau_ - """ - self.tau_ = np.empty(self.N,dtype=np.float64) - self.v_ = np.empty(self.N,dtype=np.float64) - - #Initial values - Marginal moments - z = np.empty(self.N,dtype=np.float64) - self.Z_hat = np.empty(self.N,dtype=np.float64) - phi = np.empty(self.N,dtype=np.float64) - mu_hat = np.empty(self.N,dtype=np.float64) - sigma2_hat = np.empty(self.N,dtype=np.float64) - - #Approximation - epsilon_np1 = 1 - epsilon_np2 = 1 - self.iterations = 0 - self.np1 = [self.tau_tilde.copy()] - self.np2 = [self.v_tilde.copy()] - while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) - for i in update_order: - #Cavity distribution parameters - self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] - self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] - #Marginal moments - self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) - #Site parameters update - Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) - Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) - self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau - self.v_tilde[i] = self.v_tilde[i] + Delta_v - #Posterior distribution parameters update - dtd1 = Delta_tau*self.Diag[i] + 1. - dii = self.Diag[i] - self.Diag[i] = dii - (Delta_tau * dii**2.)/dtd1 - pi_ = self.P[i,:].reshape(1,self.M) - self.P[i,:] = pi_ - (Delta_tau*dii)/dtd1 * pi_ - Rp_i = np.dot(self.R,pi_.T) - RTR = np.dot(self.R.T,np.dot(np.eye(self.M) - Delta_tau/(1.+Delta_tau*self.Sigma_diag[i]) * np.dot(Rp_i,Rp_i.T),self.R)) - self.R = jitchol(RTR).T - self.w[i] = self.w[i] + (Delta_v - Delta_tau*self.w[i])*dii/dtd1 - self.gamma = self.gamma + (Delta_v - Delta_tau*self.mu[i])*np.dot(RTR,self.P[i,:].T) - self.RPT = np.dot(self.R,self.P.T) - self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) - self.mu = self.w + np.dot(self.P,self.gamma) - self.iterations += 1 - #Sigma recomptutation with Cholesky decompositon - self.Diag = self.Diag0/(1.+ self.Diag0 * self.tau_tilde) - self.P = (self.Diag / self.Diag0)[:,None] * self.P0 - self.RPT0 = np.dot(self.R0,self.P0.T) - L = jitchol(np.eye(self.M) + np.dot(self.RPT0,(1./self.Diag0 - self.Diag/(self.Diag0**2))[:,None]*self.RPT0.T)) - self.R,info = linalg.flapack.dtrtrs(L,self.R0,lower=1) - self.RPT = np.dot(self.R,self.P.T) - self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) - self.w = self.Diag * self.v_tilde - self.gamma = np.dot(self.R.T, np.dot(self.RPT,self.v_tilde)) - self.mu = self.w + np.dot(self.P,self.gamma) - epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N - epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N - self.np1.append(self.tau_tilde.copy()) - self.np2.append(self.v_tilde.copy()) diff --git a/GPy/models/GP_EP.py b/GPy/models/GP_EP.py deleted file mode 100644 index 1c0b9cf6..00000000 --- a/GPy/models/GP_EP.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - - -import numpy as np -import pylab as pb -from scipy import stats, linalg -from .. import kern -from ..inference.Expectation_Propagation import Full -from ..inference.likelihoods import likelihood,probit#,poisson,gaussian -from ..core import model -from ..util.linalg import pdinv,jitchol -from ..util.plot import gpplot - -class GP_EP(model): - def __init__(self,X,likelihood,kernel=None,epsilon_ep=1e-3,epsion_em=.1,powerep=[1.,1.]): - """ - Simple Gaussian Process with Non-Gaussian likelihood - - Arguments - --------- - :param X: input observations (NxD numpy.darray) - :param likelihood: a GPy likelihood (likelihood class) - :param kernel: a GPy kernel (kern class) - :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 (float) - :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] (list) - :rtype: GPy model class. - """ - if kernel is None: - kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) - - assert isinstance(kernel,kern.kern), 'kernel is not a kern instance' - self.likelihood = likelihood - self.Y = self.likelihood.Y - self.kernel = kernel - self.X = X - self.N, self.D = self.X.shape - self.eta,self.delta = powerep - self.epsilon_ep = epsilon_ep - self.jitter = 1e-12 - self.K = self.kernel.K(self.X) - model.__init__(self) - - def _set_params(self,p): - self.kernel._set_params_transformed(p) - - def _get_params(self): - return self.kernel._get_params_transformed() - - def _get_param_names(self): - return self.kernel._get_param_names_transformed() - - def approximate_likelihood(self): - self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) - self.ep_approx.fit_EP() - - def posterior_param(self): - self.K = self.kernel.K(self.X) - self.Sroot_tilde_K = np.sqrt(self.ep_approx.tau_tilde)[:,None]*self.K - B = np.eye(self.N) + np.sqrt(self.ep_approx.tau_tilde)*self.Sroot_tilde_K - #self.L = np.linalg.cholesky(B) - self.L = jitchol(B) - V,info = linalg.flapack.dtrtrs(self.L,self.Sroot_tilde_K,lower=1) - self.Sigma = self.K - np.dot(V.T,V) - self.mu = np.dot(self.Sigma,self.ep_approx.v_tilde) * self.Z_hat - - def log_likelihood(self): - """ - Returns - ------- - The EP approximation to the log-marginal likelihood - """ - self.posterior_param() - mu_ = self.ep_approx.v_/self.ep_approx.tau_ - L1 =.5*sum(np.log(1+self.ep_approx.tau_tilde*1./self.ep_approx.tau_))-sum(np.log(np.diag(self.L))) - L2A =.5*np.sum((self.Sigma-np.diag(1./(self.ep_approx.tau_+self.ep_approx.tau_tilde))) * np.dot(self.ep_approx.v_tilde[:,None],self.ep_approx.v_tilde[None,:])) - L2B = .5*np.dot(mu_*(self.ep_approx.tau_/(self.ep_approx.tau_tilde+self.ep_approx.tau_)),self.ep_approx.tau_tilde*mu_ - 2*self.ep_approx.v_tilde) - L3 = sum(np.log(self.ep_approx.Z_hat)) - return L1 + L2A + L2B + L3 - - def _log_likelihood_gradients(self): - dK_dp = self.kernel.dK_dtheta(self.X) - self.dK_dp = dK_dp - aux1,info_1 = linalg.flapack.dtrtrs(self.L,np.dot(self.Sroot_tilde_K,self.ep_approx.v_tilde),lower=1) - b = self.ep_approx.v_tilde - np.sqrt(self.ep_approx.tau_tilde)*linalg.flapack.dtrtrs(self.L.T,aux1)[0] - U,info_u = linalg.flapack.dtrtrs(self.L,np.diag(np.sqrt(self.ep_approx.tau_tilde)),lower=1) - dL_dK = 0.5*(np.outer(b,b)-np.dot(U.T,U)) - self.dL_dK = dL_dK - return np.array([np.sum(dK_dpi*dL_dK) for dK_dpi in dK_dp.T]) - - def predict(self,X): - #TODO: check output dimensions - self.posterior_param() - K_x = self.kernel.K(self.X,X) - Kxx = self.kernel.K(X) - aux1,info = linalg.flapack.dtrtrs(self.L,np.dot(self.Sroot_tilde_K,self.ep_approx.v_tilde),lower=1) - aux2,info = linalg.flapack.dtrtrs(self.L.T, aux1,lower=0) - zeta = np.sqrt(self.ep_approx.tau_tilde)*aux2 - f = np.dot(K_x.T,self.ep_approx.v_tilde-zeta) - v,info = linalg.flapack.dtrtrs(self.L,np.sqrt(self.ep_approx.tau_tilde)[:,None]*K_x,lower=1) - variance = Kxx - np.dot(v.T,v) - vdiag = np.diag(variance) - y=self.likelihood.predictive_mean(f,vdiag) - return f,vdiag,y - - def plot(self): - """ - Plot the fitted model: training function values, inducing points used, mean estimate and confidence intervals. - """ - if self.X.shape[1]==1: - pb.figure() - xmin,xmax = self.X.min(),self.X.max() - xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) - Xnew = np.linspace(xmin,xmax,100)[:,None] - mu_f, var_f, mu_phi = self.predict(Xnew) - pb.subplot(211) - self.likelihood.plot1Da(X_new=Xnew,Mean_new=mu_f,Var_new=var_f,X_u=self.X,Mean_u=self.mu,Var_u=np.diag(self.Sigma)) - pb.subplot(212) - self.likelihood.plot1Db(self.X,Xnew,mu_phi) - elif self.X.shape[1]==2: - pb.figure() - x1min,x1max = self.X[:,0].min(0),self.X[:,0].max(0) - x2min,x2max = self.X[:,1].min(0),self.X[:,1].max(0) - x1min, x1max = x1min-0.2*(x1max-x1min), x1max+0.2*(x1max-x1min) - x2min, x2max = x2min-0.2*(x2max-x2min), x2max+0.2*(x1max-x1min) - axis1 = np.linspace(x1min,x1max,50) - axis2 = np.linspace(x2min,x2max,50) - XX1, XX2 = [e.flatten() for e in np.meshgrid(axis1,axis2)] - Xnew = np.c_[XX1.flatten(),XX2.flatten()] - f,v,p = self.predict(Xnew) - self.likelihood.plot2D(self.X,Xnew,p) - else: - raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" - - def em(self,max_f_eval=1e4,epsilon=.1,plot_all=False): #TODO check this makes sense - """ - Fits sparse_EP and optimizes the hyperparametes iteratively until convergence is achieved. - """ - self.epsilon_em = epsilon - log_likelihood_change = self.epsilon_em + 1. - self.parameters_path = [self.kernel._get_params()] - self.approximate_likelihood() - self.site_approximations_path = [[self.ep_approx.tau_tilde,self.ep_approx.v_tilde]] - self.log_likelihood_path = [self.log_likelihood()] - iteration = 0 - while log_likelihood_change > self.epsilon_em: - print 'EM iteration', iteration - self.optimize(max_f_eval = max_f_eval) - log_likelihood_new = self.log_likelihood() - log_likelihood_change = log_likelihood_new - self.log_likelihood_path[-1] - if log_likelihood_change < 0: - print 'log_likelihood decrement' - self.kernel._set_params_transformed(self.parameters_path[-1]) - self.kernM._set_params_transformed(self.parameters_path[-1]) - else: - self.approximate_likelihood() - self.log_likelihood_path.append(self.log_likelihood()) - self.parameters_path.append(self.kernel._get_params()) - self.site_approximations_path.append([self.ep_approx.tau_tilde,self.ep_approx.v_tilde]) - iteration += 1 diff --git a/GPy/models/GP_EP2.py b/GPy/models/GP_EP2.py deleted file mode 100644 index ce869951..00000000 --- a/GPy/models/GP_EP2.py +++ /dev/null @@ -1,279 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - -import numpy as np -import pylab as pb -from scipy import stats, linalg -from .. import kern -from ..inference.EP import Full -from ..inference.likelihoods import likelihood,probit,poisson,gaussian -from ..core import model -from ..util.linalg import pdinv,mdot #,jitchol -from ..util.plot import gpplot, Tango - -class GP_EP2(model): - def __init__(self,X,likelihood,kernel=None,normalize_X=False,Xslices=None,epsilon_ep=1e-3,epsion_em=.1,powerep=[1.,1.]): - """ - Simple Gaussian Process with Non-Gaussian likelihood - - Arguments - --------- - :param X: input observations (NxD numpy.darray) - :param likelihood: a GPy likelihood (likelihood class) - :param kernel: a GPy kernel, defaults to rbf+white - :param normalize_X: whether to normalize the input data before computing (predictions will be in original scales) - :type normalize_X: False|True - :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 1e-3 - :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] (list) - :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) - :rtype: model object. - """ - #.. Note:: Multiple independent outputs are allowed using columns of Y #TODO add this note? - if kernel is None: - kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) - - # parse arguments - self.Xslices = Xslices - assert isinstance(kernel, kern.kern) - self.likelihood = likelihood - self.kern = kernel - self.X = X - assert len(self.X.shape)==2 - assert self.X.shape[0] == self.likelihood.Y.shape[0] - self.D = self.likelihood.Y.shape[1] - self.N, self.Q = self.X.shape - - #here's some simple normalisation - if normalize_X: - self._Xmean = X.mean(0)[None,:] - self._Xstd = X.std(0)[None,:] - self.X = (X.copy() - self._Xmean) / self._Xstd - if hasattr(self,'Z'): - self.Z = (self.Z - self._Xmean) / self._Xstd - else: - self._Xmean = np.zeros((1,self.X.shape[1])) - self._Xstd = np.ones((1,self.X.shape[1])) - - #THIS PART IS NOT NEEDED - """ - if normalize_Y: - self._Ymean = Y.mean(0)[None,:] - self._Ystd = Y.std(0)[None,:] - self.Y = (Y.copy()- self._Ymean) / self._Ystd - else: - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - - if self.D > self.N: - # then it's more efficient to store YYT - self.YYT = np.dot(self.Y, self.Y.T) - else: - self.YYT = None - """ - self.eta,self.delta = powerep - self.epsilon_ep = epsilon_ep - self.tau_tilde = np.ones([self.N,self.D]) - self.v_tilde = np.zeros([self.N,self.D]) - self.tau_ = np.ones([self.N,self.D]) - self.v_ = np.zeros([self.N,self.D]) - self.Z_hat = np.ones([self.N,self.D]) - model.__init__(self) - - def _set_params(self,p): - self.kern._set_params_transformed(p) - self.K = self.kern.K(self.X,slices1=self.Xslices) - self._ep_params() - - def _get_params(self): - return self.kern._get_params_transformed() - - def _get_param_names(self): - return self.kern._get_param_names_transformed() - - def approximate_likelihood(self): - self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) - self.tau_tilde, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() - self._ep_params() - - def _ep_params(self): - # Posterior mean and Variance computation - self.Sroot_tilde_K = np.sqrt(self.tau_tilde)*self.K - B = np.eye(self.N) + np.sqrt(self.tau_tilde.flatten())[None,:]*self.Sroot_tilde_K - self.Bi,self.L,self.Li,B_logdet = pdinv(B) - V = np.dot(self.Li,self.Sroot_tilde_K) - self.Sigma = self.K - np.dot(V.T,V) #posterior variance - self.mu = np.dot(self.Sigma,self.v_tilde) #posterior mean - # Kernel plus noise variance term - self.Kplus = self.K + np.diag(1./self.tau_tilde.flatten()) - self.Kplusi,self.Lplus,self.Lplusi,self.Kplus_logdet = pdinv(self.Kplus) - # Y: EP likelihood is defined as a regression model for mu_tilde - self.Y = self.v_tilde/self.tau_tilde - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - self.YYT = None #np.dot(self.Y, self.Y.T) - self.mu_ = self.v_/self.tau_ - - def _model_fit_term(self): - """ - Computes the model fit using YYT if it's available - """ - if self.YYT is None: - return -0.5*np.sum(np.square(np.dot(self.Lplusi,self.Y))) - else: - return -0.5*np.sum(np.multiply(self.Kplusi, self.YYT)) - - def _normalization_term(self): - """ - Computes the marginal likelihood normalization constants - """ - sigma_sum = 1./self.tau_ + 1./self.tau_tilde - mu_diff_2 = (self.mu_ - self.Y)**2 - penalty_term = np.sum(np.log(self.Z_hat)) - return penalty_term + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) - - def log_likelihood(self): - """ - The log marginal likelihood for an EP model can be written as the log likelihood of - a regression model for a new variable Y* = v_tilde/tau_tilde, with a covariance - matrix K* = K + diag(1./tau_tilde) plus a normalization term. - """ - complexity_term = -0.5*self.D*self.Kplus_logdet - return complexity_term + self._model_fit_term() + self._normalization_term() - - def dL_dK(self): - if self.YYT is None: - alpha = np.dot(self.Kplusi,self.Y) - dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Kplusi) - else: - dL_dK = 0.5*(mdot(self.Kplusi, self.YYT, self.Kplusi) - self.D*self.Kplusi) - return dL_dK - - def _log_likelihood_gradients(self): - return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) - - def predict(self,Xnew, slices=None, full_cov=False): - """ - - Predict the function(s) at the new point(s) Xnew. - - Arguments - --------- - :param Xnew: The points at which to make a prediction - :type Xnew: np.ndarray, Nnew x self.Q - :param slices: specifies which outputs kernel(s) the Xnew correspond to (see below) - :type slices: (None, list of slice objects, list of ints) - :param full_cov: whether to return the folll covariance matrix, or just the diagonal - :type full_cov: bool - :rtype: posterior mean, a Numpy array, Nnew x self.D - :rtype: posterior variance, a Numpy array, Nnew x Nnew x (self.D) - - .. Note:: "slices" specifies how the the points X_new co-vary wich the training points. - - - If None, the new points covary throigh every kernel part (default) - - If a list of slices, the i^th slice specifies which data are affected by the i^th kernel part - - If a list of booleans, specifying which kernel parts are active - - If full_cov and self.D > 1, the return shape of var is Nnew x Nnew x self.D. If self.D == 1, the return shape is Nnew x Nnew. - This is to allow for different normalisations of the output dimensions. - - - """ - - #normalise X values - Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var, phi = self._raw_predict(Xnew, slices, full_cov) - - #un-normalise - mu = mu*self._Ystd + self._Ymean - if full_cov: - if self.D==1: - var *= np.square(self._Ystd) - else: - var = var[:,:,None] * np.square(self._Ystd) - else: - if self.D==1: - var *= np.square(np.squeeze(self._Ystd)) - else: - var = var[:,None] * np.square(self._Ystd) - - return mu,var,phi - - def _raw_predict(self,_Xnew,slices, full_cov=False): - """Internal helper function for making predictions, does not account for normalisation""" - K_x = self.kern.K(self.X,_Xnew,slices1=self.Xslices,slices2=slices) - aux2 = mdot(self.Bi,self.Sroot_tilde_K,self.v_tilde) - zeta = np.sqrt(self.tau_tilde)*aux2 - f = np.dot(K_x.T,self.v_tilde-zeta) - v = mdot(self.Li,np.sqrt(self.tau_tilde)*K_x) - if full_cov: - Kxx = self.kern.K(_Xnew,slices1=slices,slices2=slices) - var = Kxx - np.dot(v.T,v) - var_diag = np.diag(var)[:,None] - else: - Kxx = self.kern.Kdiag(_Xnew, slices=slices) - var_diag = (Kxx - np.sum(v**2,-2))[:,None] - phi = self.likelihood.predictive_mean(f,var_diag) - return f, var_diag, phi - - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): - """ - :param samples: the number of a posteriori samples to plot - :param which_data: which if the training data to plot (default all) - :type which_data: 'all' or a slice object to slice self.X, self.Y - :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits - :param which_functions: which of the kernel functions to plot (additively) - :type which_functions: list of bools - :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D - - Plot the posterior of the GP. - - In one dimension, the function is plotted with a shaded region identifying two standard deviations. - - In two dimsensions, a contour-plot shows the mean predicted function - - In higher dimensions, we've no implemented this yet !TODO! - - Can plot only part of the data and part of the posterior functions using which_data and which_functions - """ - if which_functions=='all': - which_functions = [True]*self.kern.Nparts - if which_data=='all': - which_data = slice(None) - - X = self.X[which_data,:] - Y = self.Y[which_data,:] - - Xorig = X*self._Xstd + self._Xmean - Yorig = Y*self._Ystd + self._Ymean - if plot_limits is None: - xmin,xmax = Xorig.min(0),Xorig.max(0) - xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) - elif len(plot_limits)==2: - xmin, xmax = plot_limits - else: - raise ValueError, "Bad limits for plotting" - - if self.X.shape[1]==1: - Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - #m,v,phi = self.predict(Xnew,slices=which_functions) - #gpplot(Xnew,m,v) - mu_f, var_f, phi_f = self.predict(Xnew,slices=which_functions) - pb.subplot(211) - self.likelihood.plot1Da(X=Xnew,mean=mu_f,var=var_f,Z=self.X,mean_Z=self.mu,var_Z=np.diag(self.Sigma)) - if samples: - s = np.random.multivariate_normal(m.flatten(),v,samples) - pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - pb.xlim(xmin,xmax) - pb.subplot(212) - self.likelihood.plot1Db(self.X,Xnew,phi_f) - - elif self.X.shape[1]==2: - resolution = 50 or resolution - xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] - Xtest = np.vstack((xx.flatten(),yy.flatten())).T - zz,vv = self.predict(Xtest,slices=which_functions) - zz = zz.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) - pb.xlim(xmin[0],xmax[0]) - pb.ylim(xmin[1],xmax[1]) - - else: - raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index ca44aab1..cc2f62d6 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -6,8 +6,6 @@ from GP_regression import GP_regression from sparse_GP_regression import sparse_GP_regression from GPLVM import GPLVM from warped_GP import warpedGP -from GP_EP import GP_EP -from GP_EP2 import GP_EP2 from generalized_FITC import generalized_FITC from sparse_GPLVM import sparse_GPLVM from uncollapsed_sparse_GP import uncollapsed_sparse_GP diff --git a/GPy/models/generalized_FITC.py b/GPy/models/generalized_FITC.py index a5ed8d0a..57ae2407 100644 --- a/GPy/models/generalized_FITC.py +++ b/GPy/models/generalized_FITC.py @@ -9,7 +9,8 @@ from .. import kern from ..core import model from ..util.linalg import pdinv,mdot from ..util.plot import gpplot -from ..inference.Expectation_Propagation import FITC +#from ..inference.Expectation_Propagation import FITC +from ..inference.EP import FITC from ..inference.likelihoods import likelihood,probit class generalized_FITC(model): From fad0e07624971b0e381db34806b1b27ae7d27fcb Mon Sep 17 00:00:00 2001 From: Ricardo Date: Mon, 28 Jan 2013 00:16:23 +0000 Subject: [PATCH 004/197] Sparse EP --- GPy/examples/ep_fix.py | 5 +- GPy/examples/poisson.py | 50 +++++++ GPy/examples/sparse_ep_fix.py | 76 ++++++++++ GPy/inference/EP.py | 9 +- GPy/models/GP.py | 8 +- GPy/models/__init__.py | 1 + GPy/models/sparse_GP.py | 258 ++++++++++++++++++++++++++++++++++ 7 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 GPy/examples/poisson.py create mode 100644 GPy/examples/sparse_ep_fix.py create mode 100644 GPy/models/sparse_GP.py diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 2da94335..9b35b3ff 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -10,6 +10,7 @@ import numpy as np import GPy pb.ion() +pb.close('all') default_seed=10000 model_type='Full' @@ -26,11 +27,13 @@ data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) m = GPy.models.GP(data['X'],likelihood=likelihood) +#m = GPy.models.GP(data['X'],Y=likelihood.Y) m.constrain_positive('var') m.constrain_positive('len') m.tie_param('lengthscale') -m.approximate_likelihood() +if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): + m.approximate_likelihood() print m.checkgrad() # Optimize and plot m.optimize() diff --git a/GPy/examples/poisson.py b/GPy/examples/poisson.py new file mode 100644 index 00000000..5a1cc6af --- /dev/null +++ b/GPy/examples/poisson.py @@ -0,0 +1,50 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +""" +Simple Gaussian Processes classification +""" +import pylab as pb +import numpy as np +import GPy +pb.ion() + +pb.close('all') +default_seed=10000 + +model_type='Full' +inducing=4 +seed=default_seed +"""Simple 1D classification example. +:param model_type: type of model to fit ['Full', 'FITC', 'DTC']. +:param seed : seed value for data generation (default is 4). +:type seed: int +:param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). +:type inducing: int +""" + +X = np.arange(0,100,5)[:,None] +F = np.round(np.sin(X/18.) + .1*X) +E = np.random.randint(-3,3,20)[:,None] +Y = F + E +pb.plot(X,F,'k-') +pb.plot(X,Y,'ro') +pb.figure() +likelihood = GPy.inference.likelihoods.poisson(Y,scale=4.) + +m = GPy.models.GP(X,likelihood=likelihood) +#m = GPy.models.GP(data['X'],Y=likelihood.Y) + +m.constrain_positive('var') +m.constrain_positive('len') +m.tie_param('lengthscale') +if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): + m.approximate_likelihood() +print m.checkgrad() +# Optimize and plot +m.optimize() +#m.em(plot_all=False) # EM algorithm +m.plot() + +print(m) diff --git a/GPy/examples/sparse_ep_fix.py b/GPy/examples/sparse_ep_fix.py new file mode 100644 index 00000000..738a82e6 --- /dev/null +++ b/GPy/examples/sparse_ep_fix.py @@ -0,0 +1,76 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +import numpy as np +""" +Sparse Gaussian Processes regression with an RBF kernel +""" +import pylab as pb +import numpy as np +import GPy +np.random.seed(2) +pb.ion() +N = 500 +M = 5 + +###################################### +## 1 dimensional example + +# sample inputs and outputs +X = np.random.uniform(-3.,3.,(N,1)) +#Y = np.sin(X)+np.random.randn(N,1)*0.05 +F = np.sin(X)+np.random.randn(N,1)*0.05 +Y = np.ones([F.shape[0],1]) +Y[F<0] = -1 +likelihood = GPy.inference.likelihoods.probit(Y) + +# construct kernel +rbf = GPy.kern.rbf(1) +noise = GPy.kern.white(1) +kernel = rbf + noise + +# create simple GP model +#m1 = GPy.models.sparse_GP_regression(X, Y, kernel, M=M) +m1 = GPy.models.sparse_GP(X, kernel, M=M,likelihood= likelihood) + +# contrain all parameters to be positive +m1.constrain_positive('(variance|lengthscale|precision)') +#m1.constrain_positive('(variance|lengthscale)') +#m1.constrain_fixed('prec',10.) + + +#check gradient FIXME unit test please +m1.checkgrad() +# optimize and plot +m1.optimize('tnc', messages = 1) +m1.plot() +# print(m1) + +###################################### +## 2 dimensional example + +# # sample inputs and outputs +# X = np.random.uniform(-3.,3.,(N,2)) +# Y = np.sin(X[:,0:1]) * np.sin(X[:,1:2])+np.random.randn(N,1)*0.05 + +# # construct kernel +# rbf = GPy.kern.rbf(2) +# noise = GPy.kern.white(2) +# kernel = rbf + noise + +# # create simple GP model +# m2 = GPy.models.sparse_GP_regression(X,Y,kernel, M = 50) +# create simple GP model + +# # contrain all parameters to be positive (but not inducing inputs) +# m2.constrain_positive('(variance|lengthscale|precision)') + +# #check gradient FIXME unit test please +# m2.checkgrad() + +# # optimize and plot +# pb.figure() +# m2.optimize('tnc', messages = 1) +# m2.plot() +# print(m2) diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py index f7c163b1..751d5ca8 100644 --- a/GPy/inference/EP.py +++ b/GPy/inference/EP.py @@ -9,7 +9,7 @@ from ..util.plot import gpplot from .. import kern class EP: - def __init__(self,covariance,likelihood,Kmn=None,Knn_diag=None,epsilon=1e-3,powerep=[1.,1.]): + def __init__(self,covariance,likelihood,Kmn=None,Knn_diag=None,epsilon=1e-3,power_ep=[1.,1.]): """ Expectation Propagation @@ -19,7 +19,7 @@ class EP: likelihood : Output's likelihood (likelihood class) kernel : a GPy kernel (kern class) inducing : Either an array specifying the inducing points location or a sacalar defining their number. None value for using a non-sparse model is used. - powerep : Power-EP parameters (eta,delta) - 2x1 numpy array (floats) + power_ep : Power-EP parameters (eta,delta) - 2x1 numpy array (floats) epsilon : Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) """ self.likelihood = likelihood @@ -38,7 +38,7 @@ class EP: assert len(Knn_diag) == self.N, 'Knn_diagonal has size different from N' self.epsilon = epsilon - self.eta, self.delta = powerep + self.eta, self.delta = power_ep self.jitter = 1e-12 """ @@ -110,6 +110,7 @@ class Full(EP): self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) self.mu = np.dot(self.Sigma,self.v_tilde) self.iterations += 1 + print self.tau_tilde[i] #TODO erase me #Sigma recomptutation with Cholesky decompositon Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*(self.K) B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K @@ -206,6 +207,7 @@ class DTC(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) + return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] class FITC(EP): def fit_EP(self): @@ -306,3 +308,4 @@ class FITC(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) + return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 4a8d23e9..ccfe95c7 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -29,8 +29,8 @@ class GP(model): """ - def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsion_em=.1,powerep=[1.,1.]): - #TODO: specify beta parameter explicitely + def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsion_em=.1,power_ep=[1.,1.]): + #TODO: make beta parameter explicit # parse arguments self.Xslices = Xslices @@ -87,7 +87,7 @@ class GP(model): else: # Y is defined after approximating the likelihood self.EP = True - self.eta,self.delta = powerep + self.eta,self.delta = power_ep self.epsilon_ep = epsilon_ep self.tau_tilde = np.ones([self.N,self.D]) self.v_tilde = np.zeros([self.N,self.D]) @@ -116,7 +116,7 @@ class GP(model): def approximate_likelihood(self): assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) + self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) self.tau_tilde, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() # Y: EP likelihood is defined as a regression model for mu_tilde self.Y = self.v_tilde/self.tau_tilde diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index cc2f62d6..a839f827 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -10,3 +10,4 @@ from generalized_FITC import generalized_FITC from sparse_GPLVM import sparse_GPLVM from uncollapsed_sparse_GP import uncollapsed_sparse_GP from GP import GP +from sparse_GP import sparse_GP diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py new file mode 100644 index 00000000..1164a1af --- /dev/null +++ b/GPy/models/sparse_GP.py @@ -0,0 +1,258 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + +import numpy as np +import pylab as pb +from ..util.linalg import mdot, jitchol, chol_inv, pdinv +from ..util.plot import gpplot +from .. import kern +from GP import GP +from ..inference.EP import Full +from ..inference.likelihoods import likelihood,probit,poisson,gaussian + +#Still TODO: +# make use of slices properly (kernel can now do this) +# enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) + +class sparse_GP(GP): + """ + Variational sparse GP model (Regression) + + :param X: inputs + :type X: np.ndarray (N x Q) + :param Y: observed data + :type Y: np.ndarray of observations (N x D) + :param kernel : the kernel/covariance function. See link kernels + :type kernel: a GPy kernel + :param Z: inducing inputs (optional, see note) + :type Z: np.ndarray (M x Q) | None + :param X_uncertainty: The uncertainty in the measurements of X (Gaussian variance) + :type X_uncertainty: np.ndarray (N x Q) | None + :param Zslices: slices for the inducing inputs (see slicing TODO: link) + :param M : Number of inducing points (optional, default 10. Ignored if Z is not None) + :type M: int + :param beta: noise precision. TODO> ignore beta if doing EP + :type beta: float + :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) + :type normalize_(X|Y): bool + """ + + def __init__(self,X,Y,kernel=None, X_uncertainty=None, beta=100., Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): + + if Z is None: + self.Z = np.random.permutation(X.copy())[:M] + self.M = M + else: + assert Z.shape[1]==X.shape[1] + self.Z = Z + self.M = Z.shape[0] + if X_uncertainty is None: + self.has_uncertain_inputs=False + else: + assert X_uncertainty.shape==X.shape + self.has_uncertain_inputs=True + self.X_uncertainty = X_uncertainty + + + self.beta = beta #FIXME + GP.__init__(self, X, Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,epsion_em=epsilon_em,power_ep=power_ep) + self.beta = beta if isinstance(likelihood,gaussian) else self.tau_tilde #TODO this should be defined in GP.__init__ + + + #normalise X uncertainty also + if self.has_uncertain_inputs: + self.X_uncertainty /= np.square(self._Xstd) + + def _set_params(self, p): + if not self.EP: + self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) + self.beta = p[self.M*self.Q] + self.kern._set_params(p[self.Z.size + 1:]) + self.beta2 = self.beta**2 + self._compute_kernel_matrices() + self._computations() + else: + self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) + self.kern._set_params(p[self.Z.size:]) + #self._compute_kernel_matrices() this is replaced by _ep_covariance + self._ep_covariance() + self._ep_computations() + + def approximate_likelihood(self): + assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" + if self.ep_proxy == 'DTC': + self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + elif self.ep_proxy == 'FITC': + self.Knn_diag = self.kern.psi0(self.Z,self.X, self.X_uncertainty) #TODO psi0 already calculates this + self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.Knn_diag,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + else: + self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + self.beta, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() + # Y: EP likelihood is defined as a regression model for mu_tilde + self.Y = self.v_tilde/self.beta + self._Ymean = np.zeros((1,self.Y.shape[1])) + self._Ystd = np.ones((1,self.Y.shape[1])) + self.trbetaYYT = np.sum(self.beta*np.square(self.Y)) + if self.D > self.N: + # then it's more efficient to store YYT + self.YYT = np.dot(self.Y, self.Y.T) + else: + self.YYT = None + self.mu_ = self.v_/self.tau_ + self._ep_covariance() + self._computations() + + def _ep_covariance(self): + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) #FIXME include beta + else: + #self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() + self.Knn_diag = self.kern.Kdiag(self.X,slices=self.Xslices) + self.psi0 = (self.beta*self.Knn_diag).sum() #TODO check dimensions + self.psi1 = self.kern.K(self.Z,self.X) + #self.psi2 = np.dot(self.psi1,self.psi1.T) + self.psi2 = np.dot(self.psi1,self.beta*self.psi1.T) + + def _compute_kernel_matrices(self): + # kernel computations, using BGPLVM notation + #TODO: slices for psi statistics (easy enough) + + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() + self.psi1 = self.kern.K(self.Z,self.X) + self.psi2 = np.dot(self.psi1,self.psi1.T) + + def _ep_computations(self): + # TODO find routine to multiply triangular matrices + self.V = self.beta*self.Y + self.psi1V = np.dot(self.psi1, self.V) + self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) + #self.A = mdot(self.Lmi, self.beta*self.psi2, self.Lmi.T) + self.A = mdot(self.Lmi, self.psi2, self.Lmi.T) + self.B = np.eye(self.M) + self.A + self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) + self.LLambdai = np.dot(self.LBi, self.Lmi) + #self.trace_K = self.psi0 - np.sum(np.dot(self.Lmi,self.psi1)**2,-1) #TODO check + self.trace_K = self.psi0 - np.trace(self.A) + self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) + self.C = mdot(self.LLambdai, self.psi1V) + self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) + + # Compute dL_dpsi + #self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() * np.ones(self.N) #TODO check + self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) + #self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + + # Compute dL_dKmm + self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB + self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC + self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + + def _get_params(self): + if not self.EP: + return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) + else: + return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) + + def _get_param_names(self): + if not self.EP: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() + else: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() + + def log_likelihood(self): + """ + Compute the (lower bound on the) log marginal likelihood + """ + beta_logdet = self.N*self.D*np.log(self.beta) if not self.EP else self.D*np.sum(np.log(self.beta)) + A = -0.5*self.N*self.D*(np.log(2.*np.pi)) - 0.5*beta_logdet + B = -0.5*self.beta*self.D*self.trace_K if not self.EP else -0.5*self.D*self.trace_K + C = -0.5*self.D * self.B_logdet + D = -0.5*self.beta*self.trYYT if not self.EP else -0.5*self.trbetaYYT + E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) + return A+B+C+D+E + + def dL_dbeta(self): + """ + Compute the gradient of the log likelihood wrt beta. + """ + #TODO: suport heteroscedatic noise + dA_dbeta = 0.5 * self.N*self.D/self.beta + dB_dbeta = - 0.5 * self.D * self.trace_K + dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta + dD_dbeta = - 0.5 * self.trYYT + tmp = mdot(self.LBi.T, self.LLambdai, self.psi1V) + dE_dbeta = (np.sum(np.square(self.C)) - 0.5 * np.sum(self.A * np.dot(tmp, tmp.T)))/self.beta + + return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta + dE_dbeta) + + def dL_dtheta(self): + """ + Compute and return the derivative of the log marginal likelihood wrt the parameters of the kernel + """ + dL_dtheta = self.kern.dK_dtheta(self.dL_dKmm,self.Z) + if self.has_uncertain_inputs: + dL_dtheta += self.kern.dpsi0_dtheta(self.dL_dpsi0, self.Z,self.X,self.X_uncertainty) + dL_dtheta += self.kern.dpsi1_dtheta(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) + dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape + else: + #re-cast computations in psi2 back to psi1: + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) + dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) + + return dL_dtheta + + def dL_dZ(self): + """ + The derivative of the bound wrt the inducing inputs Z + """ + dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z,)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ + if self.has_uncertain_inputs: + dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) + dL_dZ += self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) + else: + #re-cast computations in psi2 back to psi1: + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) + return dL_dZ + + def _log_likelihood_gradients(self): + return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) + + def _raw_predict(self, Xnew, slices, full_cov=False): + """Internal helper function for making predictions, does not account for normalisation""" + Kx = self.kern.K(self.Z, Xnew) + mu = mdot(Kx.T, self.LBL_inv, self.psi1V) + if full_cov: + noise_term = np.eye(Xnew.shape[0])/self.beta if not self.EP else 0 + Kxx = self.kern.K(Xnew) + var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) + noise_term + else: + noise_term = 1./self.beta if not self.EP else 0 + Kxx = self.kern.Kdiag(Xnew) + var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) + noise_term + return mu,var + + def plot(self, *args, **kwargs): + """ + Plot the fitted model: just call the GP_regression plot function and then add inducing inputs + """ + GP_regression.plot(self,*args,**kwargs) + if self.Q==1: + pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) + if self.has_uncertain_inputs: + pb.errorbar(self.X[:,0], pb.ylim()[0]+np.zeros(self.N), xerr=2*np.sqrt(self.X_uncertainty.flatten())) + if self.Q==2: + pb.plot(self.Z[:,0],self.Z[:,1],'wo') From 29ec128c9d6620b20989c9bdb27de95c098927ef Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Mon, 28 Jan 2013 17:47:08 +0000 Subject: [PATCH 005/197] Other changes. --- GPy/examples/ep_fix.py | 12 ++-- GPy/examples/poisson.py | 2 +- GPy/examples/sparse_ep_fix.py | 34 +-------- GPy/inference/EP.py | 9 ++- GPy/inference/likelihoods.py | 32 ++++++++- GPy/models/GP.py | 92 +++++++++++-------------- GPy/models/sparse_GP.py | 126 +++++++++++++++++++++------------- 7 files changed, 164 insertions(+), 143 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 9b35b3ff..c4e025dd 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -11,11 +11,9 @@ import GPy pb.ion() pb.close('all') -default_seed=10000 model_type='Full' inducing=4 -seed=default_seed """Simple 1D classification example. :param model_type: type of model to fit ['Full', 'FITC', 'DTC']. :param seed : seed value for data generation (default is 4). @@ -23,21 +21,19 @@ seed=default_seed :param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). :type inducing: int """ -data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) +data = GPy.util.datasets.toy_linear_1d_classification(seed=0) likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) m = GPy.models.GP(data['X'],likelihood=likelihood) -#m = GPy.models.GP(data['X'],Y=likelihood.Y) +#m = GPy.models.GP(data['X'],likelihood.Y) -m.constrain_positive('var') -m.constrain_positive('len') -m.tie_param('lengthscale') +m.ensure_default_constraints() if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): m.approximate_likelihood() print m.checkgrad() # Optimize and plot m.optimize() #m.em(plot_all=False) # EM algorithm -m.plot() +m.plot(samples=3) print(m) diff --git a/GPy/examples/poisson.py b/GPy/examples/poisson.py index 5a1cc6af..71d80b30 100644 --- a/GPy/examples/poisson.py +++ b/GPy/examples/poisson.py @@ -31,7 +31,7 @@ Y = F + E pb.plot(X,F,'k-') pb.plot(X,Y,'ro') pb.figure() -likelihood = GPy.inference.likelihoods.poisson(Y,scale=4.) +likelihood = GPy.inference.likelihoods.poisson(Y,scale=6.) m = GPy.models.GP(X,likelihood=likelihood) #m = GPy.models.GP(data['X'],Y=likelihood.Y) diff --git a/GPy/examples/sparse_ep_fix.py b/GPy/examples/sparse_ep_fix.py index 738a82e6..7e3f1fc3 100644 --- a/GPy/examples/sparse_ep_fix.py +++ b/GPy/examples/sparse_ep_fix.py @@ -31,46 +31,18 @@ noise = GPy.kern.white(1) kernel = rbf + noise # create simple GP model -#m1 = GPy.models.sparse_GP_regression(X, Y, kernel, M=M) -m1 = GPy.models.sparse_GP(X, kernel, M=M,likelihood= likelihood) +#m1 = GPy.models.sparse_GP(X, Y, kernel, M=M) +m1 = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) +print m1.checkgrad() # contrain all parameters to be positive m1.constrain_positive('(variance|lengthscale|precision)') #m1.constrain_positive('(variance|lengthscale)') #m1.constrain_fixed('prec',10.) - #check gradient FIXME unit test please -m1.checkgrad() # optimize and plot m1.optimize('tnc', messages = 1) m1.plot() # print(m1) -###################################### -## 2 dimensional example - -# # sample inputs and outputs -# X = np.random.uniform(-3.,3.,(N,2)) -# Y = np.sin(X[:,0:1]) * np.sin(X[:,1:2])+np.random.randn(N,1)*0.05 - -# # construct kernel -# rbf = GPy.kern.rbf(2) -# noise = GPy.kern.white(2) -# kernel = rbf + noise - -# # create simple GP model -# m2 = GPy.models.sparse_GP_regression(X,Y,kernel, M = 50) -# create simple GP model - -# # contrain all parameters to be positive (but not inducing inputs) -# m2.constrain_positive('(variance|lengthscale|precision)') - -# #check gradient FIXME unit test please -# m2.checkgrad() - -# # optimize and plot -# pb.figure() -# m2.optimize('tnc', messages = 1) -# m2.plot() -# print(m2) diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py index 751d5ca8..5d571888 100644 --- a/GPy/inference/EP.py +++ b/GPy/inference/EP.py @@ -110,7 +110,6 @@ class Full(EP): self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) self.mu = np.dot(self.Sigma,self.v_tilde) self.iterations += 1 - print self.tau_tilde[i] #TODO erase me #Sigma recomptutation with Cholesky decompositon Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*(self.K) B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K @@ -122,7 +121,13 @@ class Full(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) - return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] + + #Variables to be called from GP + mu_tilde = self.v_tilde/self.tau_tilde #When calling EP, this variable is used instead of Y in the GP model + sigma_sum = 1./self.tau_ + 1./self.tau_tilde + mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 + Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant + return self.tau_tilde[:,None], mu_tilde[:,None], Z_ep class DTC(EP): def fit_EP(self): diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index 7f5d9140..864afa57 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -21,6 +21,27 @@ class likelihood: self.location = location self.scale = scale + def plot1D(self,X,mean,var,Z=None,mean_Z=None,var_Z=None,samples=0): + """ + Plot the predictive distribution of the GP model for 1-dimensional inputs + + :param X: The points at which to make a prediction + :param Mean: mean values at X + :param Var: variance values at X + :param Z: Set of points to be highlighted in the plot, i.e. inducing points + :param mean_Z: mean values at Z + :param var_Z: variance values at Z + :samples: Number of samples to plot + """ + assert X.shape[1] == 1, 'Number of dimensions must be 1' + gpplot(X,mean,var.flatten()) + if samples: #NOTE why don't we put samples as a parameter of gpplot + s = np.random.multivariate_normal(mean.flatten(),np.diag(var),samples) + pb.plot(X.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) + #pb.subplot(211) + #self.plot1Da(X,mean,var,Z,mean_Z,var_Z) + + def plot1Da(self,X,mean,var,Z=None,mean_Z=None,var_Z=None): """ Plot the predictive distribution of the GP model for 1-dimensional inputs @@ -37,6 +58,7 @@ class likelihood: pb.errorbar(Z.flatten(),mean_Z.flatten(),2*np.sqrt(var_Z.flatten()),fmt='r+') pb.plot(Z,mean_Z,'ro') + """ def plot1Db(self,X_obs,X,phi,Z=None): assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' gpplot(X,phi,np.zeros(X.shape[0])) @@ -45,6 +67,7 @@ class likelihood: if Z is not None: pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) + """ def plot2D(self,X,X_new,F_new,U=None): """ Predictive distribution of the fitted GP model for 2-dimensional inputs @@ -98,7 +121,6 @@ class probit(likelihood): sigma2_hat = 1./tau_i - (phi/((tau_i**2+tau_i)*Z_hat))*(z+phi/Z_hat) return Z_hat, mu_hat, sigma2_hat - def predictive_mean(self,mu,var): mu = mu.flatten() var = var.flatten() @@ -107,6 +129,14 @@ class probit(likelihood): def _log_likelihood_gradients(): raise NotImplementedError + def plot(self,X,phi,X_obs,Z=None): + assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' + gpplot(X,phi,np.zeros(X.shape[0])) + pb.plot(X_obs,(self.Y+1)/2,'kx',mew=1.5) + if Z is not None: + pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) + pb.ylim(-0.2,1.2) + class poisson(likelihood): """ Poisson likelihood diff --git a/GPy/models/GP.py b/GPy/models/GP.py index ccfe95c7..3a9f6de8 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -24,13 +24,18 @@ class GP(model): :type normalize_Y: False|True :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) :rtype: model object + :parm likelihood: a GPy likelihood, defaults to gaussian + :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 + :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] + :type powerep: list .. Note:: Multiple independent outputs are allowed using columns of Y """ + #TODO: make beta parameter explicit + #TODO: when using EP, predict needs to return 3 values otherwise it just needs 2. At the moment predict returns 3 values in any case. - def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsion_em=.1,power_ep=[1.,1.]): - #TODO: make beta parameter explicit + def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): # parse arguments self.Xslices = Xslices @@ -54,7 +59,6 @@ class GP(model): self._Xmean = np.zeros((1,self.X.shape[1])) self._Xstd = np.ones((1,self.X.shape[1])) - # Y - likelihood related variables, these might change whether using EP or not if likelihood is None: assert Y is not None, "Either Y or likelihood must be defined" @@ -68,8 +72,9 @@ class GP(model): if isinstance(self.likelihood,gaussian): self.EP = False self.Y = Y + self.beta = 100.#FIXME beta should be an explicit parameter for this model - #here's some simple normalisation + # Here's some simple normalisation if normalize_Y: self._Ymean = Y.mean(0)[None,:] self._Ystd = Y.std(0)[None,:] @@ -89,50 +94,43 @@ class GP(model): self.EP = True self.eta,self.delta = power_ep self.epsilon_ep = epsilon_ep - self.tau_tilde = np.ones([self.N,self.D]) - self.v_tilde = np.zeros([self.N,self.D]) - self.tau_ = np.ones([self.N,self.D]) - self.v_ = np.zeros([self.N,self.D]) - self.Z_hat = np.ones([self.N,self.D]) + self.beta = np.ones([self.N,self.D]) + self.Z_ep = 0 + self.Y = None + self._Ymean = np.zeros((1,self.D)) + self._Ystd = np.ones((1,self.D)) model.__init__(self) def _set_params(self,p): - # TODO: remove beta when using EP + # TODO: add beta when not using EP self.kern._set_params_transformed(p) - if not self.EP: - self.K = self.kern.K(self.X,slices1=self.Xslices) - self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) - else: - self._ep_covariance() + self.K = self.kern.K(self.X,slices1=self.Xslices) + if self.EP: + self.K += np.diag(1./self.beta.flatten()) + #else: + # self.beta = p[-1] + self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) def _get_params(self): - # TODO: remove beta when using EP + # TODO: add beta when not using EP return self.kern._get_params_transformed() def _get_param_names(self): - # TODO: remove beta when using EP + # TODO: add beta when not using EP return self.kern._get_param_names_transformed() def approximate_likelihood(self): assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - self.ep_approx = Full(self.K,self.likelihood,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - self.tau_tilde, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() - # Y: EP likelihood is defined as a regression model for mu_tilde - self.Y = self.v_tilde/self.tau_tilde - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) + self.ep_approx = Full(self.K,self.likelihood,epsilon = self.epsilon_ep,power_ep=[self.eta,self.delta]) + self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() if self.D > self.N: # then it's more efficient to store YYT self.YYT = np.dot(self.Y, self.Y.T) else: self.YYT = None - self.mu_ = self.v_/self.tau_ - self._ep_covariance() - - def _ep_covariance(self): # Kernel plus noise variance term - self.K = self.kern.K(self.X,slices1=self.Xslices) + np.diag(1./self.tau_tilde.flatten()) + self.K = self.kern.K(self.X,slices1=self.Xslices) + np.diag(1./self.beta.flatten()) self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) def _model_fit_term(self): @@ -144,25 +142,16 @@ class GP(model): else: return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) - def _normalization_term(self): - """ - Computes the marginal likelihood normalization constants - """ - sigma_sum = 1./self.tau_ + 1./self.tau_tilde - mu_diff_2 = (self.mu_ - self.Y)**2 - penalty_term = np.sum(np.log(self.Z_hat)) - return penalty_term + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) - def log_likelihood(self): """ The log marginal likelihood for an EP model can be written as the log likelihood of a regression model for a new variable Y* = v_tilde/tau_tilde, with a covariance matrix K* = K + diag(1./tau_tilde) plus a normalization term. """ - complexity_term = -0.5*self.D*self.Kplus_logdet - normalization_term = 0 if self.EP == False else self.normalization_term() - return complexity_term + normalization_term + self._model_fit_term() - + L = -0.5*selff.D*self.K_logdet + self.model_fit_term() + if self.EP: + L += self.normalisation_term() + return L def log_likelihood(self): complexity_term = -0.5*self.N*self.D*np.log(2.*np.pi) - 0.5*self.D*self.K_logdet @@ -174,7 +163,6 @@ class GP(model): dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) else: dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) - return dL_dK def _log_likelihood_gradients(self): @@ -267,7 +255,7 @@ class GP(model): Y = self.Y[which_data,:] Xorig = X*self._Xstd + self._Xmean - Yorig = Y*self._Ystd + self._Ymean if not self.EP else self.likelihood.Y + Yorig = Y*self._Ystd + self._Ymean #NOTE For EP this is v_tilde/beta if plot_limits is None: xmin,xmax = Xorig.min(0),Xorig.max(0) @@ -282,19 +270,17 @@ class GP(model): m,v,phi = self.predict(Xnew,slices=which_functions) if self.EP: pb.subplot(211) - gpplot(Xnew,m,v) - if samples: - s = np.random.multivariate_normal(m.flatten(),v,samples) - pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - if not self.EP: - pb.plot(Xorig,Yorig,'kx',mew=1.5) - pb.xlim(xmin,xmax) - else: - pb.xlim(xmin,xmax) + if samples: #NOTE why don't we put samples as a parameter of gpplot + s = np.random.multivariate_normal(m.flatten(),np.diag(v),samples) + pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) + pb.plot(Xorig,Yorig,'kx',mew=1.5) + pb.xlim(xmin,xmax) + + if self.EP: pb.subplot(212) - self.likelihood.plot1Db(self.X,Xnew,phi) + self.likelihood.plot(Xnew,phi,self.X) pb.xlim(xmin,xmax) elif self.X.shape[1]==2: diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 1164a1af..655f6252 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -37,7 +37,7 @@ class sparse_GP(GP): :type normalize_(X|Y): bool """ - def __init__(self,X,Y,kernel=None, X_uncertainty=None, beta=100., Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): + def __init__(self,X,Y=None,kernel=None, X_uncertainty=None, beta=100., Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): if Z is None: self.Z = np.random.permutation(X.copy())[:M] @@ -53,10 +53,8 @@ class sparse_GP(GP): self.has_uncertain_inputs=True self.X_uncertainty = X_uncertainty - - self.beta = beta #FIXME - GP.__init__(self, X, Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,epsion_em=epsilon_em,power_ep=power_ep) - self.beta = beta if isinstance(likelihood,gaussian) else self.tau_tilde #TODO this should be defined in GP.__init__ + GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,epsilon_em=epsilon_em,power_ep=power_ep) + self.trYYT = np.sum(np.square(self.Y)) if not self.EP else None #normalise X uncertainty also @@ -74,10 +72,55 @@ class sparse_GP(GP): else: self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) self.kern._set_params(p[self.Z.size:]) - #self._compute_kernel_matrices() this is replaced by _ep_covariance - self._ep_covariance() + #self._compute_kernel_matrices() this is replaced by _ep_kernel_matrices + self._ep_kernel_matrices() self._ep_computations() + def _compute_kernel_matrices(self): + # kernel computations, using BGPLVM notation + #TODO: slices for psi statistics (easy enough) + + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + if self.hetero_noise: + raise NotImplementedError, "uncertain ips and het noise not yet supported" + else: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) + else: + if self.hetero_noise: + print "rick's stuff here" + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() + self.psi1 = self.kern.K(self.Z,self.X) + self.psi2 = np.dot(self.psi1,self.psi1.T) + + def _computations(self): + # TODO find routine to multiply triangular matrices + self.V = self.beta*self.Y + self.psi1V = np.dot(self.psi1, self.V) + self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) + self.A = mdot(self.Lmi, self.beta*self.psi2, self.Lmi.T) + self.B = np.eye(self.M) + self.A + self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) + self.LLambdai = np.dot(self.LBi, self.Lmi) + self.trace_K = self.psi0 - np.trace(self.A)/self.beta + self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) + self.C = mdot(self.LLambdai, self.psi1V) + self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) + + # Compute dL_dpsi + self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) + self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + + # Compute dL_dKmm + self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB + self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC + self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + def approximate_likelihood(self): assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" if self.ep_proxy == 'DTC': @@ -88,6 +131,22 @@ class sparse_GP(GP): else: self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) self.beta, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() + self._ep_kernel_matrices() + self._computations() + + def _ep_kernel_matrices(self): + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) #FIXME include beta + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices) + self.psi1 = self.kern.K(self.Z,self.X) + self.psi2 = np.dot(self.psi1,self.psi1.T) + self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) + + def _ep_computations(self): # Y: EP likelihood is defined as a regression model for mu_tilde self.Y = self.v_tilde/self.beta self._Ymean = np.zeros((1,self.Y.shape[1])) @@ -99,50 +158,17 @@ class sparse_GP(GP): else: self.YYT = None self.mu_ = self.v_/self.tau_ - self._ep_covariance() - self._computations() - - def _ep_covariance(self): - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) #FIXME include beta - else: - #self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() - self.Knn_diag = self.kern.Kdiag(self.X,slices=self.Xslices) - self.psi0 = (self.beta*self.Knn_diag).sum() #TODO check dimensions - self.psi1 = self.kern.K(self.Z,self.X) - #self.psi2 = np.dot(self.psi1,self.psi1.T) - self.psi2 = np.dot(self.psi1,self.beta*self.psi1.T) - - def _compute_kernel_matrices(self): - # kernel computations, using BGPLVM notation - #TODO: slices for psi statistics (easy enough) - - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() - self.psi1 = self.kern.K(self.Z,self.X) - self.psi2 = np.dot(self.psi1,self.psi1.T) - - def _ep_computations(self): # TODO find routine to multiply triangular matrices self.V = self.beta*self.Y self.psi1V = np.dot(self.psi1, self.V) self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) #self.A = mdot(self.Lmi, self.beta*self.psi2, self.Lmi.T) - self.A = mdot(self.Lmi, self.psi2, self.Lmi.T) + self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) self.B = np.eye(self.M) + self.A self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) self.LLambdai = np.dot(self.LBi, self.Lmi) - #self.trace_K = self.psi0 - np.sum(np.dot(self.Lmi,self.psi1)**2,-1) #TODO check - self.trace_K = self.psi0 - np.trace(self.A) + self.trace_K = self.psi0.sum() - np.trace(self.A) self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) self.C = mdot(self.LLambdai, self.psi1V) self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) @@ -176,10 +202,15 @@ class sparse_GP(GP): Compute the (lower bound on the) log marginal likelihood """ beta_logdet = self.N*self.D*np.log(self.beta) if not self.EP else self.D*np.sum(np.log(self.beta)) - A = -0.5*self.N*self.D*(np.log(2.*np.pi)) - 0.5*beta_logdet - B = -0.5*self.beta*self.D*self.trace_K if not self.EP else -0.5*self.D*self.trace_K + if self.hetero_noise: + A = foo + B = bar + D = -0.5*self.trbetaYYT + else: + A = -0.5*self.N*self.D*(np.log(2.*np.pi)) - 0.5*beta_logdet + B = -0.5*self.beta*self.D*self.trace_K if not self.EP else -0.5*self.D*self.trace_K + D = -0.5*self.beta*self.trYYT C = -0.5*self.D * self.B_logdet - D = -0.5*self.beta*self.trYYT if not self.EP else -0.5*self.trbetaYYT E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) return A+B+C+D+E @@ -243,13 +274,14 @@ class sparse_GP(GP): noise_term = 1./self.beta if not self.EP else 0 Kxx = self.kern.Kdiag(Xnew) var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) + noise_term - return mu,var + return mu,var,None#TODO add phi for EP def plot(self, *args, **kwargs): """ Plot the fitted model: just call the GP_regression plot function and then add inducing inputs """ - GP_regression.plot(self,*args,**kwargs) + #GP_regression.plot(self,*args,**kwargs) + GP.plot(self,*args,**kwargs) if self.Q==1: pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) if self.has_uncertain_inputs: From 7737cecf6db40188ceaf626e2287d380c6705e0e Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Mon, 28 Jan 2013 18:01:55 +0000 Subject: [PATCH 006/197] EM algorithm --- GPy/examples/ep_fix.py | 1 + GPy/models/GP.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index c4e025dd..49ebd5aa 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -35,5 +35,6 @@ print m.checkgrad() m.optimize() #m.em(plot_all=False) # EM algorithm m.plot(samples=3) +m.EM() print(m) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 3a9f6de8..51da0490 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -229,6 +229,33 @@ class GP(model): phi = None if not self.EP else self.likelihood.predictive_mean(mu,var) return mu, var, phi + def EM(self,max_f_eval=20,epsilon=.1,plot_all=False): #TODO check this makes sense + """ + Fits sparse_EP and optimizes the hyperparametes iteratively until convergence is achieved. + """ + self.epsilon_em = epsilon + log_likelihood_change = self.epsilon_em + 1. + self.parameters_path = [self._get_params()] + self.approximate_likelihood() + self.site_approximations_path = [[self.ep_approx.tau_tilde,self.ep_approx.v_tilde]] + self.log_likelihood_path = [self.log_likelihood()] + iteration = 0 + while log_likelihood_change > self.epsilon_em: + print 'EM iteration', iteration + self.optimize(max_f_eval = max_f_eval) + log_likelihood_new = self.log_likelihood() + log_likelihood_change = log_likelihood_new - self.log_likelihood_path[-1] + if log_likelihood_change < 0: + print 'log_likelihood decrement' + self._set_params(self.parameters_path[-1]) + self.kern._set_params(self.parameters_path[-1]) + else: + self.approximate_likelihood() + self.log_likelihood_path.append(self.log_likelihood()) + self.parameters_path.append(self._get_params()) + self.site_approximations_path.append([self.ep_approx.tau_tilde,self.ep_approx.v_tilde]) + iteration += 1 + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): """ :param samples: the number of a posteriori samples to plot From d9a3226f4989c15ccb1f23b3daf6c76db5c46b8e Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 12:06:34 +0000 Subject: [PATCH 007/197] EM algorithm for EP. --- GPy/core/model.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index 4a1791bd..b6b280a1 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -381,6 +381,43 @@ class model(parameterised): print grad_string print '' - return False return True + + def EM(self,epsilon=.1,**kwargs): + """ + Expectation maximization for Expectation Propagation. + + kwargs are passed to the optimize function. They can be: + + :epsilon: convergence criterion + :max_f_eval: maximum number of function evaluations + :messages: whether to display during optimisation + :param optimzer: whice optimizer to use (defaults to self.preferred optimizer) + :type optimzer: string TODO: valid strings? + + """ + assert self.EP, "EM not available for gaussian likelihood" + log_change = epsilon + 1. + self.log_likelihood_record = [] + self.gp_params_record = [] + self.ep_params_record = [] + iteration = 0 + last_value = -np.exp(1000) + while log_change > epsilon or not iteration: + print 'EM iteration %s' %iteration + self.approximate_likelihood() + self.optimize(**kwargs) + new_value = self.log_likelihood() + log_change = new_value - last_value + if log_change > epsilon: + self.log_likelihood_record.append(new_value) + self.gp_params_record.append(self._get_params()) + self.ep_params_record.append((self.beta,self.Y,self.Z_ep)) + last_value = new_value + else: + convergence = False + self.beta, self.Y, self.Z_ep = self.ep_params_record[-1] + self._set_params(self.gp_params_record[-1]) + print "Log-likelihood decrement: %s \nLast iteration discarded." %log_change + iteration += 1 From 691aeeaf22ca28f28190af3ce8ba02d0d0205e94 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 12:07:19 +0000 Subject: [PATCH 008/197] GP model works now. --- GPy/models/GP.py | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 51da0490..95145978 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -13,7 +13,7 @@ from ..inference.likelihoods import likelihood,probit,poisson,gaussian class GP(model): """ - Gaussian Process model for regression + Gaussian Process model for regression and EP :param X: input observations :param Y: observed values @@ -35,7 +35,7 @@ class GP(model): #TODO: make beta parameter explicit #TODO: when using EP, predict needs to return 3 values otherwise it just needs 2. At the moment predict returns 3 values in any case. - def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): + def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,power_ep=[1.,1.]): # parse arguments self.Xslices = Xslices @@ -121,6 +121,9 @@ class GP(model): return self.kern._get_param_names_transformed() def approximate_likelihood(self): + """ + Approximates a non-gaussian likelihood using Expectation Propagation + """ assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" self.ep_approx = Full(self.K,self.likelihood,epsilon = self.epsilon_ep,power_ep=[self.eta,self.delta]) self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() @@ -170,7 +173,6 @@ class GP(model): def predict(self,Xnew, slices=None, full_cov=False): """ - Predict the function(s) at the new point(s) Xnew. Arguments @@ -193,7 +195,6 @@ class GP(model): If full_cov and self.D > 1, the return shape of var is Nnew x Nnew x self.D. If self.D == 1, the return shape is Nnew x Nnew. This is to allow for different normalisations of the output dimensions. - """ #normalise X values @@ -229,33 +230,6 @@ class GP(model): phi = None if not self.EP else self.likelihood.predictive_mean(mu,var) return mu, var, phi - def EM(self,max_f_eval=20,epsilon=.1,plot_all=False): #TODO check this makes sense - """ - Fits sparse_EP and optimizes the hyperparametes iteratively until convergence is achieved. - """ - self.epsilon_em = epsilon - log_likelihood_change = self.epsilon_em + 1. - self.parameters_path = [self._get_params()] - self.approximate_likelihood() - self.site_approximations_path = [[self.ep_approx.tau_tilde,self.ep_approx.v_tilde]] - self.log_likelihood_path = [self.log_likelihood()] - iteration = 0 - while log_likelihood_change > self.epsilon_em: - print 'EM iteration', iteration - self.optimize(max_f_eval = max_f_eval) - log_likelihood_new = self.log_likelihood() - log_likelihood_change = log_likelihood_new - self.log_likelihood_path[-1] - if log_likelihood_change < 0: - print 'log_likelihood decrement' - self._set_params(self.parameters_path[-1]) - self.kern._set_params(self.parameters_path[-1]) - else: - self.approximate_likelihood() - self.log_likelihood_path.append(self.log_likelihood()) - self.parameters_path.append(self._get_params()) - self.site_approximations_path.append([self.ep_approx.tau_tilde,self.ep_approx.v_tilde]) - iteration += 1 - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): """ :param samples: the number of a posteriori samples to plot From 9972862ea22164a89e05b1667a45cbadf8d780e9 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 12:08:50 +0000 Subject: [PATCH 009/197] Test file. --- GPy/examples/ep_fix.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 49ebd5aa..1d7b4741 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -26,15 +26,14 @@ likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) m = GPy.models.GP(data['X'],likelihood=likelihood) #m = GPy.models.GP(data['X'],likelihood.Y) - m.ensure_default_constraints() + +# Optimize and plot if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): m.approximate_likelihood() -print m.checkgrad() -# Optimize and plot -m.optimize() -#m.em(plot_all=False) # EM algorithm -m.plot(samples=3) +#m.optimize() m.EM() +print m.log_likelihood() +m.plot(samples=3) print(m) From 01f0378f840821fdac8acc0652be213ef77a536f Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 12:23:49 +0000 Subject: [PATCH 010/197] Other change. --- GPy/examples/ep_fix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 1d7b4741..8041cc91 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -29,8 +29,8 @@ m = GPy.models.GP(data['X'],likelihood=likelihood) m.ensure_default_constraints() # Optimize and plot -if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): - m.approximate_likelihood() +#if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): +# m.approximate_likelihood() #m.optimize() m.EM() From dbf920ebd5f72746876ce9f54efa4ac7401e25a9 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 12:29:22 +0000 Subject: [PATCH 011/197] Minor change in EM explanation. --- GPy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index b6b280a1..ccfbf298 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -397,7 +397,7 @@ class model(parameterised): :type optimzer: string TODO: valid strings? """ - assert self.EP, "EM not available for gaussian likelihood" + assert self.EP, "EM is not available for gaussian likelihood" log_change = epsilon + 1. self.log_likelihood_record = [] self.gp_params_record = [] From 217fa0e70eaad1eecc3ef77f541a03435ad7ef50 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 16:44:12 +0000 Subject: [PATCH 012/197] Now it works. --- GPy/examples/poisson.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/GPy/examples/poisson.py b/GPy/examples/poisson.py index 71d80b30..e15f310d 100644 --- a/GPy/examples/poisson.py +++ b/GPy/examples/poisson.py @@ -25,16 +25,14 @@ seed=default_seed """ X = np.arange(0,100,5)[:,None] -F = np.round(np.sin(X/18.) + .1*X) -E = np.random.randint(-3,3,20)[:,None] +F = np.round(np.sin(X/18.) + .1*X) + np.arange(5,25)[:,None] +E = np.random.randint(-5,5,20)[:,None] Y = F + E -pb.plot(X,F,'k-') -pb.plot(X,Y,'ro') pb.figure() -likelihood = GPy.inference.likelihoods.poisson(Y,scale=6.) +likelihood = GPy.inference.likelihoods.poisson(Y,scale=1.) m = GPy.models.GP(X,likelihood=likelihood) -#m = GPy.models.GP(data['X'],Y=likelihood.Y) +#m = GPy.models.GP(X,Y=likelihood.Y) m.constrain_positive('var') m.constrain_positive('len') From cab3b77b6b4963415ee3e0143a650e560478ddb5 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 16:44:42 +0000 Subject: [PATCH 013/197] Assertions included. --- GPy/inference/likelihoods.py | 84 ++++++++++-------------------------- 1 file changed, 22 insertions(+), 62 deletions(-) diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index 864afa57..b170dc3d 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -9,65 +9,18 @@ import pylab as pb from ..util.plot import gpplot class likelihood: - def __init__(self,Y,location=0,scale=1): - """ - Likelihood class for doing Expectation propagation + """ + Likelihood class for doing Expectation propagation - :param Y: observed output (Nx1 numpy.darray) - ..Note:: Y values allowed depend on the likelihood used - """ + :param Y: observed output (Nx1 numpy.darray) + ..Note:: Y values allowed depend on the likelihood used + """ + def __init__(self,Y,location=0,scale=1): self.Y = Y self.N = self.Y.shape[0] self.location = location self.scale = scale - def plot1D(self,X,mean,var,Z=None,mean_Z=None,var_Z=None,samples=0): - """ - Plot the predictive distribution of the GP model for 1-dimensional inputs - - :param X: The points at which to make a prediction - :param Mean: mean values at X - :param Var: variance values at X - :param Z: Set of points to be highlighted in the plot, i.e. inducing points - :param mean_Z: mean values at Z - :param var_Z: variance values at Z - :samples: Number of samples to plot - """ - assert X.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,mean,var.flatten()) - if samples: #NOTE why don't we put samples as a parameter of gpplot - s = np.random.multivariate_normal(mean.flatten(),np.diag(var),samples) - pb.plot(X.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - #pb.subplot(211) - #self.plot1Da(X,mean,var,Z,mean_Z,var_Z) - - - def plot1Da(self,X,mean,var,Z=None,mean_Z=None,var_Z=None): - """ - Plot the predictive distribution of the GP model for 1-dimensional inputs - - :param X_new: The points at which to make a prediction - :param Mean_new: mean values at X_new - :param Var_new: variance values at X_new - :param X_u: input (inducing) points used to train the model - :param Mean_u: mean values at X_u - :param Var_new: variance values at X_u - """ - assert X.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,mean,var.flatten()) - pb.errorbar(Z.flatten(),mean_Z.flatten(),2*np.sqrt(var_Z.flatten()),fmt='r+') - pb.plot(Z,mean_Z,'ro') - - """ - def plot1Db(self,X_obs,X,phi,Z=None): - assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,phi,np.zeros(X.shape[0])) - pb.plot(X_obs,(self.Y+1)/2,'kx',mew=1.5) - pb.ylim(-0.2,1.2) - if Z is not None: - pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) - - """ def plot2D(self,X,X_new,F_new,U=None): """ Predictive distribution of the fitted GP model for 2-dimensional inputs @@ -106,6 +59,10 @@ class probit(likelihood): L(x) = \\Phi (Y_i*f_i) $$ """ + def __init__(self,Y,location=0,scale=1): + assert np.sum(np.abs(Y)-1) == 0, "Output values must be either -1 or 1" + likelihood.__init__(self,Y,location,scale) + def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -146,6 +103,10 @@ class poisson(likelihood): L(x) = \exp(\lambda) * \lambda**Y_i / Y_i! $$ """ + def __init__(self,Y,location=0,scale=1): + assert len(Y[Y<0]) == 0, "Output cannot have negative values" + likelihood.__init__(self,Y,location,scale) + def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -203,20 +164,19 @@ class poisson(likelihood): sigma2_hat = m2 - mu_hat**2 # Second central moment return float(Z_hat), float(mu_hat), float(sigma2_hat) - def plot1Db(self,X,X_new,F_new,F2_new=None,U=None): - pb.subplot(212) - #gpplot(X_new,F_new,np.sqrt(F2_new)) - pb.plot(X_new,F_new)#,np.sqrt(F2_new)) #FIXME - pb.plot(X,self.Y,'kx',mew=1.5) - if U is not None: - pb.plot(U,np.ones(U.shape[0])*self.Y.min()*.8,'r|',mew=1.5,markersize=12) def predictive_mean(self,mu,variance): return np.exp(mu*self.scale + self.location) - def predictive_variance(self,mu,variance): - return mu + def _log_likelihood_gradients(): raise NotImplementedError + def plot(self,X,phi,X_obs,Z=None): + assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' + gpplot(X,phi,np.zeros(X.shape[0])) + pb.plot(X_obs,self.Y,'kx',mew=1.5) + if Z is not None: + pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) + class gaussian(likelihood): """ Gaussian likelihood From ec89c4efc300b7e3e5622c6cd018d6fe7deda55b Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 16:45:00 +0000 Subject: [PATCH 014/197] _compute_GP_variables --- GPy/inference/EP.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py index 5d571888..5c473a8f 100644 --- a/GPy/inference/EP.py +++ b/GPy/inference/EP.py @@ -48,13 +48,13 @@ class EP: self.tau_tilde = np.zeros(self.N) self.v_tilde = np.zeros(self.N) - def restart_EP(self): - """ - Set the EP approximation to initial state - """ - self.tau_tilde = np.zeros(self.N) - self.v_tilde = np.zeros(self.N) - self.mu = np.zeros(self.N) + def _compute_GP_variables(self): + #Variables to be called from GP + mu_tilde = self.v_tilde/self.tau_tilde #When calling EP, this variable is used instead of Y in the GP model + sigma_sum = 1./self.tau_ + 1./self.tau_tilde + mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 + Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant + return self.tau_tilde[:,None], mu_tilde[:,None], Z_ep class Full(EP): def fit_EP(self): @@ -122,12 +122,7 @@ class Full(EP): self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) - #Variables to be called from GP - mu_tilde = self.v_tilde/self.tau_tilde #When calling EP, this variable is used instead of Y in the GP model - sigma_sum = 1./self.tau_ + 1./self.tau_tilde - mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 - Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant - return self.tau_tilde[:,None], mu_tilde[:,None], Z_ep + return self._compute_GP_variables() class DTC(EP): def fit_EP(self): @@ -212,7 +207,8 @@ class DTC(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) - return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] + + return self._compute_GP_variables() class FITC(EP): def fit_EP(self): @@ -313,4 +309,5 @@ class FITC(EP): epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) self.np2.append(self.v_tilde.copy()) - return self.tau_tilde[:,None], self.v_tilde[:,None], self.Z_hat[:,None], self.tau_[:,None], self.v_[:,None] + + return self._compute_GP_variables() From 0a88df62c3bd9d7bac7b183ca316e666a452438b Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 16:45:31 +0000 Subject: [PATCH 015/197] Minor changes. --- GPy/models/sparse_GP.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 655f6252..f5381eed 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -35,9 +35,13 @@ class sparse_GP(GP): :type beta: float :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) :type normalize_(X|Y): bool + :parm likelihood: a GPy likelihood, defaults to gaussian + :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 + :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] + :type powerep: list """ - def __init__(self,X,Y=None,kernel=None, X_uncertainty=None, beta=100., Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,epsilon_em=.1,power_ep=[1.,1.]): + def __init__(self,X,Y=None,kernel=None,X_uncertainty=None,beta=100.,Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,power_ep=[1.,1.]): if Z is None: self.Z = np.random.permutation(X.copy())[:M] @@ -53,7 +57,7 @@ class sparse_GP(GP): self.has_uncertain_inputs=True self.X_uncertainty = X_uncertainty - GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,epsilon_em=epsilon_em,power_ep=power_ep) + GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) self.trYYT = np.sum(np.square(self.Y)) if not self.EP else None @@ -91,6 +95,9 @@ class sparse_GP(GP): else: if self.hetero_noise: print "rick's stuff here" + + + else: self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() self.psi1 = self.kern.K(self.Z,self.X) From bb1e0021d7ae0ebd5d06ec19e2f6b47d02d240c9 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Tue, 29 Jan 2013 18:01:47 +0000 Subject: [PATCH 016/197] More changes. --- GPy/models/GP.py | 4 +- GPy/models/sparse_GP.py | 136 ++++++++++++++-------------------------- 2 files changed, 48 insertions(+), 92 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 95145978..4d80ab87 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -73,7 +73,6 @@ class GP(model): self.EP = False self.Y = Y self.beta = 100.#FIXME beta should be an explicit parameter for this model - # Here's some simple normalisation if normalize_Y: self._Ymean = Y.mean(0)[None,:] @@ -88,8 +87,9 @@ class GP(model): self.YYT = np.dot(self.Y, self.Y.T) else: self.YYT = None - else: + if self.D > 1: + raise NotImplementedError, "EP is not implemented for D > 1" # Y is defined after approximating the likelihood self.EP = True self.eta,self.delta = power_ep diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index f5381eed..ea1ba100 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -60,48 +60,52 @@ class sparse_GP(GP): GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) self.trYYT = np.sum(np.square(self.Y)) if not self.EP else None - #normalise X uncertainty also if self.has_uncertain_inputs: self.X_uncertainty /= np.square(self._Xstd) def _set_params(self, p): + self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) if not self.EP: - self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) - self.beta = p[self.M*self.Q] + #self.beta = p[self.M*self.Q] + self.beta = np.repeat(p[self.M*self.Q],self.N)[:,None] self.kern._set_params(p[self.Z.size + 1:]) self.beta2 = self.beta**2 - self._compute_kernel_matrices() - self._computations() else: - self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) self.kern._set_params(p[self.Z.size:]) - #self._compute_kernel_matrices() this is replaced by _ep_kernel_matrices - self._ep_kernel_matrices() - self._ep_computations() + if self.Y is None: + self.Y = np.ones([self.N,1]) + self._compute_kernel_matrices() + self._computations() + + def _get_params(self): + if not self.EP: + return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) + else: + return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) + + def _get_param_names(self): + if not self.EP: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() + else: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() def _compute_kernel_matrices(self): # kernel computations, using BGPLVM notation #TODO: slices for psi statistics (easy enough) - self.Kmm = self.kern.K(self.Z) if self.has_uncertain_inputs: - if self.hetero_noise: - raise NotImplementedError, "uncertain ips and het noise not yet supported" - else: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + if not self.EP: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - else: - if self.hetero_noise: - print "rick's stuff here" - - - + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty)#FIXME add beta vector else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() - self.psi1 = self.kern.K(self.Z,self.X) - self.psi2 = np.dot(self.psi1,self.psi1.T) + raise NotImplementedError, "uncertain_inputs not yet supported for EP" + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() FIXME + self.psi1 = self.kern.K(self.Z,self.X) + self.psi2 = np.dot(self.psi1,self.psi1.T) + self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) def _computations(self): # TODO find routine to multiply triangular matrices @@ -109,17 +113,17 @@ class sparse_GP(GP): self.psi1V = np.dot(self.psi1, self.V) self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.A = mdot(self.Lmi, self.beta*self.psi2, self.Lmi.T) + self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) self.B = np.eye(self.M) + self.A self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) self.LLambdai = np.dot(self.LBi, self.Lmi) - self.trace_K = self.psi0 - np.trace(self.A)/self.beta + self.trace_K = self.psi0.sum() - np.trace(self.A) self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) self.C = mdot(self.LLambdai, self.psi1V) self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) # Compute dL_dpsi - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones([self.N,1]) self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) @@ -133,76 +137,28 @@ class sparse_GP(GP): if self.ep_proxy == 'DTC': self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) elif self.ep_proxy == 'FITC': - self.Knn_diag = self.kern.psi0(self.Z,self.X, self.X_uncertainty) #TODO psi0 already calculates this - self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.Knn_diag,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.psi0,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) else: self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - self.beta, self.v_tilde, self.Z_hat, self.tau_, self.v_=self.ep_approx.fit_EP() - self._ep_kernel_matrices() + self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() self._computations() - def _ep_kernel_matrices(self): - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) #FIXME include beta - else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices) - self.psi1 = self.kern.K(self.Z,self.X) - self.psi2 = np.dot(self.psi1,self.psi1.T) - self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) - - def _ep_computations(self): - # Y: EP likelihood is defined as a regression model for mu_tilde - self.Y = self.v_tilde/self.beta - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - self.trbetaYYT = np.sum(self.beta*np.square(self.Y)) - if self.D > self.N: - # then it's more efficient to store YYT - self.YYT = np.dot(self.Y, self.Y.T) - else: - self.YYT = None - self.mu_ = self.v_/self.tau_ - # TODO find routine to multiply triangular matrices - self.V = self.beta*self.Y - self.psi1V = np.dot(self.psi1, self.V) - self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - #self.A = mdot(self.Lmi, self.beta*self.psi2, self.Lmi.T) - self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) - self.B = np.eye(self.M) + self.A - self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) - self.LLambdai = np.dot(self.LBi, self.Lmi) - self.trace_K = self.psi0.sum() - np.trace(self.A) - self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) - self.C = mdot(self.LLambdai, self.psi1V) - self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) - - # Compute dL_dpsi - #self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) - self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() * np.ones(self.N) #TODO check - self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) - #self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - - # Compute dL_dKmm - self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB - self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC - self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE - - def _get_params(self): + def log_likelihood(self): + """ + Compute the (lower bound on the) log marginal likelihood + """ if not self.EP: - return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) + A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) else: - return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) + A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) + B = -0.5*self.D*self.trace_K + C = -0.5*self.D * self.B_logdet + D = -0.5*self.beta*self.trYYT + E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) + return A+B+C+D+E + + - def _get_param_names(self): - if not self.EP: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() - else: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() def log_likelihood(self): """ From d1a0883c12f49bc25956812b4dcdfc0c66ca3b3b Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 29 Jan 2013 23:54:02 +0000 Subject: [PATCH 017/197] Log-likelihood,predictions and plotting are working. --- GPy/examples/sparse_ep_fix.py | 19 ++++---- GPy/inference/EP.py | 4 +- GPy/models/GP.py | 16 ++++--- GPy/models/sparse_GP.py | 81 +++++++++++++++++++---------------- 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/GPy/examples/sparse_ep_fix.py b/GPy/examples/sparse_ep_fix.py index 7e3f1fc3..f2c25898 100644 --- a/GPy/examples/sparse_ep_fix.py +++ b/GPy/examples/sparse_ep_fix.py @@ -31,18 +31,17 @@ noise = GPy.kern.white(1) kernel = rbf + noise # create simple GP model -#m1 = GPy.models.sparse_GP(X, Y, kernel, M=M) -m1 = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) +m = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) +#m = GPy.models.sparse_GP(X, Y, kernel, M=M) -print m1.checkgrad() # contrain all parameters to be positive -m1.constrain_positive('(variance|lengthscale|precision)') -#m1.constrain_positive('(variance|lengthscale)') -#m1.constrain_fixed('prec',10.) - +m.ensure_default_constraints() +if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): + m.approximate_likelihood() +print m.checkgrad() #check gradient FIXME unit test please # optimize and plot -m1.optimize('tnc', messages = 1) -m1.plot() -# print(m1) +#m.optimize('tnc', messages = 1) +m.plot(samples=3,full_cov=False) +# print(m) diff --git a/GPy/inference/EP.py b/GPy/inference/EP.py index 5c473a8f..c3aad7c1 100644 --- a/GPy/inference/EP.py +++ b/GPy/inference/EP.py @@ -136,7 +136,7 @@ class DTC(EP): q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) Sigma0 = Qnn = Knm*Kmmi*Kmn """ - self.Kmmi, self.Kmm_hld = pdinv(self.Kmm) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) self.KmnKnm = np.dot(self.Kmn, self.Kmn.T) self.KmmiKmn = np.dot(self.Kmmi,self.Kmn) self.Qnn_diag = np.sum(self.Kmn*self.KmmiKmn,-2) @@ -222,7 +222,7 @@ class FITC(EP): q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) Sigma0 = diag(Knn-Qnn) + Qnn, Qnn = Knm*Kmmi*Kmn """ - self.Kmmi, self.Kmm_hld = pdinv(self.Kmm) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) self.P0 = self.Kmn.T self.KmnKnm = np.dot(self.P0.T, self.P0) self.KmmiKmn = np.dot(self.Kmmi,self.P0.T) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 4d80ab87..482143d6 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -196,7 +196,6 @@ class GP(model): This is to allow for different normalisations of the output dimensions. """ - #normalise X values Xnew = (Xnew.copy() - self._Xmean) / self._Xstd mu, var, phi = self._raw_predict(Xnew, slices, full_cov) @@ -224,13 +223,18 @@ class GP(model): if full_cov: Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) var = Kxx - np.dot(KiKx.T,Kx) + if self.EP: + raise NotImplementedError, "full_cov = True not implemented for EP" + #var = np.diag(var)[:,None] + #phi = self.likelihood.predictive_mean(mu,var) else: Kxx = self.kern.Kdiag(_Xnew, slices=slices) var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - phi = None if not self.EP else self.likelihood.predictive_mean(mu,var) + if self.EP: + phi = self.likelihood.predictive_mean(mu,var) return mu, var, phi - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): """ :param samples: the number of a posteriori samples to plot :param which_data: which if the training data to plot (default all) @@ -268,13 +272,13 @@ class GP(model): if self.X.shape[1]==1: Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - m,v,phi = self.predict(Xnew,slices=which_functions) + m,v,phi = self.predict(Xnew,slices=which_functions,full_cov=full_cov) if self.EP: pb.subplot(211) gpplot(Xnew,m,v) if samples: #NOTE why don't we put samples as a parameter of gpplot - s = np.random.multivariate_normal(m.flatten(),np.diag(v),samples) + s = np.random.multivariate_normal(m.flatten(),np.diag(v.flatten()),samples) pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) pb.plot(Xorig,Yorig,'kx',mew=1.5) pb.xlim(xmin,xmax) @@ -288,7 +292,7 @@ class GP(model): resolution = 50 or resolution xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] Xtest = np.vstack((xx.flatten(),yy.flatten())).T - zz,vv,phi = self.predict(Xtest,slices=which_functions) + zz,vv,phi = self.predict(Xtest,slices=which_functions,full_cov=full_cov) zz = zz.reshape(resolution,resolution) pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index ea1ba100..8b1b6fb9 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -7,7 +7,7 @@ from ..util.linalg import mdot, jitchol, chol_inv, pdinv from ..util.plot import gpplot from .. import kern from GP import GP -from ..inference.EP import Full +from ..inference.EP import Full,DTC,FITC from ..inference.likelihoods import likelihood,probit,poisson,gaussian #Still TODO: @@ -36,6 +36,8 @@ class sparse_GP(GP): :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) :type normalize_(X|Y): bool :parm likelihood: a GPy likelihood, defaults to gaussian + :param method_ep: sparse approximation used by Expectation Propagation algorithm, defaults to DTC + :type M: string (Full|DTC|FITC) :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] :type powerep: list @@ -58,17 +60,22 @@ class sparse_GP(GP): self.X_uncertainty = X_uncertainty GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) - self.trYYT = np.sum(np.square(self.Y)) if not self.EP else None #normalise X uncertainty also if self.has_uncertain_inputs: self.X_uncertainty /= np.square(self._Xstd) + if not self.EP: + self.trYYT = np.sum(np.square(self.Y)) + else: + self.method_ep = method_ep + + def _set_params(self, p): self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) if not self.EP: - #self.beta = p[self.M*self.Q] - self.beta = np.repeat(p[self.M*self.Q],self.N)[:,None] + self.beta = p[self.M*self.Q] + #self.beta = np.repeat(p[self.M*self.Q],self.N)[:,None] self.kern._set_params(p[self.Z.size + 1:]) self.beta2 = self.beta**2 else: @@ -76,7 +83,7 @@ class sparse_GP(GP): if self.Y is None: self.Y = np.ones([self.N,1]) self._compute_kernel_matrices() - self._computations() + self._computations() #NOTE At this point computations of dL are not needed def _get_params(self): if not self.EP: @@ -123,24 +130,29 @@ class sparse_GP(GP): self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) # Compute dL_dpsi - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones([self.N,1]) + self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() * np.ones(self.N) self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) - self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + #self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + self.dL_dpsi2 = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) # Compute dL_dKmm self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB - self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC - self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + #self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC + self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*mdot(self.LBL_inv, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC + #self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + self.dL_dKmm += np.dot(np.dot(self.G,self.psi2_beta_scaled) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE def approximate_likelihood(self): assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - if self.ep_proxy == 'DTC': + if self.method_ep == 'DTC': self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - elif self.ep_proxy == 'FITC': + elif self.method_ep == 'FITC': self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.psi0,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) else: self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() + print "Aqui toy" + self.trbetaYYT = np.sum(np.square(self.Y)*self.beta) self._computations() def log_likelihood(self): @@ -149,30 +161,11 @@ class sparse_GP(GP): """ if not self.EP: A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) + D = -0.5*self.beta*self.trYYT else: A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) - B = -0.5*self.D*self.trace_K - C = -0.5*self.D * self.B_logdet - D = -0.5*self.beta*self.trYYT - E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) - return A+B+C+D+E - - - - - def log_likelihood(self): - """ - Compute the (lower bound on the) log marginal likelihood - """ - beta_logdet = self.N*self.D*np.log(self.beta) if not self.EP else self.D*np.sum(np.log(self.beta)) - if self.hetero_noise: - A = foo - B = bar D = -0.5*self.trbetaYYT - else: - A = -0.5*self.N*self.D*(np.log(2.*np.pi)) - 0.5*beta_logdet - B = -0.5*self.beta*self.D*self.trace_K if not self.EP else -0.5*self.D*self.trace_K - D = -0.5*self.beta*self.trYYT + B = -0.5*self.D*self.trace_K C = -0.5*self.D * self.B_logdet E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) return A+B+C+D+E @@ -223,21 +216,33 @@ class sparse_GP(GP): return dL_dZ def _log_likelihood_gradients(self): - return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) + if not self.EP: + return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) + else: + return np.hstack([self.dL_dZ().flatten(), self.dL_dtheta()]) def _raw_predict(self, Xnew, slices, full_cov=False): """Internal helper function for making predictions, does not account for normalisation""" Kx = self.kern.K(self.Z, Xnew) mu = mdot(Kx.T, self.LBL_inv, self.psi1V) + phi = None if full_cov: - noise_term = np.eye(Xnew.shape[0])/self.beta if not self.EP else 0 Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) + noise_term + var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) + if not self.EP: + var += np.eye(Xnew.shape[0])/self.beta # TODO: This beta doesn't belong here in the EP case. + else: + raise NotImplementedError, "full_cov = True not implemented for EP" + #var = np.diag(var)[:,None] + #phi = self.likelihood.predictive_mean(mu,var) else: - noise_term = 1./self.beta if not self.EP else 0 Kxx = self.kern.Kdiag(Xnew) - var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) + noise_term - return mu,var,None#TODO add phi for EP + var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) + if not self.EP: + var += 1./self.beta # TODO: This beta doesn't belong here in the EP case. + else: + phi = self.likelihood.predictive_mean(mu,var) + return mu,var,phi def plot(self, *args, **kwargs): """ From 29eb61d65efd224dc63b9141f7361d437119a3f3 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Wed, 30 Jan 2013 12:14:32 +0000 Subject: [PATCH 018/197] EP plots samples now for the phi transformation. --- GPy/examples/poisson.py | 2 +- GPy/examples/sparse_ep_fix.py | 2 ++ GPy/inference/likelihoods.py | 24 +++++++++++++++++++----- GPy/models/GP.py | 2 +- GPy/models/sparse_GP.py | 1 - 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/GPy/examples/poisson.py b/GPy/examples/poisson.py index e15f310d..934637f1 100644 --- a/GPy/examples/poisson.py +++ b/GPy/examples/poisson.py @@ -43,6 +43,6 @@ print m.checkgrad() # Optimize and plot m.optimize() #m.em(plot_all=False) # EM algorithm -m.plot() +m.plot(samples=4) print(m) diff --git a/GPy/examples/sparse_ep_fix.py b/GPy/examples/sparse_ep_fix.py index f2c25898..ff90f2bb 100644 --- a/GPy/examples/sparse_ep_fix.py +++ b/GPy/examples/sparse_ep_fix.py @@ -14,6 +14,7 @@ pb.ion() N = 500 M = 5 +pb.close('all') ###################################### ## 1 dimensional example @@ -42,6 +43,7 @@ print m.checkgrad() #check gradient FIXME unit test please # optimize and plot #m.optimize('tnc', messages = 1) +m.EM() m.plot(samples=3,full_cov=False) # print(m) diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index b170dc3d..acf1aa2d 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -83,12 +83,20 @@ class probit(likelihood): var = var.flatten() return stats.norm.cdf(mu/np.sqrt(1+var)) + def predictive_var(self,mu,var): + p=self.predictive_mean(mu,var) + return p*(1-p) + def _log_likelihood_gradients(): raise NotImplementedError - def plot(self,X,phi,X_obs,Z=None): + def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,phi,np.zeros(X.shape[0])) + phi_var = self.predictive_var(mu,var) + gpplot(X,phi,phi_var) + if samples: + phi_samples = np.vstack([np.random.binomial(1,phi.flatten()) for s in range(samples)]) + pb.plot(X,phi_samples.T,'x', alpha = 0.4, c='#3465a4' ) pb.plot(X_obs,(self.Y+1)/2,'kx',mew=1.5) if Z is not None: pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) @@ -164,16 +172,22 @@ class poisson(likelihood): sigma2_hat = m2 - mu_hat**2 # Second central moment return float(Z_hat), float(mu_hat), float(sigma2_hat) - def predictive_mean(self,mu,variance): + def predictive_mean(self,mu,var): return np.exp(mu*self.scale + self.location) + def predictive_var(self,mu,var): + return predictive_mean(mu,var) + def _log_likelihood_gradients(): raise NotImplementedError - def plot(self,X,phi,X_obs,Z=None): + def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,phi,np.zeros(X.shape[0])) + gpplot(X,phi,phi.flatten()) pb.plot(X_obs,self.Y,'kx',mew=1.5) + if samples: + phi_samples = np.vstack([np.random.poisson(phi.flatten(),phi.size) for s in range(samples)]) + pb.plot(X,phi_samples.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) if Z is not None: pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 482143d6..8222fd6a 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -285,7 +285,7 @@ class GP(model): if self.EP: pb.subplot(212) - self.likelihood.plot(Xnew,phi,self.X) + self.likelihood.plot(Xnew,m,v,phi,self.X,samples=samples) pb.xlim(xmin,xmax) elif self.X.shape[1]==2: diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 8b1b6fb9..ba07254f 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -151,7 +151,6 @@ class sparse_GP(GP): else: self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() - print "Aqui toy" self.trbetaYYT = np.sum(np.square(self.Y)*self.beta) self._computations() From d8eb155622e51a4a4fb62118af696b7d57c21aa8 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Wed, 30 Jan 2013 16:00:03 +0000 Subject: [PATCH 019/197] Working for regression, still some bugs for EP. --- GPy/examples/sparse_ep_fix.py | 31 +++++++++++++------- GPy/models/sparse_GP.py | 55 ++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/GPy/examples/sparse_ep_fix.py b/GPy/examples/sparse_ep_fix.py index ff90f2bb..defcb4eb 100644 --- a/GPy/examples/sparse_ep_fix.py +++ b/GPy/examples/sparse_ep_fix.py @@ -32,18 +32,29 @@ noise = GPy.kern.white(1) kernel = rbf + noise # create simple GP model -m = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) -#m = GPy.models.sparse_GP(X, Y, kernel, M=M) +#m = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) # contrain all parameters to be positive +#m.constrain_fixed('prec',100.) +m = GPy.models.sparse_GP(X, Y, kernel, M=M) m.ensure_default_constraints() -if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): - m.approximate_likelihood() +#if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): +# m.approximate_likelihood() print m.checkgrad() -#check gradient FIXME unit test please -# optimize and plot -#m.optimize('tnc', messages = 1) -m.EM() -m.plot(samples=3,full_cov=False) -# print(m) +m.optimize('tnc', messages = 1) +m.plot(samples=3) +print m +n = GPy.models.sparse_GP(X,Y=None, kernel=kernel, M=M,likelihood= likelihood) +n.ensure_default_constraints() +if not isinstance(n.likelihood,GPy.inference.likelihoods.gaussian): + n.approximate_likelihood() +print n.checkgrad() +pb.figure() +n.plot() + +""" +m = GPy.models.sparse_GP_regression(X, Y, kernel, M=M) +m.ensure_default_constraints() +print m.checkgrad() +""" diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index ba07254f..7f287174 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -10,6 +10,7 @@ from GP import GP from ..inference.EP import Full,DTC,FITC from ..inference.likelihoods import likelihood,probit,poisson,gaussian + #Still TODO: # make use of slices properly (kernel can now do this) # enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) @@ -35,12 +36,6 @@ class sparse_GP(GP): :type beta: float :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) :type normalize_(X|Y): bool - :parm likelihood: a GPy likelihood, defaults to gaussian - :param method_ep: sparse approximation used by Expectation Propagation algorithm, defaults to DTC - :type M: string (Full|DTC|FITC) - :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 - :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] - :type powerep: list """ def __init__(self,X,Y=None,kernel=None,X_uncertainty=None,beta=100.,Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,power_ep=[1.,1.]): @@ -70,20 +65,21 @@ class sparse_GP(GP): else: self.method_ep = method_ep + #normalise X uncertainty also + if self.has_uncertain_inputs: + self.X_uncertainty /= np.square(self._Xstd) def _set_params(self, p): self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) if not self.EP: self.beta = p[self.M*self.Q] - #self.beta = np.repeat(p[self.M*self.Q],self.N)[:,None] self.kern._set_params(p[self.Z.size + 1:]) - self.beta2 = self.beta**2 else: self.kern._set_params(p[self.Z.size:]) if self.Y is None: self.Y = np.ones([self.N,1]) self._compute_kernel_matrices() - self._computations() #NOTE At this point computations of dL are not needed + self._computations() def _get_params(self): if not self.EP: @@ -97,19 +93,22 @@ class sparse_GP(GP): else: return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() + def _compute_kernel_matrices(self): # kernel computations, using BGPLVM notation #TODO: slices for psi statistics (easy enough) + self.Kmm = self.kern.K(self.Z) if self.has_uncertain_inputs: if not self.EP: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() NOTE psi0 is now a vector self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty)#FIXME add beta vector + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) + #self.psi2_beta_scaled = ? else: raise NotImplementedError, "uncertain_inputs not yet supported for EP" else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() FIXME + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() self.psi1 = self.kern.K(self.Z,self.X) self.psi2 = np.dot(self.psi1,self.psi1.T) self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) @@ -124,22 +123,29 @@ class sparse_GP(GP): self.B = np.eye(self.M) + self.A self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) self.LLambdai = np.dot(self.LBi, self.Lmi) - self.trace_K = self.psi0.sum() - np.trace(self.A) self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) self.C = mdot(self.LLambdai, self.psi1V) self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) + self.trace_K_beta_scaled = (self.psi0*self.beta).sum() - np.trace(self.A) + if not self.EP: + self.trace_K = self.psi0.sum() - np.trace(self.A)/self.beta # Compute dL_dpsi - self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() * np.ones(self.N) self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) - #self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - self.dL_dpsi2 = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + if not self.EP: + self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + if self.has_uncertain_inputs: + self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + else: + self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + else: + self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() + if not self.has_uncertain_inputs: + self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) # Compute dL_dKmm self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB - #self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*self.beta*mdot(self.LBL_inv, self.psi2, self.Kmmi) + self.Kmmi) # dC self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*mdot(self.LBL_inv, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC - #self.dL_dKmm += np.dot(np.dot(self.G,self.beta*self.psi2) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE self.dL_dKmm += np.dot(np.dot(self.G,self.psi2_beta_scaled) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE def approximate_likelihood(self): @@ -164,7 +170,7 @@ class sparse_GP(GP): else: A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) D = -0.5*self.trbetaYYT - B = -0.5*self.D*self.trace_K + B = -0.5*self.D*self.trace_K_beta_scaled C = -0.5*self.D * self.B_logdet E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) return A+B+C+D+E @@ -194,7 +200,7 @@ class sparse_GP(GP): dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape else: #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1) #dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) @@ -210,7 +216,7 @@ class sparse_GP(GP): dL_dZ += self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) else: #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1)#dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) return dL_dZ @@ -229,16 +235,14 @@ class sparse_GP(GP): Kxx = self.kern.K(Xnew) var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) if not self.EP: - var += np.eye(Xnew.shape[0])/self.beta # TODO: This beta doesn't belong here in the EP case. + var += np.eye(Xnew.shape[0])/self.beta else: raise NotImplementedError, "full_cov = True not implemented for EP" - #var = np.diag(var)[:,None] - #phi = self.likelihood.predictive_mean(mu,var) else: Kxx = self.kern.Kdiag(Xnew) var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) if not self.EP: - var += 1./self.beta # TODO: This beta doesn't belong here in the EP case. + var += 1./self.beta else: phi = self.likelihood.predictive_mean(mu,var) return mu,var,phi @@ -247,7 +251,6 @@ class sparse_GP(GP): """ Plot the fitted model: just call the GP_regression plot function and then add inducing inputs """ - #GP_regression.plot(self,*args,**kwargs) GP.plot(self,*args,**kwargs) if self.Q==1: pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) From c17d4758246df2bd53fc26d0612e880d48083ece Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:02:27 +0000 Subject: [PATCH 020/197] Trying to fix docs, might break them --- doc/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 474836a2..9fb5f02a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,7 +33,10 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: sys.path.append("../GPy") os.system("pwd") - os.system("sphinx-apidoc -f -o . ../GPy") + os.system("cd ..") + #os.system("sphinx-apidoc -f -o . ../GPy") + os.system("sphinx-apidoc -f -o ./docs ./GPy") + os.system("cd ./docs") # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 85572836939c4cb79f810f5e6202e0f201e9b8e6 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:08:25 +0000 Subject: [PATCH 021/197] Changed docs back for newGP --- doc/conf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 9fb5f02a..474836a2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,10 +33,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: sys.path.append("../GPy") os.system("pwd") - os.system("cd ..") - #os.system("sphinx-apidoc -f -o . ../GPy") - os.system("sphinx-apidoc -f -o ./docs ./GPy") - os.system("cd ./docs") + os.system("sphinx-apidoc -f -o . ../GPy") # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 1fb7ebe67a6d640a728e8f1ac5cfcea6bfba4ff1 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:09:23 +0000 Subject: [PATCH 022/197] Attempting to fix docs but may break them --- doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index f1c120b5..e76bf8c9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,7 +33,9 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: sys.path.append("../GPy") os.system("pwd") - os.system("sphinx-apidoc -f -o . ../GPy") + os.system("cd ..") + os.system("sphinx-apidoc -f -o ./docs ./GPy") + os.system("cd ./docs") # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From a605416d060e71441eae899798187eaddbaf9834 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:15:54 +0000 Subject: [PATCH 023/197] Adding pylab mock module --- doc/conf.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index e76bf8c9..6ebf2684 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,6 +13,29 @@ import sys, os +#Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html +class Mock(object): + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return Mock() + + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() + +MOCK_MODULES = ['pylab'] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. From 690bdcd62b32e993a398d9c148f039ea40cef1a9 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:17:52 +0000 Subject: [PATCH 024/197] Same again --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 6ebf2684..cf574676 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -32,7 +32,7 @@ class Mock(object): else: return Mock() -MOCK_MODULES = ['pylab'] +MOCK_MODULES = ['pylab', 'packages.pylab'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From b94ea750485cbea132d46e95978e93d68c75b574 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:22:59 +0000 Subject: [PATCH 025/197] Above again.... --- doc/conf.py | 52 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index cf574676..0baee5c8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -14,32 +14,52 @@ import sys, os #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html + +# To avoid problem with ReadTheDocs and compiled extensions. class Mock(object): - def __init__(self, *args, **kwargs): + """Special Healpix values for masked pixels. + """ + pi = 3.141516 + class Axes(object): + pass + class Locator(object): + pass + class Normalize(object): + pass + def __init__(self, *args): + """Mock init + """ pass - def __call__(self, *args, **kwargs): + def __getattr__(self, name): + return Mock + + def __div__(self, x): return Mock() - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() + def __getitem__(self, idx): + return str(Mock()) -MOCK_MODULES = ['pylab', 'packages.pylab'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() +try: + import GPy +except exceptions.ImportError: + MOCK_MODULES = ['matplotlib', 'pylab', 'matplotlib.colors', + 'matplotlib.cbook', 'pyfits', + 'numpy'] + + for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + + +# If your extensions are in another directory, add it here. If the directory +# is relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +sys.path.append(os.path.abspath('.')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- From d4a20232c06f2dc519e6665c1de5d735f85fd881 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:23:58 +0000 Subject: [PATCH 026/197] Forgot exceptions import --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 0baee5c8..ec7a129c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,7 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys, os, exceptions #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html From 3c8b0a030fe6f1aa2ef6bdc8bbd6c844613f1cbd Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:28:07 +0000 Subject: [PATCH 027/197] Adding extra mock... hopefully this won't carry on --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ec7a129c..ad577b94 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -26,6 +26,8 @@ class Mock(object): pass class Normalize(object): pass + class LinearSegmentedColormap(object): + pass def __init__(self, *args): """Mock init """ @@ -44,8 +46,7 @@ try: import GPy except exceptions.ImportError: MOCK_MODULES = ['matplotlib', 'pylab', 'matplotlib.colors', - 'matplotlib.cbook', 'pyfits', - 'numpy'] + 'matplotlib.cbook', 'pyfits', 'numpy'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From ba63b1a658876d508e53cc28b8268a7b408cb120 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:29:09 +0000 Subject: [PATCH 028/197] Removed matplotlib mock --- doc/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ad577b94..b7114395 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,8 +45,7 @@ class Mock(object): try: import GPy except exceptions.ImportError: - MOCK_MODULES = ['matplotlib', 'pylab', 'matplotlib.colors', - 'matplotlib.cbook', 'pyfits', 'numpy'] + MOCK_MODULES = ['pylab'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 9fec748be5870fc9c95e87c3d76086e329a98a57 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:31:43 +0000 Subject: [PATCH 029/197] Added some more mocks --- doc/conf.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index b7114395..65558ab4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -19,15 +19,15 @@ import sys, os, exceptions class Mock(object): """Special Healpix values for masked pixels. """ - pi = 3.141516 - class Axes(object): - pass - class Locator(object): - pass - class Normalize(object): - pass - class LinearSegmentedColormap(object): - pass + #pi = 3.141516 + #class Axes(object): + #pass + #class Locator(object): + #pass + #class Normalize(object): + #pass + #class LinearSegmentedColormap(object): + #pass def __init__(self, *args): """Mock init """ @@ -45,7 +45,11 @@ class Mock(object): try: import GPy except exceptions.ImportError: - MOCK_MODULES = ['pylab'] + MOCK_MODULES = ['matplotlib', 'pylab', 'matplotlib.colors', + 'matplotlib.cbook', 'pyfits', 'numpy', 'matplotlib', + 'matplotlib.cm', 'matplotlib.patches', 'matplotlib.projections', + 'matplotlib.projections.polar', 'matplotlib.pyplot', + 'matplotlib.text', 'matplotlib.ticker'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 23aa9be73c761c5203c70d16afa3c2273eb4246a Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:39:30 +0000 Subject: [PATCH 030/197] More attempts at mocking --- doc/conf.py | 66 ++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 65558ab4..3b57514d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,44 +15,42 @@ import sys, os, exceptions #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html -# To avoid problem with ReadTheDocs and compiled extensions. -class Mock(object): - """Special Healpix values for masked pixels. - """ - #pi = 3.141516 - #class Axes(object): - #pass - #class Locator(object): - #pass - #class Normalize(object): - #pass - #class LinearSegmentedColormap(object): - #pass - def __init__(self, *args): - """Mock init - """ - pass - - def __getattr__(self, name): - return Mock - - def __div__(self, x): - return Mock() - - def __getitem__(self, idx): - return str(Mock()) +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath( + os.path.join(os.pardir,os.pardir))) +# -- Mocking modules for Read the Docs compatibility --------------------------- try: - import GPy -except exceptions.ImportError: - MOCK_MODULES = ['matplotlib', 'pylab', 'matplotlib.colors', - 'matplotlib.cbook', 'pyfits', 'numpy', 'matplotlib', - 'matplotlib.cm', 'matplotlib.patches', 'matplotlib.projections', - 'matplotlib.projections.polar', 'matplotlib.pyplot', - 'matplotlib.text', 'matplotlib.ticker'] + import scipy + import numpy + import pylab +except ImportError: + from mock import MagicMock + MOCK_MODULES = ['numpy', 'quantities', 'scipy', 'scipy.spatial', + 'scipy.spatial.distance'] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() + sys.modules[mod_name] = MagicMock() + + # Needed for spykeutils.plot.Dialog.PlotDialog + #class QDialog: + #pass + #class PlotManager: + #pass + #sys.modules['PyQt4.QtGui'].QDialog = QDialog + #sys.modules['guiqwt.plot'].PlotManager = PlotManager + + ## Needed for spykeutils.plot.guiqwt_tools + #class CommandTool: + #pass + #class InteractiveTool: + #pass + #sys.modules['guiqwt.tools'].CommandTool = CommandTool + #sys.modules['guiqwt.tools'].InteractiveTool = InteractiveTool + +import Gpy # If your extensions are in another directory, add it here. If the directory From fe1c5431dfff3c45fb2349d41690f794eb52fbc5 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:43:33 +0000 Subject: [PATCH 031/197] More fixing... --- doc/conf.py | 5 ++--- setup.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 3b57514d..77d69ba7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -29,8 +29,7 @@ try: except ImportError: from mock import MagicMock - MOCK_MODULES = ['numpy', 'quantities', 'scipy', 'scipy.spatial', - 'scipy.spatial.distance'] + MOCK_MODULES = ['numpy', 'pylab', 'scipy'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = MagicMock() @@ -50,7 +49,7 @@ except ImportError: #sys.modules['guiqwt.tools'].CommandTool = CommandTool #sys.modules['guiqwt.tools'].InteractiveTool = InteractiveTool -import Gpy +import GPy # If your extensions are in another directory, add it here. If the directory diff --git a/setup.py b/setup.py index 432b8b13..aec5e4ed 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1', 'mock'], setup_requires=['sphinx'], cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From 57a79e0d7501c4446f850f0b24c6791d4ea0f865 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:48:32 +0000 Subject: [PATCH 032/197] Importing mock better --- doc/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 77d69ba7..a7a1bd98 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,7 +27,7 @@ try: import numpy import pylab except ImportError: - from mock import MagicMock + from unittest.mock import MagicMock MOCK_MODULES = ['numpy', 'pylab', 'scipy'] for mod_name in MOCK_MODULES: diff --git a/setup.py b/setup.py index aec5e4ed..432b8b13 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1', 'mock'], + install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx'], cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From 173375fec3ea4bb8a07a5b6854c1a1e62c1ae825 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:57:48 +0000 Subject: [PATCH 033/197] More --- doc/conf.py | 74 +++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a7a1bd98..13481238 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,39 +18,7 @@ import sys, os, exceptions # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath( - os.path.join(os.pardir,os.pardir))) - -# -- Mocking modules for Read the Docs compatibility --------------------------- -try: - import scipy - import numpy - import pylab -except ImportError: - from unittest.mock import MagicMock - - MOCK_MODULES = ['numpy', 'pylab', 'scipy'] - for mod_name in MOCK_MODULES: - sys.modules[mod_name] = MagicMock() - - # Needed for spykeutils.plot.Dialog.PlotDialog - #class QDialog: - #pass - #class PlotManager: - #pass - #sys.modules['PyQt4.QtGui'].QDialog = QDialog - #sys.modules['guiqwt.plot'].PlotManager = PlotManager - - ## Needed for spykeutils.plot.guiqwt_tools - #class CommandTool: - #pass - #class InteractiveTool: - #pass - #sys.modules['guiqwt.tools'].CommandTool = CommandTool - #sys.modules['guiqwt.tools'].InteractiveTool = InteractiveTool - -import GPy - +sys.path.insert(0, os.path.abspath('../')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it @@ -340,3 +308,43 @@ epub_copyright = u'2013, Author' # Allow duplicate toc entries. #epub_tocdup = True + +############################################################################# +# +# Include constructors in all the docs +# Got this method from: +# http://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc-to-document-a-classs-init-self-method +def skip(app, what, name, obj, skip, options): + if name == "__init__": + return False + return skip + +def setup(app): + app.connect("autodoc-skip-member", skip) + +############################################################################# +# +# Mock out imports with C dependencies because ReadTheDocs can't build them. +class Mock(object): + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return Mock() + + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() + +MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', + 'numpy', 'numpy.linalg', 'pylab' + ] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() From 70222278a1381db306b28cea6105cf034826e84a Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 11:59:46 +0000 Subject: [PATCH 034/197] More... --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 13481238..e777b63d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -322,6 +322,7 @@ def skip(app, what, name, obj, skip, options): def setup(app): app.connect("autodoc-skip-member", skip) + ############################################################################# # # Mock out imports with C dependencies because ReadTheDocs can't build them. @@ -344,7 +345,7 @@ class Mock(object): return Mock() MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', - 'numpy', 'numpy.linalg', 'pylab' + 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From ea0802d9388e0a9476c79045ffffefca4b0f00b3 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Thu, 31 Jan 2013 12:00:57 +0000 Subject: [PATCH 035/197] much tidying and breakage in the GP class --- GPy/inference/likelihoods.py | 9 +- GPy/models/GP.py | 189 ++++++++++++----------------------- 2 files changed, 72 insertions(+), 126 deletions(-) diff --git a/GPy/inference/likelihoods.py b/GPy/inference/likelihoods.py index acf1aa2d..4c8090f6 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/inference/likelihoods.py @@ -196,6 +196,9 @@ class gaussian(likelihood): Gaussian likelihood Y is expected to take values in (-inf,inf) """ + self.variance = variance + self._data = Y + self. def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -219,8 +222,8 @@ class gaussian(likelihood): if U is not None: pb.plot(U,np.ones(U.shape[0])*self.Y.min()*.8,'r|',mew=1.5,markersize=12) - def predictive_mean(self,mu,Sigma): - return mu - def _log_likelihood_gradients(): raise NotImplementedError + else: + var = var[:,None] * np.square(self._Ystd) + diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 8222fd6a..f5a0711d 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -8,23 +8,22 @@ from .. import kern from ..core import model from ..util.linalg import pdinv,mdot from ..util.plot import gpplot, Tango -from ..inference.EP import Full -from ..inference.likelihoods import likelihood,probit,poisson,gaussian +from ..inference.EP import Full # TODO: tidy +from ..inference import likelihoods class GP(model): """ Gaussian Process model for regression and EP :param X: input observations - :param Y: observed values :param kernel: a GPy kernel, defaults to rbf+white + :parm likelihood: a GPy likelihood :param normalize_X: whether to normalize the input data before computing (predictions will be in original scales) :type normalize_X: False|True :param normalize_Y: whether to normalize the input data before computing (predictions will be in original scales) :type normalize_Y: False|True :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) :rtype: model object - :parm likelihood: a GPy likelihood, defaults to gaussian :param epsilon_ep: convergence criterion for the Expectation Propagation algorithm, defaults to 0.1 :param powerep: power-EP parameters [$\eta$,$\delta$], defaults to [1.,1.] :type powerep: list @@ -32,23 +31,19 @@ class GP(model): .. Note:: Multiple independent outputs are allowed using columns of Y """ - #TODO: make beta parameter explicit #TODO: when using EP, predict needs to return 3 values otherwise it just needs 2. At the moment predict returns 3 values in any case. - def __init__(self,X,Y=None,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,likelihood=None,epsilon_ep=1e-3,power_ep=[1.,1.]): + def __init__(self, X, kernel, likelihood, normalize_X=False, Xslices=None): # parse arguments self.Xslices = Xslices self.X = X - self.N, self.Q = self.X.shape assert len(self.X.shape)==2 - if kernel is None: - kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) - else: - assert isinstance(kernel, kern.kern) + self.N, self.Q = self.X.shape + assert isinstance(kernel, kern.kern) self.kern = kernel - #here's some simple normalisation + #here's some simple normalisation for the inputs if normalize_X: self._Xmean = X.mean(0)[None,:] self._Xstd = X.std(0)[None,:] @@ -59,82 +54,48 @@ class GP(model): self._Xmean = np.zeros((1,self.X.shape[1])) self._Xstd = np.ones((1,self.X.shape[1])) - # Y - likelihood related variables, these might change whether using EP or not - if likelihood is None: - assert Y is not None, "Either Y or likelihood must be defined" - self.likelihood = gaussian(Y) - else: - self.likelihood = likelihood - assert len(self.likelihood.Y.shape)==2 + self.likelihood = likelihood + self.Y = self.likelihood.Y + self.YYT = self.likelihood.YYT # TODO: this is ugly. what about sufficient_stats? assert self.X.shape[0] == self.likelihood.Y.shape[0] self.N, self.D = self.likelihood.Y.shape - if isinstance(self.likelihood,gaussian): - self.EP = False - self.Y = Y - self.beta = 100.#FIXME beta should be an explicit parameter for this model - # Here's some simple normalisation - if normalize_Y: - self._Ymean = Y.mean(0)[None,:] - self._Ystd = Y.std(0)[None,:] - self.Y = (Y.copy()- self._Ymean) / self._Ystd - else: - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - - if self.D > self.N: - # then it's more efficient to store YYT - self.YYT = np.dot(self.Y, self.Y.T) - else: - self.YYT = None - else: - if self.D > 1: - raise NotImplementedError, "EP is not implemented for D > 1" - # Y is defined after approximating the likelihood - self.EP = True - self.eta,self.delta = power_ep - self.epsilon_ep = epsilon_ep - self.beta = np.ones([self.N,self.D]) - self.Z_ep = 0 - self.Y = None - self._Ymean = np.zeros((1,self.D)) - self._Ystd = np.ones((1,self.D)) - model.__init__(self) def _set_params(self,p): - # TODO: add beta when not using EP - self.kern._set_params_transformed(p) + self.kern._set_params_transformed(p[:self.kern.Nparam]) + self.likelihood._set_params(p[self.kern.Nparam:]) + self.K = self.kern.K(self.X,slices1=self.Xslices) - if self.EP: - self.K += np.diag(1./self.beta.flatten()) - #else: - # self.beta = p[-1] + self.K += np.diag(self.likelihood_variance) + self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) + #the gradient of the likelihood wrt the covariance matrix + if self.YYT is None: + self._alpha = np.dot(self.Ki,self.Y) + self._alpha2 = np.square(self._alpha) + self.dL_dK = 0.5*(np.dot(self._alpha,self._alpha.T)-self.D*self.Ki) + else: + tmp = mdot(self.Ki, self.YYT, self.Ki) + self._alpha2 = np.diag(tmp) + self.dL_dK = 0.5*(tmp - self.D*self.Ki) + def _get_params(self): - # TODO: add beta when not using EP - return self.kern._get_params_transformed() + return np.hstack((self.kern._get_params_transformed(), self.likelihood._get_params())) def _get_param_names(self): - # TODO: add beta when not using EP - return self.kern._get_param_names_transformed() + return self.kern._get_param_names_transformed() + self.likelihood._get_param_names() - def approximate_likelihood(self): + def update_likelihood_approximation(self): """ Approximates a non-gaussian likelihood using Expectation Propagation + + For a Gaussian (or direct: TODO) likelihood, no iteration is required: + this function does nothing """ - assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - self.ep_approx = Full(self.K,self.likelihood,epsilon = self.epsilon_ep,power_ep=[self.eta,self.delta]) - self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() - if self.D > self.N: - # then it's more efficient to store YYT - self.YYT = np.dot(self.Y, self.Y.T) - else: - self.YYT = None - # Kernel plus noise variance term - self.K = self.kern.K(self.X,slices1=self.Xslices) + np.diag(1./self.beta.flatten()) - self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) + self.likelihood.fit(self.K) + self.Y, self.YYT, self.likelihood_variance, self.likelihood_Z = self.likelihood.sufficient_stats() # TODO: just store these in the likelihood? def _model_fit_term(self): """ @@ -147,29 +108,41 @@ class GP(model): def log_likelihood(self): """ - The log marginal likelihood for an EP model can be written as the log likelihood of - a regression model for a new variable Y* = v_tilde/tau_tilde, with a covariance + The log marginal likelihood of the GP. + + For an EP model, can be written as the log likelihood of a regression + model for a new variable Y* = v_tilde/tau_tilde, with a covariance matrix K* = K + diag(1./tau_tilde) plus a normalization term. """ - L = -0.5*selff.D*self.K_logdet + self.model_fit_term() - if self.EP: - L += self.normalisation_term() - return L + return -0.5*self.D*self.K_logdet + self.model_fit_term() + self.likelihood.Z - def log_likelihood(self): - complexity_term = -0.5*self.N*self.D*np.log(2.*np.pi) - 0.5*self.D*self.K_logdet - return complexity_term + self._model_fit_term() - - def dL_dK(self): - if self.YYT is None: - alpha = np.dot(self.Ki,self.Y) - dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) - else: - dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) - return dL_dK def _log_likelihood_gradients(self): - return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) + """ + The gradient of all parameters. + + For the kernel parameters, use the chain rule via dL_dK + + For the likelihood parameters, pass in alpha = K^-1 y + """ + return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X), self.likelihood._gradients(self.alpha2))) + + def _raw_predict(self,_Xnew,slices, full_cov=False): + """ + Internal helper function for making predictions, does not account + for normalisation or likelihood + """ + Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) + mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) + KiKx = np.dot(self.Ki,Kx) + if full_cov: + Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) + var = Kxx - np.dot(KiKx.T,Kx) + else: + Kxx = self.kern.Kdiag(_Xnew, slices=slices) + var = Kxx - np.sum(np.multiply(KiKx,Kx),0) + return mu, var + def predict(self,Xnew, slices=None, full_cov=False): """ @@ -198,41 +171,11 @@ class GP(model): """ #normalise X values Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var, phi = self._raw_predict(Xnew, slices, full_cov) + mu, var, phi = self._raw_predict(Xnew, slices, full_cov=full_cov) - #un-normalise - mu = mu*self._Ystd + self._Ymean - if full_cov: - if self.D==1: - var *= np.square(self._Ystd) - else: - var = var[:,:,None] * np.square(self._Ystd) - else: - if self.D==1: - var *= np.square(np.squeeze(self._Ystd)) - else: - var = var[:,None] * np.square(self._Ystd) + #now push through likelihood TODO - return mu,var,phi - - def _raw_predict(self,_Xnew,slices, full_cov=False): - """Internal helper function for making predictions, does not account for normalisation""" - Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) - mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) - KiKx = np.dot(self.Ki,Kx) - if full_cov: - Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) - var = Kxx - np.dot(KiKx.T,Kx) - if self.EP: - raise NotImplementedError, "full_cov = True not implemented for EP" - #var = np.diag(var)[:,None] - #phi = self.likelihood.predictive_mean(mu,var) - else: - Kxx = self.kern.Kdiag(_Xnew, slices=slices) - var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - if self.EP: - phi = self.likelihood.predictive_mean(mu,var) - return mu, var, phi + return mean, _5pc, _95pc def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): """ From 55ecc4306d7dead1ea2a6520dff72b61a651af7d Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:06:58 +0000 Subject: [PATCH 036/197] Remove matplotlib mock? --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index e777b63d..a5e7a5f3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -344,7 +344,7 @@ class Mock(object): else: return Mock() -MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', +MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: From 32e61ce1e8fc253b66f721cf3c9d7b21680f0146 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:09:59 +0000 Subject: [PATCH 037/197] Adding requirements file? --- doc/doc-requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/doc-requirements.txt diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt new file mode 100644 index 00000000..9f8ad0d0 --- /dev/null +++ b/doc/doc-requirements.txt @@ -0,0 +1,4 @@ +matplotlib +numpy +pylab +scipy From 98c06c1e7b51e3588e12233d5e70cfcb9aa38bcd Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:19:12 +0000 Subject: [PATCH 038/197] conf edit --- doc/conf.py | 8 ++++---- doc/doc-requirements.txt | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 doc/doc-requirements.txt diff --git a/doc/conf.py b/doc/conf.py index a5e7a5f3..a6827fcf 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ import sys, os, exceptions # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath('..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it @@ -45,9 +45,9 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: sys.path.append("../GPy") os.system("pwd") - os.system("cd ..") - os.system("sphinx-apidoc -f -o ./docs ./GPy") - os.system("cd ./docs") + #os.system("cd ..") + os.system("sphinx-apidoc -f -o . ../GPy") + #os.system("cd ./docs") # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt deleted file mode 100644 index 9f8ad0d0..00000000 --- a/doc/doc-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -matplotlib -numpy -pylab -scipy From 86f745f66ddf3a052846f8177e325e3b145dd800 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:20:44 +0000 Subject: [PATCH 039/197] more --- doc/conf.py | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a6827fcf..e46e6bb6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,7 +43,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: - sys.path.append("../GPy") + #sys.path.append("../GPy") os.system("pwd") #os.system("cd ..") os.system("sphinx-apidoc -f -o . ../GPy") @@ -314,38 +314,38 @@ epub_copyright = u'2013, Author' # Include constructors in all the docs # Got this method from: # http://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc-to-document-a-classs-init-self-method -def skip(app, what, name, obj, skip, options): - if name == "__init__": - return False - return skip +#def skip(app, what, name, obj, skip, options): + #if name == "__init__": + #return False + #return skip -def setup(app): - app.connect("autodoc-skip-member", skip) +#def setup(app): + #app.connect("autodoc-skip-member", skip) -############################################################################# -# -# Mock out imports with C dependencies because ReadTheDocs can't build them. -class Mock(object): - def __init__(self, *args, **kwargs): - pass +############################################################################## +## +## Mock out imports with C dependencies because ReadTheDocs can't build them. +#class Mock(object): + #def __init__(self, *args, **kwargs): + #pass - def __call__(self, *args, **kwargs): - return Mock() + #def __call__(self, *args, **kwargs): + #return Mock() - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() + #@classmethod + #def __getattr__(cls, name): + #if name in ('__file__', '__path__'): + #return '/dev/null' + #elif name[0] == name[0].upper(): + #mockType = type(name, (), {}) + #mockType.__module__ = __name__ + #return mockType + #else: + #return Mock() -MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', - 'pylab' - ] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() +#MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', + #'pylab' + #] +#for mod_name in MOCK_MODULES: + #sys.modules[mod_name] = Mock() From 5c8aebdb596b63317ef59cb8bd0aac92f1ce9c38 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:21:31 +0000 Subject: [PATCH 040/197] more --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index e46e6bb6..df2cae60 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ import sys, os, exceptions # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +#sys.path.insert(0, os.path.abspath('..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it From d63cd4ad7e0140112885d1ae1f7299383577455d Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:25:26 +0000 Subject: [PATCH 041/197] Back to the beginning? --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index df2cae60..ee99e210 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -23,12 +23,12 @@ import sys, os, exceptions # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -sys.path.append(os.path.abspath('.')) +#sys.path.append(os.path.abspath('.')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) +#sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- @@ -43,7 +43,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: - #sys.path.append("../GPy") + sys.path.append("../GPy") os.system("pwd") #os.system("cd ..") os.system("sphinx-apidoc -f -o . ../GPy") From 9758c90b164331a33152893a53385e962d7bb976 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:27:27 +0000 Subject: [PATCH 042/197] Add just pylab mock back --- doc/conf.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ee99e210..3ca2f6f1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -326,26 +326,26 @@ epub_copyright = u'2013, Author' ############################################################################## ## ## Mock out imports with C dependencies because ReadTheDocs can't build them. -#class Mock(object): - #def __init__(self, *args, **kwargs): - #pass +class Mock(object): + def __init__(self, *args, **kwargs): + pass - #def __call__(self, *args, **kwargs): - #return Mock() + def __call__(self, *args, **kwargs): + return Mock() - #@classmethod - #def __getattr__(cls, name): - #if name in ('__file__', '__path__'): - #return '/dev/null' - #elif name[0] == name[0].upper(): - #mockType = type(name, (), {}) - #mockType.__module__ = __name__ - #return mockType - #else: - #return Mock() + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() -#MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', - #'pylab' - #] -#for mod_name in MOCK_MODULES: - #sys.modules[mod_name] = Mock() +MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', + 'pylab' + ] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() From a6851cf63d8bdb2defcd9de025f74506141ab7a9 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Thu, 31 Jan 2013 12:28:52 +0000 Subject: [PATCH 043/197] massive restructuting to make the EP likelihoods work consistently --- GPy/{inference => likelihoods}/EP.py | 116 ++++++++---------- .../likelihood_functions.py} | 37 +++--- 2 files changed, 67 insertions(+), 86 deletions(-) rename GPy/{inference => likelihoods}/EP.py (76%) rename GPy/{inference/likelihoods.py => likelihoods/likelihood_functions.py} (91%) diff --git a/GPy/inference/EP.py b/GPy/likelihoods/EP.py similarity index 76% rename from GPy/inference/EP.py rename to GPy/likelihoods/EP.py index c3aad7c1..1519bf3b 100644 --- a/GPy/inference/EP.py +++ b/GPy/likelihoods/EP.py @@ -9,7 +9,7 @@ from ..util.plot import gpplot from .. import kern class EP: - def __init__(self,covariance,likelihood,Kmn=None,Knn_diag=None,epsilon=1e-3,power_ep=[1.,1.]): + def __init__(self,data,likelihood_function,epsilon=1e-3,power_ep=[1.,1.]): """ Expectation Propagation @@ -22,24 +22,10 @@ class EP: power_ep : Power-EP parameters (eta,delta) - 2x1 numpy array (floats) epsilon : Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) """ - self.likelihood = likelihood - assert covariance.shape[0] == covariance.shape[1] - if Kmn is not None: - self.Kmm = covariance - self.Kmn = Kmn - self.M = self.Kmn.shape[0] - self.N = self.Kmn.shape[1] - assert self.M < self.N, 'The number of inducing inputs must be smaller than the number of observations' - else: - self.K = covariance - self.N = self.K.shape[0] - if Knn_diag is not None: - self.Knn_diag = Knn_diag - assert len(Knn_diag) == self.N, 'Knn_diagonal has size different from N' - + self.likelihood_function = likelihood_function self.epsilon = epsilon self.eta, self.delta = power_ep - self.jitter = 1e-12 + self.jitter = 1e-12 # TODO: is this needed? """ Initial values - Likelihood approximation parameters: @@ -54,10 +40,9 @@ class EP: sigma_sum = 1./self.tau_ + 1./self.tau_tilde mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant - return self.tau_tilde[:,None], mu_tilde[:,None], Z_ep + self.Y, self.beta, self.Z = self.tau_tilde[:,None], mu_tilde[:,None], Z_ep -class Full(EP): - def fit_EP(self): + def fit_full(self,K): """ The expectation-propagation algorithm. For nomenclature see Rasmussen & Williams 2006. @@ -66,8 +51,8 @@ class Full(EP): #self.K = self.kernel.K(self.X,self.X) #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) - self.mu=np.zeros(self.N) - self.Sigma=self.K.copy() + self.mu = np.zeros(self.N) + self.Sigma = K.copy() """ Initial values - Cavity distribution parameters: @@ -111,11 +96,11 @@ class Full(EP): self.mu = np.dot(self.Sigma,self.v_tilde) self.iterations += 1 #Sigma recomptutation with Cholesky decompositon - Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*(self.K) + Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*K B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K L = jitchol(B) V,info = linalg.flapack.dtrtrs(L,Sroot_tilde_K,lower=1) - self.Sigma = self.K - np.dot(V.T,V) + self.Sigma = K - np.dot(V.T,V) self.mu = np.dot(self.Sigma,self.v_tilde) epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N @@ -124,32 +109,33 @@ class Full(EP): return self._compute_GP_variables() -class DTC(EP): - def fit_EP(self): + def fit_DTC(self, Knn_diag, Kmn, Kmm): """ The expectation-propagation algorithm with sparse pseudo-input. For nomenclature see ... 2013. """ + #TODO: this doesn;t work with uncertain inputs! + """ Prior approximation parameters: q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) Sigma0 = Qnn = Knm*Kmmi*Kmn """ - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.KmnKnm = np.dot(self.Kmn, self.Kmn.T) - self.KmmiKmn = np.dot(self.Kmmi,self.Kmn) - self.Qnn_diag = np.sum(self.Kmn*self.KmmiKmn,-2) - self.LLT0 = self.Kmm.copy() + Kmmi, Lm, Lmi, Kmm_logdet = pdinv(Kmm) + KmnKnm = np.dot(Kmn, Kmn.T) + KmmiKmn = np.dot(Kmmi,self.Kmn) + Qnn_diag = np.sum(Kmn*KmmiKmn,-2) + LLT0 = Kmm.copy() """ Posterior approximation: q(f|y) = N(f| mu, Sigma) Sigma = Diag + P*R.T*R*P.T + K mu = w + P*gamma """ - self.mu = np.zeros(self.N) - self.LLT = self.Kmm.copy() - self.Sigma_diag = self.Qnn_diag.copy() + mu = np.zeros(self.N) + LLT = Kmm.copy() + Sigma_diag = Qnn_diag.copy() """ Initial values - Cavity distribution parameters: @@ -157,12 +143,12 @@ class DTC(EP): sigma_ = 1./tau_ mu_ = v_/tau_ """ - self.tau_ = np.empty(self.N,dtype=float) - self.v_ = np.empty(self.N,dtype=float) + tau_ = np.empty(self.N,dtype=float) + v_ = np.empty(self.N,dtype=float) #Initial values - Marginal moments z = np.empty(self.N,dtype=float) - self.Z_hat = np.empty(self.N,dtype=float) + Z_hat = np.empty(self.N,dtype=float) phi = np.empty(self.N,dtype=float) mu_hat = np.empty(self.N,dtype=float) sigma2_hat = np.empty(self.N,dtype=float) @@ -171,47 +157,45 @@ class DTC(EP): epsilon_np1 = 1 epsilon_np2 = 1 self.iterations = 0 - self.np1 = [self.tau_tilde.copy()] - self.np2 = [self.v_tilde.copy()] + np1 = [tau_tilde.copy()] + np2 = [v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) + update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters - self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] - self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] + tau_[i] = 1./Sigma_diag[i] - self.eta*tau_tilde[i] + v_[i] = mu[i]/Sigma_diag[i] - self.eta*v_tilde[i] #Marginal moments - self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood_function.moments_match(self.data[i],tau_[i],v_[i]) #Site parameters update - Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) - Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) - self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau - self.v_tilde[i] = self.v_tilde[i] + Delta_v + Delta_tau = delta/self.eta*(1./sigma2_hat[i] - 1./Sigma_diag[i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - mu[i]/Sigma_diag[i]) + tau_tilde[i] = tau_tilde[i] + Delta_tau + v_tilde[i] = v_tilde[i] + Delta_v #Posterior distribution parameters update - self.LLT = self.LLT + np.outer(self.Kmn[:,i],self.Kmn[:,i])*Delta_tau - L = jitchol(self.LLT) - V,info = linalg.flapack.dtrtrs(L,self.Kmn,lower=1) - self.Sigma_diag = np.sum(V*V,-2) + LLT = LLT + np.outer(Kmn[:,i],Kmn[:,i])*Delta_tau + L = jitchol(LLT) + V,info = linalg.flapack.dtrtrs(L,Kmn,lower=1) + Sigma_diag = np.sum(V*V,-2) si = np.sum(V.T*V[:,i],-1) - self.mu = self.mu + (Delta_v-Delta_tau*self.mu[i])*si + mu = mu + (Delta_v-Delta_tau*mu[i])*si self.iterations += 1 #Sigma recomputation with Cholesky decompositon - self.LLT0 = self.LLT0 + np.dot(self.Kmn*self.tau_tilde[None,:],self.Kmn.T) - self.L = jitchol(self.LLT) - V,info = linalg.flapack.dtrtrs(L,self.Kmn,lower=1) + LLT0 = LLT0 + np.dot(Kmn*tau_tilde[None,:],Kmn.T) + L = jitchol(LLT) + V,info = linalg.flapack.dtrtrs(L,Kmn,lower=1) V2,info = linalg.flapack.dtrtrs(L.T,V,lower=0) - self.Sigma_diag = np.sum(V*V,-2) - Knmv_tilde = np.dot(self.Kmn,self.v_tilde) - self.mu = np.dot(V2.T,Knmv_tilde) - epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N - epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N - self.np1.append(self.tau_tilde.copy()) - self.np2.append(self.v_tilde.copy()) + Sigma_diag = np.sum(V*V,-2) + Knmv_tilde = np.dot(Kmn,v_tilde) + mu = np.dot(V2.T,Knmv_tilde) + epsilon_np1 = sum((tau_tilde-np1[-1])**2)/self.N + epsilon_np2 = sum((v_tilde-np2[-1])**2)/self.N + np1.append(tau_tilde.copy()) + np2.append(v_tilde.copy()) - return self._compute_GP_variables() + self._compute_GP_variables() -class FITC(EP): - def fit_EP(self): + def fit_FITC(self, Knn_diag, Kmn): """ The expectation-propagation algorithm with sparse pseudo-input. For nomenclature see Naish-Guzman and Holden, 2008. diff --git a/GPy/inference/likelihoods.py b/GPy/likelihoods/likelihood_functions.py similarity index 91% rename from GPy/inference/likelihoods.py rename to GPy/likelihoods/likelihood_functions.py index 4c8090f6..1387c53d 100644 --- a/GPy/inference/likelihoods.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Copyright (c) 2012, 2013 Ricardo Andrade # Licensed under the BSD 3-clause license (see LICENSE.txt) @@ -15,9 +15,7 @@ class likelihood: :param Y: observed output (Nx1 numpy.darray) ..Note:: Y values allowed depend on the likelihood used """ - def __init__(self,Y,location=0,scale=1): - self.Y = Y - self.N = self.Y.shape[0] + def __init__(self,location=0,scale=1): self.location = location self.scale = scale @@ -59,11 +57,10 @@ class probit(likelihood): L(x) = \\Phi (Y_i*f_i) $$ """ - def __init__(self,Y,location=0,scale=1): - assert np.sum(np.abs(Y)-1) == 0, "Output values must be either -1 or 1" + def __init__(self,location=0,scale=1): likelihood.__init__(self,Y,location,scale) - def moments_match(self,i,tau_i,v_i): + def moments_match(self,data_i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -71,10 +68,11 @@ class probit(likelihood): :param tau_i: precision of the cavity distribution (float) :param v_i: mean/variance of the cavity distribution (float) """ - z = self.Y[i]*v_i/np.sqrt(tau_i**2 + tau_i) + # TODO: some version of assert np.sum(np.abs(Y)-1) == 0, "Output values must be either -1 or 1" + z = data_i*v_i/np.sqrt(tau_i**2 + tau_i) Z_hat = stats.norm.cdf(z) phi = stats.norm.pdf(z) - mu_hat = v_i/tau_i + self.Y[i]*phi/(Z_hat*np.sqrt(tau_i**2 + tau_i)) + mu_hat = v_i/tau_i + data_i*phi/(Z_hat*np.sqrt(tau_i**2 + tau_i)) sigma2_hat = 1./tau_i - (phi/((tau_i**2+tau_i)*Z_hat))*(z+phi/Z_hat) return Z_hat, mu_hat, sigma2_hat @@ -83,14 +81,16 @@ class probit(likelihood): var = var.flatten() return stats.norm.cdf(mu/np.sqrt(1+var)) - def predictive_var(self,mu,var): - p=self.predictive_mean(mu,var) - return p*(1-p) + def predictive_quantiles(self,mu,var): + #p=self.predictive_mean(mu,var) + #return p*(1-p) + raise NotImplementedError #TODO def _log_likelihood_gradients(): - raise NotImplementedError + return np.zeros(0) # there are no parameters of whcih to compute the gradients def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): + #TODO: remove me assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' phi_var = self.predictive_var(mu,var) gpplot(X,phi,phi_var) @@ -192,13 +192,10 @@ class poisson(likelihood): pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) class gaussian(likelihood): - """ - Gaussian likelihood - Y is expected to take values in (-inf,inf) - """ - self.variance = variance - self._data = Y - self. + """ + Gaussian likelihood + Y is expected to take values in (-inf,inf) + """ def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm From f208b49c3661815494dad73e5e465b5837e62640 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:30:38 +0000 Subject: [PATCH 044/197] Added matplotlib to mock --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 3ca2f6f1..34464d94 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -344,7 +344,7 @@ class Mock(object): else: return Mock() -MOCK_MODULES = [#'matplotlib', 'matplotlib.pyplot', +MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: From b73245e1de2605936064cd4dd62aa9ec39b078ca Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:33:34 +0000 Subject: [PATCH 045/197] Moved mock to top --- doc/conf.py | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 34464d94..3082f8c3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,6 +15,31 @@ import sys, os, exceptions #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html +############################################################################## +## +## Mock out imports with C dependencies because ReadTheDocs can't build them. +class Mock(object): + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return Mock() + + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() + +MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', 'pylab' ] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -323,29 +348,3 @@ epub_copyright = u'2013, Author' #app.connect("autodoc-skip-member", skip) -############################################################################## -## -## Mock out imports with C dependencies because ReadTheDocs can't build them. -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - -MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', - 'pylab' - ] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() From c6339116a0090fa31459578fdeb8809e6611c5af Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:35:39 +0000 Subject: [PATCH 046/197] Added matplotlib color --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 3082f8c3..fe70071b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -36,7 +36,7 @@ class Mock(object): else: return Mock() -MOCK_MODULES = ['matplotlib', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From d5bafc9bb92bed9e12803a61fc3288b0f5237629 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:55:30 +0000 Subject: [PATCH 047/197] Testing differen mock --- doc/conf.py | 102 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index fe70071b..a49a0c53 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,34 +11,108 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os, exceptions +import sys, os #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html -############################################################################## -## -## Mock out imports with C dependencies because ReadTheDocs can't build them. class Mock(object): + __all__ = [] def __init__(self, *args, **kwargs): - pass + for key, value in kwargs.iteritems(): + setattr(self, key, value) def __call__(self, *args, **kwargs): return Mock() - @classmethod - def __getattr__(cls, name): + __add__ = __mul__ = __getitem__ = __setitem__ = \ +__delitem__ = __sub__ = __floordiv__ = __mod__ = __divmod__ = \ +__pow__ = __lshift__ = __rshift__ = __and__ = __xor__ = __or__ = \ +__rmul__ = __rsub__ = __rfloordiv__ = __rmod__ = __rdivmod__ = \ +__rpow__ = __rlshift__ = __rrshift__ = __rand__ = __rxor__ = __ror__ = \ +__imul__ = __isub__ = __ifloordiv__ = __imod__ = __idivmod__ = \ +__ipow__ = __ilshift__ = __irshift__ = __iand__ = __ixor__ = __ior__ = \ +__neg__ = __pos__ = __abs__ = __invert__ = __call__ + + def __getattr__(self, name): if name in ('__file__', '__path__'): return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType + if name == 'sqrt': + return math.sqrt + elif name[0] != '_' and name[0] == name[0].upper(): + return type(name, (), {}) else: - return Mock() + return Mock(**vars(self)) -MOCK_MODULES = ['matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] + def __lt__(self, *args, **kwargs): + return True + + __nonzero__ = __le__ = __eq__ = __ne__ = __gt__ = __ge__ = __contains__ = \ +__lt__ + + + def __repr__(self): + # Use _mock_repr to fake the __repr__ call + res = getattr(self, "_mock_repr") + return res if isinstance(res, str) else "Mock" + + def __hash__(self): + return 1 + + __len__ = __int__ = __long__ = __index__ = __hash__ + + def __oct__(self): + return '01' + + def __hex__(self): + return '0x1' + + def __float__(self): + return 0.1 + + def __complex__(self): + return 1j + + +MOCK_MODULES = [ + 'pylab', 'scipy', 'matplotlib', 'matplotlib.pyplot', 'pyfits', + 'scipy.constants.constants', 'matplotlib.cm', + 'matplotlib.image', 'matplotlib.colors', 'sunpy.cm', + 'pandas', 'pandas.io', 'pandas.io.parsers', + 'suds', 'matplotlib.ticker', 'matplotlib.colorbar', + 'matplotlib.dates', 'scipy.optimize', 'scipy.ndimage', + 'matplotlib.figure', 'scipy.ndimage.interpolation', 'bs4'] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() + sys.modules[mod_name] = Mock(pi=math.pi, G=6.67364e-11) + +sys.modules['numpy'] = Mock(pi=math.pi, G=6.67364e-11, + ndarray=type('ndarray', (), {}), + dtype=lambda _: Mock(_mock_repr='np.dtype(\'float32\')')) +sys.modules['scipy.constants'] = Mock(pi=math.pi, G=6.67364e-11) + +############################################################################## +## +## Mock out imports with C dependencies because ReadTheDocs can't build them. +#class Mock(object): + #def __init__(self, *args, **kwargs): + #pass + + #def __call__(self, *args, **kwargs): + #return Mock() + + #@classmethod + #def __getattr__(cls, name): + #if name in ('__file__', '__path__'): + #return '/dev/null' + #elif name[0] == name[0].upper(): + #mockType = type(name, (), {}) + #mockType.__module__ = __name__ + #return mockType + #else: + #return Mock() + +#MOCK_MODULES = ['pylab']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +#for mod_name in MOCK_MODULES: + #sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From a8120bd0157b68a63886049f8a9e430545bd91af Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 12:56:33 +0000 Subject: [PATCH 048/197] Testing differen mock --- doc/conf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a49a0c53..a24df189 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -82,12 +82,13 @@ MOCK_MODULES = [ 'matplotlib.dates', 'scipy.optimize', 'scipy.ndimage', 'matplotlib.figure', 'scipy.ndimage.interpolation', 'bs4'] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock(pi=math.pi, G=6.67364e-11) + sys.modules[mod_name] = Mock() -sys.modules['numpy'] = Mock(pi=math.pi, G=6.67364e-11, - ndarray=type('ndarray', (), {}), - dtype=lambda _: Mock(_mock_repr='np.dtype(\'float32\')')) -sys.modules['scipy.constants'] = Mock(pi=math.pi, G=6.67364e-11) + +#sys.modules['numpy'] = Mock(pi=math.pi, G=6.67364e-11, + #ndarray=type('ndarray', (), {}), + #dtype=lambda _: Mock(_mock_repr='np.dtype(\'float32\')')) +#sys.modules['scipy.constants'] = Mock(pi=math.pi, G=6.67364e-11) ############################################################################## ## From 021c429720f80925187b5cf0ab2bcdd8baa7b7d1 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 13:01:56 +0000 Subject: [PATCH 049/197] More --- doc/conf.py | 114 ++++++++++++++++++++++++++-------------------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a24df189..d32c77f7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,74 +15,74 @@ import sys, os #Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html -class Mock(object): - __all__ = [] - def __init__(self, *args, **kwargs): - for key, value in kwargs.iteritems(): - setattr(self, key, value) +#class Mock(object): + #__all__ = [] + #def __init__(self, *args, **kwargs): + #for key, value in kwargs.iteritems(): + #setattr(self, key, value) - def __call__(self, *args, **kwargs): - return Mock() + #def __call__(self, *args, **kwargs): + #return Mock() - __add__ = __mul__ = __getitem__ = __setitem__ = \ -__delitem__ = __sub__ = __floordiv__ = __mod__ = __divmod__ = \ -__pow__ = __lshift__ = __rshift__ = __and__ = __xor__ = __or__ = \ -__rmul__ = __rsub__ = __rfloordiv__ = __rmod__ = __rdivmod__ = \ -__rpow__ = __rlshift__ = __rrshift__ = __rand__ = __rxor__ = __ror__ = \ -__imul__ = __isub__ = __ifloordiv__ = __imod__ = __idivmod__ = \ -__ipow__ = __ilshift__ = __irshift__ = __iand__ = __ixor__ = __ior__ = \ -__neg__ = __pos__ = __abs__ = __invert__ = __call__ + #__add__ = __mul__ = __getitem__ = __setitem__ = \ +#__delitem__ = __sub__ = __floordiv__ = __mod__ = __divmod__ = \ +#__pow__ = __lshift__ = __rshift__ = __and__ = __xor__ = __or__ = \ +#__rmul__ = __rsub__ = __rfloordiv__ = __rmod__ = __rdivmod__ = \ +#__rpow__ = __rlshift__ = __rrshift__ = __rand__ = __rxor__ = __ror__ = \ +#__imul__ = __isub__ = __ifloordiv__ = __imod__ = __idivmod__ = \ +#__ipow__ = __ilshift__ = __irshift__ = __iand__ = __ixor__ = __ior__ = \ +#__neg__ = __pos__ = __abs__ = __invert__ = __call__ - def __getattr__(self, name): - if name in ('__file__', '__path__'): - return '/dev/null' - if name == 'sqrt': - return math.sqrt - elif name[0] != '_' and name[0] == name[0].upper(): - return type(name, (), {}) - else: - return Mock(**vars(self)) + #def __getattr__(self, name): + #if name in ('__file__', '__path__'): + #return '/dev/null' + #if name == 'sqrt': + #return math.sqrt + #elif name[0] != '_' and name[0] == name[0].upper(): + #return type(name, (), {}) + #else: + #return Mock(**vars(self)) - def __lt__(self, *args, **kwargs): - return True + #def __lt__(self, *args, **kwargs): + #return True - __nonzero__ = __le__ = __eq__ = __ne__ = __gt__ = __ge__ = __contains__ = \ -__lt__ + #__nonzero__ = __le__ = __eq__ = __ne__ = __gt__ = __ge__ = __contains__ = \ +#__lt__ - def __repr__(self): - # Use _mock_repr to fake the __repr__ call - res = getattr(self, "_mock_repr") - return res if isinstance(res, str) else "Mock" + #def __repr__(self): + ## Use _mock_repr to fake the __repr__ call + #res = getattr(self, "_mock_repr") + #return res if isinstance(res, str) else "Mock" - def __hash__(self): - return 1 + #def __hash__(self): + #return 1 - __len__ = __int__ = __long__ = __index__ = __hash__ + #__len__ = __int__ = __long__ = __index__ = __hash__ - def __oct__(self): - return '01' + #def __oct__(self): + #return '01' - def __hex__(self): - return '0x1' + #def __hex__(self): + #return '0x1' - def __float__(self): - return 0.1 + #def __float__(self): + #return 0.1 - def __complex__(self): - return 1j + #def __complex__(self): + #return 1j -MOCK_MODULES = [ - 'pylab', 'scipy', 'matplotlib', 'matplotlib.pyplot', 'pyfits', - 'scipy.constants.constants', 'matplotlib.cm', - 'matplotlib.image', 'matplotlib.colors', 'sunpy.cm', - 'pandas', 'pandas.io', 'pandas.io.parsers', - 'suds', 'matplotlib.ticker', 'matplotlib.colorbar', - 'matplotlib.dates', 'scipy.optimize', 'scipy.ndimage', - 'matplotlib.figure', 'scipy.ndimage.interpolation', 'bs4'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() +#MOCK_MODULES = [ + #'pylab', 'scipy', 'matplotlib', 'matplotlib.pyplot', 'pyfits', + #'scipy.constants.constants', 'matplotlib.cm', + #'matplotlib.image', 'matplotlib.colors', 'sunpy.cm', + #'pandas', 'pandas.io', 'pandas.io.parsers', + #'suds', 'matplotlib.ticker', 'matplotlib.colorbar', + #'matplotlib.dates', 'scipy.optimize', 'scipy.ndimage', + #'matplotlib.figure', 'scipy.ndimage.interpolation', 'bs4'] +#for mod_name in MOCK_MODULES: + #sys.modules[mod_name] = Mock() #sys.modules['numpy'] = Mock(pi=math.pi, G=6.67364e-11, @@ -111,9 +111,11 @@ for mod_name in MOCK_MODULES: #else: #return Mock() -#MOCK_MODULES = ['pylab']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] -#for mod_name in MOCK_MODULES: - #sys.modules[mod_name] = Mock() +import mock + +MOCK_MODULES = ['pylab', 'matplotlib']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = mock.Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From bdc89170d423591f78893da199b7c33fc255e55f Mon Sep 17 00:00:00 2001 From: James Hensman Date: Thu, 31 Jan 2013 13:34:30 +0000 Subject: [PATCH 050/197] added a Gaussian likelihood class --- GPy/likelihoods/Gaussian.py | 16 ++++++++++++++++ GPy/likelihoods/__init__.py | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 GPy/likelihoods/Gaussian.py create mode 100644 GPy/likelihoods/__init__.py diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py new file mode 100644 index 00000000..2397ce38 --- /dev/null +++ b/GPy/likelihoods/Gaussian.py @@ -0,0 +1,16 @@ +import numpy as np + +class Gaussian: + def __init__(self,data,variance=1.,normalise=False): + self.data = data + if normalise: + foo + self._variance = variance + def _get_params(self): + return np.asarray(self.variance) + def _set_params(self,x): + self._variance = x + def fit(self): + pass + def _gradients(self,foo): + return bar(foo) diff --git a/GPy/likelihoods/__init__.py b/GPy/likelihoods/__init__.py new file mode 100644 index 00000000..d1369c43 --- /dev/null +++ b/GPy/likelihoods/__init__.py @@ -0,0 +1,3 @@ +from EP import EP +from Gaussian import Gaussian +# TODO: from Laplace import Laplace From 43261b601f16f0d6ea4184badd3216c7efe5652a Mon Sep 17 00:00:00 2001 From: Nicolo Fusi Date: Thu, 31 Jan 2013 13:38:24 +0000 Subject: [PATCH 051/197] making travis-ci work again --- .travis.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index af20bec4..5c654d37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: python python: - "2.7" + +#Set virtual env with system-site-packages to true +virtualenv: + system_site_packages: true + # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: - - sudo apt-get install python-scipy - - pip install sphinx - - pip install . --use-mirrors -# command to run tests, e.g. python setup.py test -script: - - nosetests --with-xcoverage --with-xunit --cover-package=GPy --cover-erase GPy/testing +before_install: + - sudo apt-get install -qq python-scipy python-pip + - sudo apt-get install -qq python-matplotlib \ No newline at end of file From 5867bf0f672c415795bd05f8a1b13f347e4477ca Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 13:39:11 +0000 Subject: [PATCH 052/197] Added mock to dependencies for docs --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 432b8b13..ebe31175 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + install_requires=['mock', 'sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx'], cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From 169f5fead9df60189cbe149bd3122732df44fb03 Mon Sep 17 00:00:00 2001 From: Nicolo Fusi Date: Thu, 31 Jan 2013 13:41:06 +0000 Subject: [PATCH 053/197] useless commit to get travis-ci started --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c69352b8..0b5d00ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ GPy === -A Gaussian processes framework in python. +A Gaussian processes framework in python * [Online documentation](https://gpy.readthedocs.org/en/latest/) * [Unit tests (Travis-CI)](https://travis-ci.org/SheffieldML/GPy) \ No newline at end of file From 4998317b7a36c8fc9ad6348d88c5e30bbb131b48 Mon Sep 17 00:00:00 2001 From: Nicolo Fusi Date: Thu, 31 Jan 2013 13:43:37 +0000 Subject: [PATCH 054/197] changed travis conf --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5c654d37..e7944d8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,12 @@ virtualenv: # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors before_install: - sudo apt-get install -qq python-scipy python-pip - - sudo apt-get install -qq python-matplotlib \ No newline at end of file + - sudo apt-get install -qq python-matplotlib + +install: + - pip install sphinx + - pip install nose + - pip install . --use-mirrors +# command to run tests, e.g. python setup.py test +script: + - nosetests GPy/testing \ No newline at end of file From ae27ce833e2b97c485e6e78222aca6690ee91260 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 13:51:02 +0000 Subject: [PATCH 055/197] Added mock file --- mock.py | 2366 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2366 insertions(+) create mode 100644 mock.py diff --git a/mock.py b/mock.py new file mode 100644 index 00000000..cc4aa653 --- /dev/null +++ b/mock.py @@ -0,0 +1,2366 @@ +# mock.py +# Test tools for mocking and patching. +# Copyright (C) 2007-2012 Michael Foord & the mock team +# E-mail: fuzzyman AT voidspace DOT org DOT uk + +# mock 1.0.1 +# http://www.voidspace.org.uk/python/mock/ + +# Released subject to the BSD License +# Please see http://www.voidspace.org.uk/python/license.shtml + +__all__ = ( + 'Mock', + 'MagicMock', + 'patch', + 'sentinel', + 'DEFAULT', + 'ANY', + 'call', + 'create_autospec', + 'FILTER_DIR', + 'NonCallableMock', + 'NonCallableMagicMock', + 'mock_open', + 'PropertyMock', +) + + +__version__ = '1.0.1' + + +import pprint +import sys + +try: + import inspect +except ImportError: + # for alternative platforms that + # may not have inspect + inspect = None + +try: + from functools import wraps as original_wraps +except ImportError: + # Python 2.4 compatibility + def wraps(original): + def inner(f): + f.__name__ = original.__name__ + f.__doc__ = original.__doc__ + f.__module__ = original.__module__ + wrapped = getattr(original, '__wrapped__', original) + f.__wrapped__ = wrapped + return f + return inner +else: + if sys.version_info[:2] >= (3, 2): + wraps = original_wraps + else: + def wraps(func): + def inner(f): + f = original_wraps(func)(f) + wrapped = getattr(func, '__wrapped__', func) + f.__wrapped__ = wrapped + return f + return inner + +try: + unicode +except NameError: + # Python 3 + basestring = unicode = str + +try: + long +except NameError: + # Python 3 + long = int + +try: + BaseException +except NameError: + # Python 2.4 compatibility + BaseException = Exception + +try: + next +except NameError: + def next(obj): + return obj.next() + + +BaseExceptions = (BaseException,) +if 'java' in sys.platform: + # jython + import java + BaseExceptions = (BaseException, java.lang.Throwable) + +try: + _isidentifier = str.isidentifier +except AttributeError: + # Python 2.X + import keyword + import re + regex = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) + def _isidentifier(string): + if string in keyword.kwlist: + return False + return regex.match(string) + + +inPy3k = sys.version_info[0] == 3 + +# Needed to work around Python 3 bug where use of "super" interferes with +# defining __class__ as a descriptor +_super = super + +self = 'im_self' +builtin = '__builtin__' +if inPy3k: + self = '__self__' + builtin = 'builtins' + +FILTER_DIR = True + + +def _is_instance_mock(obj): + # can't use isinstance on Mock objects because they override __class__ + # The base class for all mocks is NonCallableMock + return issubclass(type(obj), NonCallableMock) + + +def _is_exception(obj): + return ( + isinstance(obj, BaseExceptions) or + isinstance(obj, ClassTypes) and issubclass(obj, BaseExceptions) + ) + + +class _slotted(object): + __slots__ = ['a'] + + +DescriptorTypes = ( + type(_slotted.a), + property, +) + + +def _getsignature(func, skipfirst, instance=False): + if inspect is None: + raise ImportError('inspect module not available') + + if isinstance(func, ClassTypes) and not instance: + try: + func = func.__init__ + except AttributeError: + return + skipfirst = True + elif not isinstance(func, FunctionTypes): + # for classes where instance is True we end up here too + try: + func = func.__call__ + except AttributeError: + return + + if inPy3k: + try: + argspec = inspect.getfullargspec(func) + except TypeError: + # C function / method, possibly inherited object().__init__ + return + regargs, varargs, varkw, defaults, kwonly, kwonlydef, ann = argspec + else: + try: + regargs, varargs, varkwargs, defaults = inspect.getargspec(func) + except TypeError: + # C function / method, possibly inherited object().__init__ + return + + # instance methods and classmethods need to lose the self argument + if getattr(func, self, None) is not None: + regargs = regargs[1:] + if skipfirst: + # this condition and the above one are never both True - why? + regargs = regargs[1:] + + if inPy3k: + signature = inspect.formatargspec( + regargs, varargs, varkw, defaults, + kwonly, kwonlydef, ann, formatvalue=lambda value: "") + else: + signature = inspect.formatargspec( + regargs, varargs, varkwargs, defaults, + formatvalue=lambda value: "") + return signature[1:-1], func + + +def _check_signature(func, mock, skipfirst, instance=False): + if not _callable(func): + return + + result = _getsignature(func, skipfirst, instance) + if result is None: + return + signature, func = result + + # can't use self because "self" is common as an argument name + # unfortunately even not in the first place + src = "lambda _mock_self, %s: None" % signature + checksig = eval(src, {}) + _copy_func_details(func, checksig) + type(mock)._mock_check_sig = checksig + + +def _copy_func_details(func, funcopy): + funcopy.__name__ = func.__name__ + funcopy.__doc__ = func.__doc__ + #funcopy.__dict__.update(func.__dict__) + funcopy.__module__ = func.__module__ + if not inPy3k: + funcopy.func_defaults = func.func_defaults + return + funcopy.__defaults__ = func.__defaults__ + funcopy.__kwdefaults__ = func.__kwdefaults__ + + +def _callable(obj): + if isinstance(obj, ClassTypes): + return True + if getattr(obj, '__call__', None) is not None: + return True + return False + + +def _is_list(obj): + # checks for list or tuples + # XXXX badly named! + return type(obj) in (list, tuple) + + +def _instance_callable(obj): + """Given an object, return True if the object is callable. + For classes, return True if instances would be callable.""" + if not isinstance(obj, ClassTypes): + # already an instance + return getattr(obj, '__call__', None) is not None + + klass = obj + # uses __bases__ instead of __mro__ so that we work with old style classes + if klass.__dict__.get('__call__') is not None: + return True + + for base in klass.__bases__: + if _instance_callable(base): + return True + return False + + +def _set_signature(mock, original, instance=False): + # creates a function with signature (*args, **kwargs) that delegates to a + # mock. It still does signature checking by calling a lambda with the same + # signature as the original. + if not _callable(original): + return + + skipfirst = isinstance(original, ClassTypes) + result = _getsignature(original, skipfirst, instance) + if result is None: + # was a C function (e.g. object().__init__ ) that can't be mocked + return + + signature, func = result + + src = "lambda %s: None" % signature + checksig = eval(src, {}) + _copy_func_details(func, checksig) + + name = original.__name__ + if not _isidentifier(name): + name = 'funcopy' + context = {'_checksig_': checksig, 'mock': mock} + src = """def %s(*args, **kwargs): + _checksig_(*args, **kwargs) + return mock(*args, **kwargs)""" % name + exec (src, context) + funcopy = context[name] + _setup_func(funcopy, mock) + return funcopy + + +def _setup_func(funcopy, mock): + funcopy.mock = mock + + # can't use isinstance with mocks + if not _is_instance_mock(mock): + return + + def assert_called_with(*args, **kwargs): + return mock.assert_called_with(*args, **kwargs) + def assert_called_once_with(*args, **kwargs): + return mock.assert_called_once_with(*args, **kwargs) + def assert_has_calls(*args, **kwargs): + return mock.assert_has_calls(*args, **kwargs) + def assert_any_call(*args, **kwargs): + return mock.assert_any_call(*args, **kwargs) + def reset_mock(): + funcopy.method_calls = _CallList() + funcopy.mock_calls = _CallList() + mock.reset_mock() + ret = funcopy.return_value + if _is_instance_mock(ret) and not ret is mock: + ret.reset_mock() + + funcopy.called = False + funcopy.call_count = 0 + funcopy.call_args = None + funcopy.call_args_list = _CallList() + funcopy.method_calls = _CallList() + funcopy.mock_calls = _CallList() + + funcopy.return_value = mock.return_value + funcopy.side_effect = mock.side_effect + funcopy._mock_children = mock._mock_children + + funcopy.assert_called_with = assert_called_with + funcopy.assert_called_once_with = assert_called_once_with + funcopy.assert_has_calls = assert_has_calls + funcopy.assert_any_call = assert_any_call + funcopy.reset_mock = reset_mock + + mock._mock_delegate = funcopy + + +def _is_magic(name): + return '__%s__' % name[2:-2] == name + + +class _SentinelObject(object): + "A unique, named, sentinel object." + def __init__(self, name): + self.name = name + + def __repr__(self): + return 'sentinel.%s' % self.name + + +class _Sentinel(object): + """Access attributes to return a named object, usable as a sentinel.""" + def __init__(self): + self._sentinels = {} + + def __getattr__(self, name): + if name == '__bases__': + # Without this help(mock) raises an exception + raise AttributeError + return self._sentinels.setdefault(name, _SentinelObject(name)) + + +sentinel = _Sentinel() + +DEFAULT = sentinel.DEFAULT +_missing = sentinel.MISSING +_deleted = sentinel.DELETED + + +class OldStyleClass: + pass +ClassType = type(OldStyleClass) + + +def _copy(value): + if type(value) in (dict, list, tuple, set): + return type(value)(value) + return value + + +ClassTypes = (type,) +if not inPy3k: + ClassTypes = (type, ClassType) + +_allowed_names = set( + [ + 'return_value', '_mock_return_value', 'side_effect', + '_mock_side_effect', '_mock_parent', '_mock_new_parent', + '_mock_name', '_mock_new_name' + ] +) + + +def _delegating_property(name): + _allowed_names.add(name) + _the_name = '_mock_' + name + def _get(self, name=name, _the_name=_the_name): + sig = self._mock_delegate + if sig is None: + return getattr(self, _the_name) + return getattr(sig, name) + def _set(self, value, name=name, _the_name=_the_name): + sig = self._mock_delegate + if sig is None: + self.__dict__[_the_name] = value + else: + setattr(sig, name, value) + + return property(_get, _set) + + + +class _CallList(list): + + def __contains__(self, value): + if not isinstance(value, list): + return list.__contains__(self, value) + len_value = len(value) + len_self = len(self) + if len_value > len_self: + return False + + for i in range(0, len_self - len_value + 1): + sub_list = self[i:i+len_value] + if sub_list == value: + return True + return False + + def __repr__(self): + return pprint.pformat(list(self)) + + +def _check_and_set_parent(parent, value, name, new_name): + if not _is_instance_mock(value): + return False + if ((value._mock_name or value._mock_new_name) or + (value._mock_parent is not None) or + (value._mock_new_parent is not None)): + return False + + _parent = parent + while _parent is not None: + # setting a mock (value) as a child or return value of itself + # should not modify the mock + if _parent is value: + return False + _parent = _parent._mock_new_parent + + if new_name: + value._mock_new_parent = parent + value._mock_new_name = new_name + if name: + value._mock_parent = parent + value._mock_name = name + return True + + + +class Base(object): + _mock_return_value = DEFAULT + _mock_side_effect = None + def __init__(self, *args, **kwargs): + pass + + + +class NonCallableMock(Base): + """A non-callable version of `Mock`""" + + def __new__(cls, *args, **kw): + # every instance has its own class + # so we can create magic methods on the + # class without stomping on other mocks + new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__}) + instance = object.__new__(new) + return instance + + + def __init__( + self, spec=None, wraps=None, name=None, spec_set=None, + parent=None, _spec_state=None, _new_name='', _new_parent=None, + **kwargs + ): + if _new_parent is None: + _new_parent = parent + + __dict__ = self.__dict__ + __dict__['_mock_parent'] = parent + __dict__['_mock_name'] = name + __dict__['_mock_new_name'] = _new_name + __dict__['_mock_new_parent'] = _new_parent + + if spec_set is not None: + spec = spec_set + spec_set = True + + self._mock_add_spec(spec, spec_set) + + __dict__['_mock_children'] = {} + __dict__['_mock_wraps'] = wraps + __dict__['_mock_delegate'] = None + + __dict__['_mock_called'] = False + __dict__['_mock_call_args'] = None + __dict__['_mock_call_count'] = 0 + __dict__['_mock_call_args_list'] = _CallList() + __dict__['_mock_mock_calls'] = _CallList() + + __dict__['method_calls'] = _CallList() + + if kwargs: + self.configure_mock(**kwargs) + + _super(NonCallableMock, self).__init__( + spec, wraps, name, spec_set, parent, + _spec_state + ) + + + def attach_mock(self, mock, attribute): + """ + Attach a mock as an attribute of this one, replacing its name and + parent. Calls to the attached mock will be recorded in the + `method_calls` and `mock_calls` attributes of this one.""" + mock._mock_parent = None + mock._mock_new_parent = None + mock._mock_name = '' + mock._mock_new_name = None + + setattr(self, attribute, mock) + + + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + + + def _mock_add_spec(self, spec, spec_set): + _spec_class = None + + if spec is not None and not _is_list(spec): + if isinstance(spec, ClassTypes): + _spec_class = spec + else: + _spec_class = _get_class(spec) + + spec = dir(spec) + + __dict__ = self.__dict__ + __dict__['_spec_class'] = _spec_class + __dict__['_spec_set'] = spec_set + __dict__['_mock_methods'] = spec + + + def __get_return_value(self): + ret = self._mock_return_value + if self._mock_delegate is not None: + ret = self._mock_delegate.return_value + + if ret is DEFAULT: + ret = self._get_child_mock( + _new_parent=self, _new_name='()' + ) + self.return_value = ret + return ret + + + def __set_return_value(self, value): + if self._mock_delegate is not None: + self._mock_delegate.return_value = value + else: + self._mock_return_value = value + _check_and_set_parent(self, value, None, '()') + + __return_value_doc = "The value to be returned when the mock is called." + return_value = property(__get_return_value, __set_return_value, + __return_value_doc) + + + @property + def __class__(self): + if self._spec_class is None: + return type(self) + return self._spec_class + + called = _delegating_property('called') + call_count = _delegating_property('call_count') + call_args = _delegating_property('call_args') + call_args_list = _delegating_property('call_args_list') + mock_calls = _delegating_property('mock_calls') + + + def __get_side_effect(self): + sig = self._mock_delegate + if sig is None: + return self._mock_side_effect + return sig.side_effect + + def __set_side_effect(self, value): + value = _try_iter(value) + sig = self._mock_delegate + if sig is None: + self._mock_side_effect = value + else: + sig.side_effect = value + + side_effect = property(__get_side_effect, __set_side_effect) + + + def reset_mock(self): + "Restore the mock object to its initial state." + self.called = False + self.call_args = None + self.call_count = 0 + self.mock_calls = _CallList() + self.call_args_list = _CallList() + self.method_calls = _CallList() + + for child in self._mock_children.values(): + if isinstance(child, _SpecState): + continue + child.reset_mock() + + ret = self._mock_return_value + if _is_instance_mock(ret) and ret is not self: + ret.reset_mock() + + + def configure_mock(self, **kwargs): + """Set attributes on the mock through keyword arguments. + + Attributes plus return values and side effects can be set on child + mocks using standard dot notation and unpacking a dictionary in the + method call: + + >>> attrs = {'method.return_value': 3, 'other.side_effect': KeyError} + >>> mock.configure_mock(**attrs)""" + for arg, val in sorted(kwargs.items(), + # we sort on the number of dots so that + # attributes are set before we set attributes on + # attributes + key=lambda entry: entry[0].count('.')): + args = arg.split('.') + final = args.pop() + obj = self + for entry in args: + obj = getattr(obj, entry) + setattr(obj, final, val) + + + def __getattr__(self, name): + if name == '_mock_methods': + raise AttributeError(name) + elif self._mock_methods is not None: + if name not in self._mock_methods or name in _all_magics: + raise AttributeError("Mock object has no attribute %r" % name) + elif _is_magic(name): + raise AttributeError(name) + + result = self._mock_children.get(name) + if result is _deleted: + raise AttributeError(name) + elif result is None: + wraps = None + if self._mock_wraps is not None: + # XXXX should we get the attribute without triggering code + # execution? + wraps = getattr(self._mock_wraps, name) + + result = self._get_child_mock( + parent=self, name=name, wraps=wraps, _new_name=name, + _new_parent=self + ) + self._mock_children[name] = result + + elif isinstance(result, _SpecState): + result = create_autospec( + result.spec, result.spec_set, result.instance, + result.parent, result.name + ) + self._mock_children[name] = result + + return result + + + def __repr__(self): + _name_list = [self._mock_new_name] + _parent = self._mock_new_parent + last = self + + dot = '.' + if _name_list == ['()']: + dot = '' + seen = set() + while _parent is not None: + last = _parent + + _name_list.append(_parent._mock_new_name + dot) + dot = '.' + if _parent._mock_new_name == '()': + dot = '' + + _parent = _parent._mock_new_parent + + # use ids here so as not to call __hash__ on the mocks + if id(_parent) in seen: + break + seen.add(id(_parent)) + + _name_list = list(reversed(_name_list)) + _first = last._mock_name or 'mock' + if len(_name_list) > 1: + if _name_list[1] not in ('()', '().'): + _first += '.' + _name_list[0] = _first + name = ''.join(_name_list) + + name_string = '' + if name not in ('mock', 'mock.'): + name_string = ' name=%r' % name + + spec_string = '' + if self._spec_class is not None: + spec_string = ' spec=%r' + if self._spec_set: + spec_string = ' spec_set=%r' + spec_string = spec_string % self._spec_class.__name__ + return "<%s%s%s id='%s'>" % ( + type(self).__name__, + name_string, + spec_string, + id(self) + ) + + + def __dir__(self): + """Filter the output of `dir(mock)` to only useful members. + XXXX + """ + extras = self._mock_methods or [] + from_type = dir(type(self)) + from_dict = list(self.__dict__) + + if FILTER_DIR: + from_type = [e for e in from_type if not e.startswith('_')] + from_dict = [e for e in from_dict if not e.startswith('_') or + _is_magic(e)] + return sorted(set(extras + from_type + from_dict + + list(self._mock_children))) + + + def __setattr__(self, name, value): + if name in _allowed_names: + # property setters go through here + return object.__setattr__(self, name, value) + elif (self._spec_set and self._mock_methods is not None and + name not in self._mock_methods and + name not in self.__dict__): + raise AttributeError("Mock object has no attribute '%s'" % name) + elif name in _unsupported_magics: + msg = 'Attempting to set unsupported magic method %r.' % name + raise AttributeError(msg) + elif name in _all_magics: + if self._mock_methods is not None and name not in self._mock_methods: + raise AttributeError("Mock object has no attribute '%s'" % name) + + if not _is_instance_mock(value): + setattr(type(self), name, _get_method(name, value)) + original = value + value = lambda *args, **kw: original(self, *args, **kw) + else: + # only set _new_name and not name so that mock_calls is tracked + # but not method calls + _check_and_set_parent(self, value, None, name) + setattr(type(self), name, value) + self._mock_children[name] = value + elif name == '__class__': + self._spec_class = value + return + else: + if _check_and_set_parent(self, value, name, name): + self._mock_children[name] = value + return object.__setattr__(self, name, value) + + + def __delattr__(self, name): + if name in _all_magics and name in type(self).__dict__: + delattr(type(self), name) + if name not in self.__dict__: + # for magic methods that are still MagicProxy objects and + # not set on the instance itself + return + + if name in self.__dict__: + object.__delattr__(self, name) + + obj = self._mock_children.get(name, _missing) + if obj is _deleted: + raise AttributeError(name) + if obj is not _missing: + del self._mock_children[name] + self._mock_children[name] = _deleted + + + + def _format_mock_call_signature(self, args, kwargs): + name = self._mock_name or 'mock' + return _format_call_signature(name, args, kwargs) + + + def _format_mock_failure_message(self, args, kwargs): + message = 'Expected call: %s\nActual call: %s' + expected_string = self._format_mock_call_signature(args, kwargs) + call_args = self.call_args + if len(call_args) == 3: + call_args = call_args[1:] + actual_string = self._format_mock_call_signature(*call_args) + return message % (expected_string, actual_string) + + + def assert_called_with(_mock_self, *args, **kwargs): + """assert that the mock was called with the specified arguments. + + Raises an AssertionError if the args and keyword args passed in are + different to the last call to the mock.""" + self = _mock_self + if self.call_args is None: + expected = self._format_mock_call_signature(args, kwargs) + raise AssertionError('Expected call: %s\nNot called' % (expected,)) + + if self.call_args != (args, kwargs): + msg = self._format_mock_failure_message(args, kwargs) + raise AssertionError(msg) + + + def assert_called_once_with(_mock_self, *args, **kwargs): + """assert that the mock was called exactly once and with the specified + arguments.""" + self = _mock_self + if not self.call_count == 1: + msg = ("Expected to be called once. Called %s times." % + self.call_count) + raise AssertionError(msg) + return self.assert_called_with(*args, **kwargs) + + + def assert_has_calls(self, calls, any_order=False): + """assert the mock has been called with the specified calls. + The `mock_calls` list is checked for the calls. + + If `any_order` is False (the default) then the calls must be + sequential. There can be extra calls before or after the + specified calls. + + If `any_order` is True then the calls can be in any order, but + they must all appear in `mock_calls`.""" + if not any_order: + if calls not in self.mock_calls: + raise AssertionError( + 'Calls not found.\nExpected: %r\n' + 'Actual: %r' % (calls, self.mock_calls) + ) + return + + all_calls = list(self.mock_calls) + + not_found = [] + for kall in calls: + try: + all_calls.remove(kall) + except ValueError: + not_found.append(kall) + if not_found: + raise AssertionError( + '%r not all found in call list' % (tuple(not_found),) + ) + + + def assert_any_call(self, *args, **kwargs): + """assert the mock has been called with the specified arguments. + + The assert passes if the mock has *ever* been called, unlike + `assert_called_with` and `assert_called_once_with` that only pass if + the call is the most recent one.""" + kall = call(*args, **kwargs) + if kall not in self.call_args_list: + expected_string = self._format_mock_call_signature(args, kwargs) + raise AssertionError( + '%s call not found' % expected_string + ) + + + def _get_child_mock(self, **kw): + """Create the child mocks for attributes and return value. + By default child mocks will be the same type as the parent. + Subclasses of Mock may want to override this to customize the way + child mocks are made. + + For non-callable mocks the callable variant will be used (rather than + any custom subclass).""" + _type = type(self) + if not issubclass(_type, CallableMixin): + if issubclass(_type, NonCallableMagicMock): + klass = MagicMock + elif issubclass(_type, NonCallableMock) : + klass = Mock + else: + klass = _type.__mro__[1] + return klass(**kw) + + + +def _try_iter(obj): + if obj is None: + return obj + if _is_exception(obj): + return obj + if _callable(obj): + return obj + try: + return iter(obj) + except TypeError: + # XXXX backwards compatibility + # but this will blow up on first call - so maybe we should fail early? + return obj + + + +class CallableMixin(Base): + + def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, + wraps=None, name=None, spec_set=None, parent=None, + _spec_state=None, _new_name='', _new_parent=None, **kwargs): + self.__dict__['_mock_return_value'] = return_value + + _super(CallableMixin, self).__init__( + spec, wraps, name, spec_set, parent, + _spec_state, _new_name, _new_parent, **kwargs + ) + + self.side_effect = side_effect + + + def _mock_check_sig(self, *args, **kwargs): + # stub method that can be replaced with one with a specific signature + pass + + + def __call__(_mock_self, *args, **kwargs): + # can't use self in-case a function / method we are mocking uses self + # in the signature + _mock_self._mock_check_sig(*args, **kwargs) + return _mock_self._mock_call(*args, **kwargs) + + + def _mock_call(_mock_self, *args, **kwargs): + self = _mock_self + self.called = True + self.call_count += 1 + self.call_args = _Call((args, kwargs), two=True) + self.call_args_list.append(_Call((args, kwargs), two=True)) + + _new_name = self._mock_new_name + _new_parent = self._mock_new_parent + self.mock_calls.append(_Call(('', args, kwargs))) + + seen = set() + skip_next_dot = _new_name == '()' + do_method_calls = self._mock_parent is not None + name = self._mock_name + while _new_parent is not None: + this_mock_call = _Call((_new_name, args, kwargs)) + if _new_parent._mock_new_name: + dot = '.' + if skip_next_dot: + dot = '' + + skip_next_dot = False + if _new_parent._mock_new_name == '()': + skip_next_dot = True + + _new_name = _new_parent._mock_new_name + dot + _new_name + + if do_method_calls: + if _new_name == name: + this_method_call = this_mock_call + else: + this_method_call = _Call((name, args, kwargs)) + _new_parent.method_calls.append(this_method_call) + + do_method_calls = _new_parent._mock_parent is not None + if do_method_calls: + name = _new_parent._mock_name + '.' + name + + _new_parent.mock_calls.append(this_mock_call) + _new_parent = _new_parent._mock_new_parent + + # use ids here so as not to call __hash__ on the mocks + _new_parent_id = id(_new_parent) + if _new_parent_id in seen: + break + seen.add(_new_parent_id) + + ret_val = DEFAULT + effect = self.side_effect + if effect is not None: + if _is_exception(effect): + raise effect + + if not _callable(effect): + result = next(effect) + if _is_exception(result): + raise result + return result + + ret_val = effect(*args, **kwargs) + if ret_val is DEFAULT: + ret_val = self.return_value + + if (self._mock_wraps is not None and + self._mock_return_value is DEFAULT): + return self._mock_wraps(*args, **kwargs) + if ret_val is DEFAULT: + ret_val = self.return_value + return ret_val + + + +class Mock(CallableMixin, NonCallableMock): + """ + Create a new `Mock` object. `Mock` takes several optional arguments + that specify the behaviour of the Mock object: + + * `spec`: This can be either a list of strings or an existing object (a + class or instance) that acts as the specification for the mock object. If + you pass in an object then a list of strings is formed by calling dir on + the object (excluding unsupported magic attributes and methods). Accessing + any attribute not in this list will raise an `AttributeError`. + + If `spec` is an object (rather than a list of strings) then + `mock.__class__` returns the class of the spec object. This allows mocks + to pass `isinstance` tests. + + * `spec_set`: A stricter variant of `spec`. If used, attempting to *set* + or get an attribute on the mock that isn't on the object passed as + `spec_set` will raise an `AttributeError`. + + * `side_effect`: A function to be called whenever the Mock is called. See + the `side_effect` attribute. Useful for raising exceptions or + dynamically changing return values. The function is called with the same + arguments as the mock, and unless it returns `DEFAULT`, the return + value of this function is used as the return value. + + Alternatively `side_effect` can be an exception class or instance. In + this case the exception will be raised when the mock is called. + + If `side_effect` is an iterable then each call to the mock will return + the next value from the iterable. If any of the members of the iterable + are exceptions they will be raised instead of returned. + + * `return_value`: The value returned when the mock is called. By default + this is a new Mock (created on first access). See the + `return_value` attribute. + + * `wraps`: Item for the mock object to wrap. If `wraps` is not None then + calling the Mock will pass the call through to the wrapped object + (returning the real result). Attribute access on the mock will return a + Mock object that wraps the corresponding attribute of the wrapped object + (so attempting to access an attribute that doesn't exist will raise an + `AttributeError`). + + If the mock has an explicit `return_value` set then calls are not passed + to the wrapped object and the `return_value` is returned instead. + + * `name`: If the mock has a name then it will be used in the repr of the + mock. This can be useful for debugging. The name is propagated to child + mocks. + + Mocks can also be called with arbitrary keyword arguments. These will be + used to set attributes on the mock after it is created. + """ + + + +def _dot_lookup(thing, comp, import_path): + try: + return getattr(thing, comp) + except AttributeError: + __import__(import_path) + return getattr(thing, comp) + + +def _importer(target): + components = target.split('.') + import_path = components.pop(0) + thing = __import__(import_path) + + for comp in components: + import_path += ".%s" % comp + thing = _dot_lookup(thing, comp, import_path) + return thing + + +def _is_started(patcher): + # XXXX horrible + return hasattr(patcher, 'is_local') + + +class _patch(object): + + attribute_name = None + _active_patches = set() + + def __init__( + self, getter, attribute, new, spec, create, + spec_set, autospec, new_callable, kwargs + ): + if new_callable is not None: + if new is not DEFAULT: + raise ValueError( + "Cannot use 'new' and 'new_callable' together" + ) + if autospec is not None: + raise ValueError( + "Cannot use 'autospec' and 'new_callable' together" + ) + + self.getter = getter + self.attribute = attribute + self.new = new + self.new_callable = new_callable + self.spec = spec + self.create = create + self.has_local = False + self.spec_set = spec_set + self.autospec = autospec + self.kwargs = kwargs + self.additional_patchers = [] + + + def copy(self): + patcher = _patch( + self.getter, self.attribute, self.new, self.spec, + self.create, self.spec_set, + self.autospec, self.new_callable, self.kwargs + ) + patcher.attribute_name = self.attribute_name + patcher.additional_patchers = [ + p.copy() for p in self.additional_patchers + ] + return patcher + + + def __call__(self, func): + if isinstance(func, ClassTypes): + return self.decorate_class(func) + return self.decorate_callable(func) + + + def decorate_class(self, klass): + for attr in dir(klass): + if not attr.startswith(patch.TEST_PREFIX): + continue + + attr_value = getattr(klass, attr) + if not hasattr(attr_value, "__call__"): + continue + + patcher = self.copy() + setattr(klass, attr, patcher(attr_value)) + return klass + + + def decorate_callable(self, func): + if hasattr(func, 'patchings'): + func.patchings.append(self) + return func + + @wraps(func) + def patched(*args, **keywargs): + # don't use a with here (backwards compatability with Python 2.4) + extra_args = [] + entered_patchers = [] + + # can't use try...except...finally because of Python 2.4 + # compatibility + exc_info = tuple() + try: + try: + for patching in patched.patchings: + arg = patching.__enter__() + entered_patchers.append(patching) + if patching.attribute_name is not None: + keywargs.update(arg) + elif patching.new is DEFAULT: + extra_args.append(arg) + + args += tuple(extra_args) + return func(*args, **keywargs) + except: + if (patching not in entered_patchers and + _is_started(patching)): + # the patcher may have been started, but an exception + # raised whilst entering one of its additional_patchers + entered_patchers.append(patching) + # Pass the exception to __exit__ + exc_info = sys.exc_info() + # re-raise the exception + raise + finally: + for patching in reversed(entered_patchers): + patching.__exit__(*exc_info) + + patched.patchings = [self] + if hasattr(func, 'func_code'): + # not in Python 3 + patched.compat_co_firstlineno = getattr( + func, "compat_co_firstlineno", + func.func_code.co_firstlineno + ) + return patched + + + def get_original(self): + target = self.getter() + name = self.attribute + + original = DEFAULT + local = False + + try: + original = target.__dict__[name] + except (AttributeError, KeyError): + original = getattr(target, name, DEFAULT) + else: + local = True + + if not self.create and original is DEFAULT: + raise AttributeError( + "%s does not have the attribute %r" % (target, name) + ) + return original, local + + + def __enter__(self): + """Perform the patch.""" + new, spec, spec_set = self.new, self.spec, self.spec_set + autospec, kwargs = self.autospec, self.kwargs + new_callable = self.new_callable + self.target = self.getter() + + # normalise False to None + if spec is False: + spec = None + if spec_set is False: + spec_set = None + if autospec is False: + autospec = None + + if spec is not None and autospec is not None: + raise TypeError("Can't specify spec and autospec") + if ((spec is not None or autospec is not None) and + spec_set not in (True, None)): + raise TypeError("Can't provide explicit spec_set *and* spec or autospec") + + original, local = self.get_original() + + if new is DEFAULT and autospec is None: + inherit = False + if spec is True: + # set spec to the object we are replacing + spec = original + if spec_set is True: + spec_set = original + spec = None + elif spec is not None: + if spec_set is True: + spec_set = spec + spec = None + elif spec_set is True: + spec_set = original + + if spec is not None or spec_set is not None: + if original is DEFAULT: + raise TypeError("Can't use 'spec' with create=True") + if isinstance(original, ClassTypes): + # If we're patching out a class and there is a spec + inherit = True + + Klass = MagicMock + _kwargs = {} + if new_callable is not None: + Klass = new_callable + elif spec is not None or spec_set is not None: + this_spec = spec + if spec_set is not None: + this_spec = spec_set + if _is_list(this_spec): + not_callable = '__call__' not in this_spec + else: + not_callable = not _callable(this_spec) + if not_callable: + Klass = NonCallableMagicMock + + if spec is not None: + _kwargs['spec'] = spec + if spec_set is not None: + _kwargs['spec_set'] = spec_set + + # add a name to mocks + if (isinstance(Klass, type) and + issubclass(Klass, NonCallableMock) and self.attribute): + _kwargs['name'] = self.attribute + + _kwargs.update(kwargs) + new = Klass(**_kwargs) + + if inherit and _is_instance_mock(new): + # we can only tell if the instance should be callable if the + # spec is not a list + this_spec = spec + if spec_set is not None: + this_spec = spec_set + if (not _is_list(this_spec) and not + _instance_callable(this_spec)): + Klass = NonCallableMagicMock + + _kwargs.pop('name') + new.return_value = Klass(_new_parent=new, _new_name='()', + **_kwargs) + elif autospec is not None: + # spec is ignored, new *must* be default, spec_set is treated + # as a boolean. Should we check spec is not None and that spec_set + # is a bool? + if new is not DEFAULT: + raise TypeError( + "autospec creates the mock for you. Can't specify " + "autospec and new." + ) + if original is DEFAULT: + raise TypeError("Can't use 'autospec' with create=True") + spec_set = bool(spec_set) + if autospec is True: + autospec = original + + new = create_autospec(autospec, spec_set=spec_set, + _name=self.attribute, **kwargs) + elif kwargs: + # can't set keyword args when we aren't creating the mock + # XXXX If new is a Mock we could call new.configure_mock(**kwargs) + raise TypeError("Can't pass kwargs to a mock we aren't creating") + + new_attr = new + + self.temp_original = original + self.is_local = local + setattr(self.target, self.attribute, new_attr) + if self.attribute_name is not None: + extra_args = {} + if self.new is DEFAULT: + extra_args[self.attribute_name] = new + for patching in self.additional_patchers: + arg = patching.__enter__() + if patching.new is DEFAULT: + extra_args.update(arg) + return extra_args + + return new + + + def __exit__(self, *exc_info): + """Undo the patch.""" + if not _is_started(self): + raise RuntimeError('stop called on unstarted patcher') + + if self.is_local and self.temp_original is not DEFAULT: + setattr(self.target, self.attribute, self.temp_original) + else: + delattr(self.target, self.attribute) + if not self.create and not hasattr(self.target, self.attribute): + # needed for proxy objects like django settings + setattr(self.target, self.attribute, self.temp_original) + + del self.temp_original + del self.is_local + del self.target + for patcher in reversed(self.additional_patchers): + if _is_started(patcher): + patcher.__exit__(*exc_info) + + + def start(self): + """Activate a patch, returning any created mock.""" + result = self.__enter__() + self._active_patches.add(self) + return result + + + def stop(self): + """Stop an active patch.""" + self._active_patches.discard(self) + return self.__exit__() + + + +def _get_target(target): + try: + target, attribute = target.rsplit('.', 1) + except (TypeError, ValueError): + raise TypeError("Need a valid target to patch. You supplied: %r" % + (target,)) + getter = lambda: _importer(target) + return getter, attribute + + +def _patch_object( + target, attribute, new=DEFAULT, spec=None, + create=False, spec_set=None, autospec=None, + new_callable=None, **kwargs + ): + """ + patch.object(target, attribute, new=DEFAULT, spec=None, create=False, + spec_set=None, autospec=None, new_callable=None, **kwargs) + + patch the named member (`attribute`) on an object (`target`) with a mock + object. + + `patch.object` can be used as a decorator, class decorator or a context + manager. Arguments `new`, `spec`, `create`, `spec_set`, + `autospec` and `new_callable` have the same meaning as for `patch`. Like + `patch`, `patch.object` takes arbitrary keyword arguments for configuring + the mock object it creates. + + When used as a class decorator `patch.object` honours `patch.TEST_PREFIX` + for choosing which methods to wrap. + """ + getter = lambda: target + return _patch( + getter, attribute, new, spec, create, + spec_set, autospec, new_callable, kwargs + ) + + +def _patch_multiple(target, spec=None, create=False, spec_set=None, + autospec=None, new_callable=None, **kwargs): + """Perform multiple patches in a single call. It takes the object to be + patched (either as an object or a string to fetch the object by importing) + and keyword arguments for the patches:: + + with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'): + ... + + Use `DEFAULT` as the value if you want `patch.multiple` to create + mocks for you. In this case the created mocks are passed into a decorated + function by keyword, and a dictionary is returned when `patch.multiple` is + used as a context manager. + + `patch.multiple` can be used as a decorator, class decorator or a context + manager. The arguments `spec`, `spec_set`, `create`, + `autospec` and `new_callable` have the same meaning as for `patch`. These + arguments will be applied to *all* patches done by `patch.multiple`. + + When used as a class decorator `patch.multiple` honours `patch.TEST_PREFIX` + for choosing which methods to wrap. + """ + if type(target) in (unicode, str): + getter = lambda: _importer(target) + else: + getter = lambda: target + + if not kwargs: + raise ValueError( + 'Must supply at least one keyword argument with patch.multiple' + ) + # need to wrap in a list for python 3, where items is a view + items = list(kwargs.items()) + attribute, new = items[0] + patcher = _patch( + getter, attribute, new, spec, create, spec_set, + autospec, new_callable, {} + ) + patcher.attribute_name = attribute + for attribute, new in items[1:]: + this_patcher = _patch( + getter, attribute, new, spec, create, spec_set, + autospec, new_callable, {} + ) + this_patcher.attribute_name = attribute + patcher.additional_patchers.append(this_patcher) + return patcher + + +def patch( + target, new=DEFAULT, spec=None, create=False, + spec_set=None, autospec=None, new_callable=None, **kwargs + ): + """ + `patch` acts as a function decorator, class decorator or a context + manager. Inside the body of the function or with statement, the `target` + is patched with a `new` object. When the function/with statement exits + the patch is undone. + + If `new` is omitted, then the target is replaced with a + `MagicMock`. If `patch` is used as a decorator and `new` is + omitted, the created mock is passed in as an extra argument to the + decorated function. If `patch` is used as a context manager the created + mock is returned by the context manager. + + `target` should be a string in the form `'package.module.ClassName'`. The + `target` is imported and the specified object replaced with the `new` + object, so the `target` must be importable from the environment you are + calling `patch` from. The target is imported when the decorated function + is executed, not at decoration time. + + The `spec` and `spec_set` keyword arguments are passed to the `MagicMock` + if patch is creating one for you. + + In addition you can pass `spec=True` or `spec_set=True`, which causes + patch to pass in the object being mocked as the spec/spec_set object. + + `new_callable` allows you to specify a different class, or callable object, + that will be called to create the `new` object. By default `MagicMock` is + used. + + A more powerful form of `spec` is `autospec`. If you set `autospec=True` + then the mock with be created with a spec from the object being replaced. + All attributes of the mock will also have the spec of the corresponding + attribute of the object being replaced. Methods and functions being + mocked will have their arguments checked and will raise a `TypeError` if + they are called with the wrong signature. For mocks replacing a class, + their return value (the 'instance') will have the same spec as the class. + + Instead of `autospec=True` you can pass `autospec=some_object` to use an + arbitrary object as the spec instead of the one being replaced. + + By default `patch` will fail to replace attributes that don't exist. If + you pass in `create=True`, and the attribute doesn't exist, patch will + create the attribute for you when the patched function is called, and + delete it again afterwards. This is useful for writing tests against + attributes that your production code creates at runtime. It is off by by + default because it can be dangerous. With it switched on you can write + passing tests against APIs that don't actually exist! + + Patch can be used as a `TestCase` class decorator. It works by + decorating each test method in the class. This reduces the boilerplate + code when your test methods share a common patchings set. `patch` finds + tests by looking for method names that start with `patch.TEST_PREFIX`. + By default this is `test`, which matches the way `unittest` finds tests. + You can specify an alternative prefix by setting `patch.TEST_PREFIX`. + + Patch can be used as a context manager, with the with statement. Here the + patching applies to the indented block after the with statement. If you + use "as" then the patched object will be bound to the name after the + "as"; very useful if `patch` is creating a mock object for you. + + `patch` takes arbitrary keyword arguments. These will be passed to + the `Mock` (or `new_callable`) on construction. + + `patch.dict(...)`, `patch.multiple(...)` and `patch.object(...)` are + available for alternate use-cases. + """ + getter, attribute = _get_target(target) + return _patch( + getter, attribute, new, spec, create, + spec_set, autospec, new_callable, kwargs + ) + + +class _patch_dict(object): + """ + Patch a dictionary, or dictionary like object, and restore the dictionary + to its original state after the test. + + `in_dict` can be a dictionary or a mapping like container. If it is a + mapping then it must at least support getting, setting and deleting items + plus iterating over keys. + + `in_dict` can also be a string specifying the name of the dictionary, which + will then be fetched by importing it. + + `values` can be a dictionary of values to set in the dictionary. `values` + can also be an iterable of `(key, value)` pairs. + + If `clear` is True then the dictionary will be cleared before the new + values are set. + + `patch.dict` can also be called with arbitrary keyword arguments to set + values in the dictionary:: + + with patch.dict('sys.modules', mymodule=Mock(), other_module=Mock()): + ... + + `patch.dict` can be used as a context manager, decorator or class + decorator. When used as a class decorator `patch.dict` honours + `patch.TEST_PREFIX` for choosing which methods to wrap. + """ + + def __init__(self, in_dict, values=(), clear=False, **kwargs): + if isinstance(in_dict, basestring): + in_dict = _importer(in_dict) + self.in_dict = in_dict + # support any argument supported by dict(...) constructor + self.values = dict(values) + self.values.update(kwargs) + self.clear = clear + self._original = None + + + def __call__(self, f): + if isinstance(f, ClassTypes): + return self.decorate_class(f) + @wraps(f) + def _inner(*args, **kw): + self._patch_dict() + try: + return f(*args, **kw) + finally: + self._unpatch_dict() + + return _inner + + + def decorate_class(self, klass): + for attr in dir(klass): + attr_value = getattr(klass, attr) + if (attr.startswith(patch.TEST_PREFIX) and + hasattr(attr_value, "__call__")): + decorator = _patch_dict(self.in_dict, self.values, self.clear) + decorated = decorator(attr_value) + setattr(klass, attr, decorated) + return klass + + + def __enter__(self): + """Patch the dict.""" + self._patch_dict() + + + def _patch_dict(self): + values = self.values + in_dict = self.in_dict + clear = self.clear + + try: + original = in_dict.copy() + except AttributeError: + # dict like object with no copy method + # must support iteration over keys + original = {} + for key in in_dict: + original[key] = in_dict[key] + self._original = original + + if clear: + _clear_dict(in_dict) + + try: + in_dict.update(values) + except AttributeError: + # dict like object with no update method + for key in values: + in_dict[key] = values[key] + + + def _unpatch_dict(self): + in_dict = self.in_dict + original = self._original + + _clear_dict(in_dict) + + try: + in_dict.update(original) + except AttributeError: + for key in original: + in_dict[key] = original[key] + + + def __exit__(self, *args): + """Unpatch the dict.""" + self._unpatch_dict() + return False + + start = __enter__ + stop = __exit__ + + +def _clear_dict(in_dict): + try: + in_dict.clear() + except AttributeError: + keys = list(in_dict) + for key in keys: + del in_dict[key] + + +def _patch_stopall(): + """Stop all active patches.""" + for patch in list(_patch._active_patches): + patch.stop() + + +patch.object = _patch_object +patch.dict = _patch_dict +patch.multiple = _patch_multiple +patch.stopall = _patch_stopall +patch.TEST_PREFIX = 'test' + +magic_methods = ( + "lt le gt ge eq ne " + "getitem setitem delitem " + "len contains iter " + "hash str sizeof " + "enter exit " + "divmod neg pos abs invert " + "complex int float index " + "trunc floor ceil " +) + +numerics = "add sub mul div floordiv mod lshift rshift and xor or pow " +inplace = ' '.join('i%s' % n for n in numerics.split()) +right = ' '.join('r%s' % n for n in numerics.split()) +extra = '' +if inPy3k: + extra = 'bool next ' +else: + extra = 'unicode long nonzero oct hex truediv rtruediv ' + +# not including __prepare__, __instancecheck__, __subclasscheck__ +# (as they are metaclass methods) +# __del__ is not supported at all as it causes problems if it exists + +_non_defaults = set('__%s__' % method for method in [ + 'cmp', 'getslice', 'setslice', 'coerce', 'subclasses', + 'format', 'get', 'set', 'delete', 'reversed', + 'missing', 'reduce', 'reduce_ex', 'getinitargs', + 'getnewargs', 'getstate', 'setstate', 'getformat', + 'setformat', 'repr', 'dir' +]) + + +def _get_method(name, func): + "Turns a callable object (like a mock) into a real function" + def method(self, *args, **kw): + return func(self, *args, **kw) + method.__name__ = name + return method + + +_magics = set( + '__%s__' % method for method in + ' '.join([magic_methods, numerics, inplace, right, extra]).split() +) + +_all_magics = _magics | _non_defaults + +_unsupported_magics = set([ + '__getattr__', '__setattr__', + '__init__', '__new__', '__prepare__' + '__instancecheck__', '__subclasscheck__', + '__del__' +]) + +_calculate_return_value = { + '__hash__': lambda self: object.__hash__(self), + '__str__': lambda self: object.__str__(self), + '__sizeof__': lambda self: object.__sizeof__(self), + '__unicode__': lambda self: unicode(object.__str__(self)), +} + +_return_values = { + '__lt__': NotImplemented, + '__gt__': NotImplemented, + '__le__': NotImplemented, + '__ge__': NotImplemented, + '__int__': 1, + '__contains__': False, + '__len__': 0, + '__exit__': False, + '__complex__': 1j, + '__float__': 1.0, + '__bool__': True, + '__nonzero__': True, + '__oct__': '1', + '__hex__': '0x1', + '__long__': long(1), + '__index__': 1, +} + + +def _get_eq(self): + def __eq__(other): + ret_val = self.__eq__._mock_return_value + if ret_val is not DEFAULT: + return ret_val + return self is other + return __eq__ + +def _get_ne(self): + def __ne__(other): + if self.__ne__._mock_return_value is not DEFAULT: + return DEFAULT + return self is not other + return __ne__ + +def _get_iter(self): + def __iter__(): + ret_val = self.__iter__._mock_return_value + if ret_val is DEFAULT: + return iter([]) + # if ret_val was already an iterator, then calling iter on it should + # return the iterator unchanged + return iter(ret_val) + return __iter__ + +_side_effect_methods = { + '__eq__': _get_eq, + '__ne__': _get_ne, + '__iter__': _get_iter, +} + + + +def _set_return_value(mock, method, name): + fixed = _return_values.get(name, DEFAULT) + if fixed is not DEFAULT: + method.return_value = fixed + return + + return_calulator = _calculate_return_value.get(name) + if return_calulator is not None: + try: + return_value = return_calulator(mock) + except AttributeError: + # XXXX why do we return AttributeError here? + # set it as a side_effect instead? + return_value = AttributeError(name) + method.return_value = return_value + return + + side_effector = _side_effect_methods.get(name) + if side_effector is not None: + method.side_effect = side_effector(mock) + + + +class MagicMixin(object): + def __init__(self, *args, **kw): + _super(MagicMixin, self).__init__(*args, **kw) + self._mock_set_magics() + + + def _mock_set_magics(self): + these_magics = _magics + + if self._mock_methods is not None: + these_magics = _magics.intersection(self._mock_methods) + + remove_magics = set() + remove_magics = _magics - these_magics + + for entry in remove_magics: + if entry in type(self).__dict__: + # remove unneeded magic methods + delattr(self, entry) + + # don't overwrite existing attributes if called a second time + these_magics = these_magics - set(type(self).__dict__) + + _type = type(self) + for entry in these_magics: + setattr(_type, entry, MagicProxy(entry, self)) + + + +class NonCallableMagicMock(MagicMixin, NonCallableMock): + """A version of `MagicMock` that isn't callable.""" + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + self._mock_set_magics() + + + +class MagicMock(MagicMixin, Mock): + """ + MagicMock is a subclass of Mock with default implementations + of most of the magic methods. You can use MagicMock without having to + configure the magic methods yourself. + + If you use the `spec` or `spec_set` arguments then *only* magic + methods that exist in the spec will be created. + + Attributes and the return value of a `MagicMock` will also be `MagicMocks`. + """ + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + self._mock_set_magics() + + + +class MagicProxy(object): + def __init__(self, name, parent): + self.name = name + self.parent = parent + + def __call__(self, *args, **kwargs): + m = self.create_mock() + return m(*args, **kwargs) + + def create_mock(self): + entry = self.name + parent = self.parent + m = parent._get_child_mock(name=entry, _new_name=entry, + _new_parent=parent) + setattr(parent, entry, m) + _set_return_value(parent, m, entry) + return m + + def __get__(self, obj, _type=None): + return self.create_mock() + + + +class _ANY(object): + "A helper object that compares equal to everything." + + def __eq__(self, other): + return True + + def __ne__(self, other): + return False + + def __repr__(self): + return '' + +ANY = _ANY() + + + +def _format_call_signature(name, args, kwargs): + message = '%s(%%s)' % name + formatted_args = '' + args_string = ', '.join([repr(arg) for arg in args]) + kwargs_string = ', '.join([ + '%s=%r' % (key, value) for key, value in kwargs.items() + ]) + if args_string: + formatted_args = args_string + if kwargs_string: + if formatted_args: + formatted_args += ', ' + formatted_args += kwargs_string + + return message % formatted_args + + + +class _Call(tuple): + """ + A tuple for holding the results of a call to a mock, either in the form + `(args, kwargs)` or `(name, args, kwargs)`. + + If args or kwargs are empty then a call tuple will compare equal to + a tuple without those values. This makes comparisons less verbose:: + + _Call(('name', (), {})) == ('name',) + _Call(('name', (1,), {})) == ('name', (1,)) + _Call(((), {'a': 'b'})) == ({'a': 'b'},) + + The `_Call` object provides a useful shortcut for comparing with call:: + + _Call(((1, 2), {'a': 3})) == call(1, 2, a=3) + _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3) + + If the _Call has no name then it will match any name. + """ + def __new__(cls, value=(), name=None, parent=None, two=False, + from_kall=True): + name = '' + args = () + kwargs = {} + _len = len(value) + if _len == 3: + name, args, kwargs = value + elif _len == 2: + first, second = value + if isinstance(first, basestring): + name = first + if isinstance(second, tuple): + args = second + else: + kwargs = second + else: + args, kwargs = first, second + elif _len == 1: + value, = value + if isinstance(value, basestring): + name = value + elif isinstance(value, tuple): + args = value + else: + kwargs = value + + if two: + return tuple.__new__(cls, (args, kwargs)) + + return tuple.__new__(cls, (name, args, kwargs)) + + + def __init__(self, value=(), name=None, parent=None, two=False, + from_kall=True): + self.name = name + self.parent = parent + self.from_kall = from_kall + + + def __eq__(self, other): + if other is ANY: + return True + try: + len_other = len(other) + except TypeError: + return False + + self_name = '' + if len(self) == 2: + self_args, self_kwargs = self + else: + self_name, self_args, self_kwargs = self + + other_name = '' + if len_other == 0: + other_args, other_kwargs = (), {} + elif len_other == 3: + other_name, other_args, other_kwargs = other + elif len_other == 1: + value, = other + if isinstance(value, tuple): + other_args = value + other_kwargs = {} + elif isinstance(value, basestring): + other_name = value + other_args, other_kwargs = (), {} + else: + other_args = () + other_kwargs = value + else: + # len 2 + # could be (name, args) or (name, kwargs) or (args, kwargs) + first, second = other + if isinstance(first, basestring): + other_name = first + if isinstance(second, tuple): + other_args, other_kwargs = second, {} + else: + other_args, other_kwargs = (), second + else: + other_args, other_kwargs = first, second + + if self_name and other_name != self_name: + return False + + # this order is important for ANY to work! + return (other_args, other_kwargs) == (self_args, self_kwargs) + + + def __ne__(self, other): + return not self.__eq__(other) + + + def __call__(self, *args, **kwargs): + if self.name is None: + return _Call(('', args, kwargs), name='()') + + name = self.name + '()' + return _Call((self.name, args, kwargs), name=name, parent=self) + + + def __getattr__(self, attr): + if self.name is None: + return _Call(name=attr, from_kall=False) + name = '%s.%s' % (self.name, attr) + return _Call(name=name, parent=self, from_kall=False) + + + def __repr__(self): + if not self.from_kall: + name = self.name or 'call' + if name.startswith('()'): + name = 'call%s' % name + return name + + if len(self) == 2: + name = 'call' + args, kwargs = self + else: + name, args, kwargs = self + if not name: + name = 'call' + elif not name.startswith('()'): + name = 'call.%s' % name + else: + name = 'call%s' % name + return _format_call_signature(name, args, kwargs) + + + def call_list(self): + """For a call object that represents multiple calls, `call_list` + returns a list of all the intermediate calls as well as the + final call.""" + vals = [] + thing = self + while thing is not None: + if thing.from_kall: + vals.append(thing) + thing = thing.parent + return _CallList(reversed(vals)) + + +call = _Call(from_kall=False) + + + +def create_autospec(spec, spec_set=False, instance=False, _parent=None, + _name=None, **kwargs): + """Create a mock object using another object as a spec. Attributes on the + mock will use the corresponding attribute on the `spec` object as their + spec. + + Functions or methods being mocked will have their arguments checked + to check that they are called with the correct signature. + + If `spec_set` is True then attempting to set attributes that don't exist + on the spec object will raise an `AttributeError`. + + If a class is used as a spec then the return value of the mock (the + instance of the class) will have the same spec. You can use a class as the + spec for an instance object by passing `instance=True`. The returned mock + will only be callable if instances of the mock are callable. + + `create_autospec` also takes arbitrary keyword arguments that are passed to + the constructor of the created mock.""" + if _is_list(spec): + # can't pass a list instance to the mock constructor as it will be + # interpreted as a list of strings + spec = type(spec) + + is_type = isinstance(spec, ClassTypes) + + _kwargs = {'spec': spec} + if spec_set: + _kwargs = {'spec_set': spec} + elif spec is None: + # None we mock with a normal mock without a spec + _kwargs = {} + + _kwargs.update(kwargs) + + Klass = MagicMock + if type(spec) in DescriptorTypes: + # descriptors don't have a spec + # because we don't know what type they return + _kwargs = {} + elif not _callable(spec): + Klass = NonCallableMagicMock + elif is_type and instance and not _instance_callable(spec): + Klass = NonCallableMagicMock + + _new_name = _name + if _parent is None: + # for a top level object no _new_name should be set + _new_name = '' + + mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name, + name=_name, **_kwargs) + + if isinstance(spec, FunctionTypes): + # should only happen at the top level because we don't + # recurse for functions + mock = _set_signature(mock, spec) + else: + _check_signature(spec, mock, is_type, instance) + + if _parent is not None and not instance: + _parent._mock_children[_name] = mock + + if is_type and not instance and 'return_value' not in kwargs: + mock.return_value = create_autospec(spec, spec_set, instance=True, + _name='()', _parent=mock) + + for entry in dir(spec): + if _is_magic(entry): + # MagicMock already does the useful magic methods for us + continue + + if isinstance(spec, FunctionTypes) and entry in FunctionAttributes: + # allow a mock to actually be a function + continue + + # XXXX do we need a better way of getting attributes without + # triggering code execution (?) Probably not - we need the actual + # object to mock it so we would rather trigger a property than mock + # the property descriptor. Likewise we want to mock out dynamically + # provided attributes. + # XXXX what about attributes that raise exceptions other than + # AttributeError on being fetched? + # we could be resilient against it, or catch and propagate the + # exception when the attribute is fetched from the mock + try: + original = getattr(spec, entry) + except AttributeError: + continue + + kwargs = {'spec': original} + if spec_set: + kwargs = {'spec_set': original} + + if not isinstance(original, FunctionTypes): + new = _SpecState(original, spec_set, mock, entry, instance) + mock._mock_children[entry] = new + else: + parent = mock + if isinstance(spec, FunctionTypes): + parent = mock.mock + + new = MagicMock(parent=parent, name=entry, _new_name=entry, + _new_parent=parent, **kwargs) + mock._mock_children[entry] = new + skipfirst = _must_skip(spec, entry, is_type) + _check_signature(original, new, skipfirst=skipfirst) + + # so functions created with _set_signature become instance attributes, + # *plus* their underlying mock exists in _mock_children of the parent + # mock. Adding to _mock_children may be unnecessary where we are also + # setting as an instance attribute? + if isinstance(new, FunctionTypes): + setattr(mock, entry, new) + + return mock + + +def _must_skip(spec, entry, is_type): + if not isinstance(spec, ClassTypes): + if entry in getattr(spec, '__dict__', {}): + # instance attribute - shouldn't skip + return False + spec = spec.__class__ + if not hasattr(spec, '__mro__'): + # old style class: can't have descriptors anyway + return is_type + + for klass in spec.__mro__: + result = klass.__dict__.get(entry, DEFAULT) + if result is DEFAULT: + continue + if isinstance(result, (staticmethod, classmethod)): + return False + return is_type + + # shouldn't get here unless function is a dynamically provided attribute + # XXXX untested behaviour + return is_type + + +def _get_class(obj): + try: + return obj.__class__ + except AttributeError: + # in Python 2, _sre.SRE_Pattern objects have no __class__ + return type(obj) + + +class _SpecState(object): + + def __init__(self, spec, spec_set=False, parent=None, + name=None, ids=None, instance=False): + self.spec = spec + self.ids = ids + self.spec_set = spec_set + self.parent = parent + self.instance = instance + self.name = name + + +FunctionTypes = ( + # python function + type(create_autospec), + # instance method + type(ANY.__eq__), + # unbound method + type(_ANY.__eq__), +) + +FunctionAttributes = set([ + 'func_closure', + 'func_code', + 'func_defaults', + 'func_dict', + 'func_doc', + 'func_globals', + 'func_name', +]) + + +file_spec = None + + +def mock_open(mock=None, read_data=''): + """ + A helper function to create a mock to replace the use of `open`. It works + for `open` called directly or used as a context manager. + + The `mock` argument is the mock object to configure. If `None` (the + default) then a `MagicMock` will be created for you, with the API limited + to methods or attributes available on standard file handles. + + `read_data` is a string for the `read` method of the file handle to return. + This is an empty string by default. + """ + global file_spec + if file_spec is None: + # set on first use + if inPy3k: + import _io + file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) + else: + file_spec = file + + if mock is None: + mock = MagicMock(name='open', spec=open) + + handle = MagicMock(spec=file_spec) + handle.write.return_value = None + handle.__enter__.return_value = handle + handle.read.return_value = read_data + + mock.return_value = handle + return mock + + +class PropertyMock(Mock): + """ + A mock intended to be used as a property, or other descriptor, on a class. + `PropertyMock` provides `__get__` and `__set__` methods so you can specify + a return value when it is fetched. + + Fetching a `PropertyMock` instance from an object calls the mock, with + no args. Setting it calls the mock with the value being set. + """ + def _get_child_mock(self, **kwargs): + return MagicMock(**kwargs) + + def __get__(self, obj, obj_type): + return self() + def __set__(self, obj, val): + self(val) + From f78541e9d7ccd3a14935be422b27d77d756b7129 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 13:53:48 +0000 Subject: [PATCH 056/197] Moved mock into docs --- mock.py => doc/mock.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mock.py => doc/mock.py (100%) diff --git a/mock.py b/doc/mock.py similarity index 100% rename from mock.py rename to doc/mock.py From 4537be65d88ab733ed4485af8d3f42e39b9e4f30 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:15:49 +0000 Subject: [PATCH 057/197] Moved import a bit --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index d32c77f7..a588990b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -147,8 +147,8 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: sys.path.append("../GPy") os.system("pwd") - #os.system("cd ..") os.system("sphinx-apidoc -f -o . ../GPy") + #os.system("cd ..") #os.system("cd ./docs") # Add any paths that contain templates here, relative to this directory. From 1911f85c4b747c11dd78a47a32b86de3b973dd44 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:18:00 +0000 Subject: [PATCH 058/197] Added path higher --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index a588990b..d8344fbb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -111,6 +111,7 @@ import sys, os #else: #return Mock() +sys.path.append("../GPy") import mock MOCK_MODULES = ['pylab', 'matplotlib']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] @@ -120,7 +121,7 @@ for mod_name in MOCK_MODULES: # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it From 00240a7da025313b7f44fe0c5b88ed2603875d66 Mon Sep 17 00:00:00 2001 From: Nicolo Fusi Date: Thu, 31 Jan 2013 14:37:39 +0000 Subject: [PATCH 059/197] "fixed" Tango imports --- GPy/util/Tango.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/GPy/util/Tango.py b/GPy/util/Tango.py index d2a16fdf..8035ffe6 100644 --- a/GPy/util/Tango.py +++ b/GPy/util/Tango.py @@ -3,7 +3,6 @@ import matplotlib as mpl - import pylab as pb import sys #sys.path.append('/home/james/mlprojects/sitran_cluster/') @@ -15,12 +14,12 @@ def removeRightTicks(ax=None): ax = ax or pb.gca() for i, line in enumerate(ax.get_yticklines()): if i%2 == 1: # odd indices - line.set_visible(False) + line.set_visible(False) def removeUpperTicks(ax=None): ax = ax or pb.gca() for i, line in enumerate(ax.get_xticklines()): if i%2 == 1: # odd indices - line.set_visible(False) + line.set_visible(False) def fewerXticks(ax=None,divideby=2): ax = ax or pb.gca() ax.set_xticks(ax.get_xticks()[::divideby]) @@ -126,8 +125,6 @@ cdict_RB = {'red' :((0.,coloursRGB['mediumRed'][0]/256.,coloursRGB['mediumRed'][ 'blue':((0.,coloursRGB['mediumRed'][2]/256.,coloursRGB['mediumRed'][2]/256.), (.5,coloursRGB['mediumPurple'][2]/256.,coloursRGB['mediumPurple'][2]/256.), (1.,coloursRGB['mediumBlue'][2]/256.,coloursRGB['mediumBlue'][2]/256.))} -cmap_RB = mpl.colors.LinearSegmentedColormap('TangoRedBlue',cdict_RB,256) - cdict_BGR = {'red' :((0.,coloursRGB['mediumBlue'][0]/256.,coloursRGB['mediumBlue'][0]/256.), (.5,coloursRGB['mediumGreen'][0]/256.,coloursRGB['mediumGreen'][0]/256.), @@ -138,7 +135,7 @@ cdict_BGR = {'red' :((0.,coloursRGB['mediumBlue'][0]/256.,coloursRGB['mediumBlue 'blue':((0.,coloursRGB['mediumBlue'][2]/256.,coloursRGB['mediumBlue'][2]/256.), (.5,coloursRGB['mediumGreen'][2]/256.,coloursRGB['mediumGreen'][2]/256.), (1.,coloursRGB['mediumRed'][2]/256.,coloursRGB['mediumRed'][2]/256.))} -cmap_BGR = mpl.colors.LinearSegmentedColormap('TangoRedBlue',cdict_BGR,256) + cdict_Alu = {'red' :((0./5,coloursRGB['Aluminium1'][0]/256.,coloursRGB['Aluminium1'][0]/256.), (1./5,coloursRGB['Aluminium2'][0]/256.,coloursRGB['Aluminium2'][0]/256.), @@ -158,13 +155,12 @@ cdict_Alu = {'red' :((0./5,coloursRGB['Aluminium1'][0]/256.,coloursRGB['Aluminiu (3./5,coloursRGB['Aluminium4'][2]/256.,coloursRGB['Aluminium4'][2]/256.), (4./5,coloursRGB['Aluminium5'][2]/256.,coloursRGB['Aluminium5'][2]/256.), (5./5,coloursRGB['Aluminium6'][2]/256.,coloursRGB['Aluminium6'][2]/256.))} -cmap_Alu = mpl.colors.LinearSegmentedColormap('TangoAluminium',cdict_Alu,256) - +# cmap_Alu = mpl.colors.LinearSegmentedColormap('TangoAluminium',cdict_Alu,256) +# cmap_BGR = mpl.colors.LinearSegmentedColormap('TangoRedBlue',cdict_BGR,256) +# cmap_RB = mpl.colors.LinearSegmentedColormap('TangoRedBlue',cdict_RB,256) if __name__=='__main__': import pylab as pb pb.figure() pb.pcolor(pb.rand(10,10),cmap=cmap_RB) pb.colorbar() pb.show() - - From 00fa1d64e5bf92861356381f4981ccf5ed58ac12 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:39:53 +0000 Subject: [PATCH 060/197] Changed mock back --- doc/conf.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index d8344fbb..0166afa9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -93,26 +93,26 @@ import sys, os ############################################################################## ## ## Mock out imports with C dependencies because ReadTheDocs can't build them. -#class Mock(object): - #def __init__(self, *args, **kwargs): - #pass +class Mock(object): + def __init__(self, *args, **kwargs): + pass - #def __call__(self, *args, **kwargs): - #return Mock() + def __call__(self, *args, **kwargs): + return Mock() - #@classmethod - #def __getattr__(cls, name): - #if name in ('__file__', '__path__'): - #return '/dev/null' - #elif name[0] == name[0].upper(): - #mockType = type(name, (), {}) - #mockType.__module__ = __name__ - #return mockType - #else: - #return Mock() + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() -sys.path.append("../GPy") -import mock +#sys.path.append("../GPy") +#import mock MOCK_MODULES = ['pylab', 'matplotlib']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: @@ -121,7 +121,7 @@ for mod_name in MOCK_MODULES: # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +#sys.path.insert(0, os.path.abspath('..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it From 93f5e1c42b816b4bb77e94b14b98c246cae203bf Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:40:32 +0000 Subject: [PATCH 061/197] Changed mock --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 0166afa9..3a1350bb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -116,7 +116,7 @@ class Mock(object): MOCK_MODULES = ['pylab', 'matplotlib']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = mock.Mock() + sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From 92ec84133de77c40e83cea1a3aa8a8894d7f6f54 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:41:36 +0000 Subject: [PATCH 062/197] Mocked sympy aswell... --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 3a1350bb..ee5c98ea 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From e2a64c60d0ee159316c011ce439b8db60f019063 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:43:06 +0000 Subject: [PATCH 063/197] Mocked sympy.utilities --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index ee5c98ea..74d2e527 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 9feae765dc2253edaa37b25e3417a364e5b9acdc Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Thu, 31 Jan 2013 14:43:32 +0000 Subject: [PATCH 064/197] predictive_mean changed to predictive_values --- GPy/likelihoods/likelihood_functions.py | 84 +++++++------------------ GPy/models/GP.py | 7 +-- 2 files changed, 25 insertions(+), 66 deletions(-) diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 1387c53d..49547b88 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -19,35 +19,6 @@ class likelihood: self.location = location self.scale = scale - def plot2D(self,X,X_new,F_new,U=None): - """ - Predictive distribution of the fitted GP model for 2-dimensional inputs - - :param X_new: The points at which to make a prediction - :param Mean_new: mean values at X_new - :param Var_new: variance values at X_new - :param X_u: input points used to train the model - :param Mean_u: mean values at X_u - :param Var_new: variance values at X_u - """ - N,D = X_new.shape - assert D == 2, 'Number of dimensions must be 2' - n = np.sqrt(N) - x1min = X_new[:,0].min() - x1max = X_new[:,0].max() - x2min = X_new[:,1].min() - x2max = X_new[:,1].max() - pb.imshow(F_new.reshape(n,n),extent=(x1min,x1max,x2max,x2min),vmin=0,vmax=1) - pb.colorbar() - C1 = np.arange(self.N)[self.Y.flatten()==1] - C2 = np.arange(self.N)[self.Y.flatten()==-1] - [pb.plot(X[i,0],X[i,1],'ro') for i in C1] - [pb.plot(X[i,0],X[i,1],'bo') for i in C2] - pb.xlim(x1min,x1max) - pb.ylim(x2min,x2max) - if U is not None: - [pb.plot(a,b,'wo') for a,b in U] - class probit(likelihood): """ Probit likelihood @@ -76,32 +47,23 @@ class probit(likelihood): sigma2_hat = 1./tau_i - (phi/((tau_i**2+tau_i)*Z_hat))*(z+phi/Z_hat) return Z_hat, mu_hat, sigma2_hat - def predictive_mean(self,mu,var): + def predictive_values(self,mu,var,all=False): + """ + Compute mean, variance, and conficence interval (percentiles 5 and 95) of the prediction + """ mu = mu.flatten() var = var.flatten() - return stats.norm.cdf(mu/np.sqrt(1+var)) - - def predictive_quantiles(self,mu,var): - #p=self.predictive_mean(mu,var) - #return p*(1-p) - raise NotImplementedError #TODO + mean = stats.norm.cdf(mu/np.sqrt(1+var)) + if all: + p_05 = np.zeros([mu.size]) + p_95 = np.ones([mu.size]) + return mean, mean*(1-mean),p_05,p_95 + else: + return mean def _log_likelihood_gradients(): return np.zeros(0) # there are no parameters of whcih to compute the gradients - def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): - #TODO: remove me - assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - phi_var = self.predictive_var(mu,var) - gpplot(X,phi,phi_var) - if samples: - phi_samples = np.vstack([np.random.binomial(1,phi.flatten()) for s in range(samples)]) - pb.plot(X,phi_samples.T,'x', alpha = 0.4, c='#3465a4' ) - pb.plot(X_obs,(self.Y+1)/2,'kx',mew=1.5) - if Z is not None: - pb.plot(Z,Z*0+.5,'r|',mew=1.5,markersize=12) - pb.ylim(-0.2,1.2) - class poisson(likelihood): """ Poisson likelihood @@ -172,11 +134,18 @@ class poisson(likelihood): sigma2_hat = m2 - mu_hat**2 # Second central moment return float(Z_hat), float(mu_hat), float(sigma2_hat) - def predictive_mean(self,mu,var): - return np.exp(mu*self.scale + self.location) - - def predictive_var(self,mu,var): - return predictive_mean(mu,var) + def predictive_values(self,mu,var,all=False): + """ + Compute mean, variance, and conficence interval (percentiles 5 and 95) of the prediction + """ + mean = np.exp(mu*self.scale + self.location) + if all: + tmp = stats.poisson.ppf(np.array([.05,.95]),mu) + p_05 = tmp[:,0] + p_95 = tmp[:,1] + return mean,mean,p_05,p_95 + else: + return mean def _log_likelihood_gradients(): raise NotImplementedError @@ -212,13 +181,6 @@ class gaussian(likelihood): Z_hat = 1./np.sqrt(2*np.pi) * 1./np.sqrt(sigma**2+s**2) * np.exp(-.5*(mu-self.Y[i])**2/(sigma**2 + s**2)) return Z_hat, mu_hat, sigma2_hat - def plot1Db(self,X,X_new,F_new,U=None): - assert X.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X_new,F_new,np.zeros(X_new.shape[0])) - pb.plot(X,self.Y,'kx',mew=1.5) - if U is not None: - pb.plot(U,np.ones(U.shape[0])*self.Y.min()*.8,'r|',mew=1.5,markersize=12) - def _log_likelihood_gradients(): raise NotImplementedError else: diff --git a/GPy/models/GP.py b/GPy/models/GP.py index f5a0711d..dfd22d9c 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -215,11 +215,10 @@ class GP(model): if self.X.shape[1]==1: Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - m,v,phi = self.predict(Xnew,slices=which_functions,full_cov=full_cov) + m,v = self.predict(Xnew,slices=which_functions,full_cov=full_cov) if self.EP: pb.subplot(211) gpplot(Xnew,m,v) - if samples: #NOTE why don't we put samples as a parameter of gpplot s = np.random.multivariate_normal(m.flatten(),np.diag(v.flatten()),samples) pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) @@ -227,9 +226,7 @@ class GP(model): pb.xlim(xmin,xmax) if self.EP: - pb.subplot(212) - self.likelihood.plot(Xnew,m,v,phi,self.X,samples=samples) - pb.xlim(xmin,xmax) + phi_m, phi_v, phi_l, phi_u = self.likelihood.predictive_values(m,v) elif self.X.shape[1]==2: resolution = 50 or resolution From f27d4aa81e93c20156aac22038a788c07f6d3894 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:44:13 +0000 Subject: [PATCH 065/197] sympy.utilities.codegen --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 74d2e527..9c3b679f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 9a6228b31487dc3e0f5860d41023ae7621443915 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:46:15 +0000 Subject: [PATCH 066/197] Added sympy.core.cache mock...... --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 9c3b679f..09b9c3fb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 7ad5b9d4d968019a4ea331587301b872606d9941 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:51:50 +0000 Subject: [PATCH 067/197] Added sympy.core --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 09b9c3fb..3a824b70 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From eec133c30198c27f8be727b4689674fa6c41b6f6 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:52:46 +0000 Subject: [PATCH 068/197] added sympy.parsing --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 3a824b70..94798ee1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From 8498332f7658a21d68773760be31c82d82e6f510 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 14:53:41 +0000 Subject: [PATCH 069/197] asf --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 94798ee1..54135023 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() From d077d28fd1667645f2776d96e2b7914964263821 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Thu, 31 Jan 2013 15:02:34 +0000 Subject: [PATCH 070/197] very basic functionality is now working --- GPy/__init__.py | 2 +- GPy/likelihoods/EP.py | 10 +- GPy/likelihoods/Gaussian.py | 37 ++++- GPy/likelihoods/__init__.py | 1 + GPy/likelihoods/likelihood_functions.py | 11 +- GPy/models/GP.py | 70 ++++---- GPy/models/GP_regression.py | 209 +----------------------- GPy/models/__init__.py | 14 +- GPy/models/sparse_GP.py | 2 - GPy/util/plot.py | 16 +- 10 files changed, 88 insertions(+), 284 deletions(-) diff --git a/GPy/__init__.py b/GPy/__init__.py index 381d6232..c0772c27 100644 --- a/GPy/__init__.py +++ b/GPy/__init__.py @@ -7,5 +7,5 @@ import models import inference import util import examples -#import examples TODO: discuss! from core import priors +import likelihoods diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index 1519bf3b..3e975436 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -1,12 +1,9 @@ import numpy as np import random -import pylab as pb #TODO erase me from scipy import stats, linalg -from .likelihoods import likelihood from ..core import model from ..util.linalg import pdinv,mdot,jitchol from ..util.plot import gpplot -from .. import kern class EP: def __init__(self,data,likelihood_function,epsilon=1e-3,power_ep=[1.,1.]): @@ -15,12 +12,8 @@ class EP: Arguments --------- - X : input observations - likelihood : Output's likelihood (likelihood class) - kernel : a GPy kernel (kern class) - inducing : Either an array specifying the inducing points location or a sacalar defining their number. None value for using a non-sparse model is used. - power_ep : Power-EP parameters (eta,delta) - 2x1 numpy array (floats) epsilon : Convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) + likelihood_function : a likelihood function (see likelihood_functions.py) """ self.likelihood_function = likelihood_function self.epsilon = epsilon @@ -48,7 +41,6 @@ class EP: For nomenclature see Rasmussen & Williams 2006. """ #Prior distribution parameters: p(f|X) = N(f|0,K) - #self.K = self.kernel.K(self.X,self.X) #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) self.mu = np.zeros(self.N) diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 2397ce38..fe954b78 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -1,16 +1,39 @@ import numpy as np class Gaussian: - def __init__(self,data,variance=1.,normalise=False): + def __init__(self,data,variance=1.,normalize=False): self.data = data - if normalise: - foo - self._variance = variance + self.N,D = data.shape + self.Z = 0. # a correction factor which accounts for the approximation made + + #normalisation + if normalize: + self._mean = data.mean(0)[None,:] + self._std = data.std(0)[None,:] + self.Y = (self.data - self._mean)/self._std + else: + self._mean = np.zeros((1,D)) + self._std = np.ones((1,D)) + self.Y = self.data + + self.YYT = np.dot(self.Y,self.Y.T) + self._set_params(np.asarray(variance)) + def _get_params(self): - return np.asarray(self.variance) + return np.asarray(self._variance) + + def _get_param_names(self): + return ["noise variance"] + def _set_params(self,x): self._variance = x + self.variance = np.eye(self.N)*self._variance + def fit(self): + """ + No approximations needed + """ pass - def _gradients(self,foo): - return bar(foo) + + def _gradients(self,partial): + return np.sum(np.diag(partial)) diff --git a/GPy/likelihoods/__init__.py b/GPy/likelihoods/__init__.py index d1369c43..83413255 100644 --- a/GPy/likelihoods/__init__.py +++ b/GPy/likelihoods/__init__.py @@ -1,3 +1,4 @@ from EP import EP from Gaussian import Gaussian # TODO: from Laplace import Laplace +import likelihood_functions as functions diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 1387c53d..7e6a5ba1 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -192,10 +192,10 @@ class poisson(likelihood): pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) class gaussian(likelihood): - """ - Gaussian likelihood - Y is expected to take values in (-inf,inf) - """ + """ + Gaussian likelihood + Y is expected to take values in (-inf,inf) + """ def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -221,6 +221,3 @@ class gaussian(likelihood): def _log_likelihood_gradients(): raise NotImplementedError - else: - var = var[:,None] * np.square(self._Ystd) - diff --git a/GPy/models/GP.py b/GPy/models/GP.py index f5a0711d..827b94b7 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -8,8 +8,6 @@ from .. import kern from ..core import model from ..util.linalg import pdinv,mdot from ..util.plot import gpplot, Tango -from ..inference.EP import Full # TODO: tidy -from ..inference import likelihoods class GP(model): """ @@ -55,8 +53,6 @@ class GP(model): self._Xstd = np.ones((1,self.X.shape[1])) self.likelihood = likelihood - self.Y = self.likelihood.Y - self.YYT = self.likelihood.YYT # TODO: this is ugly. what about sufficient_stats? assert self.X.shape[0] == self.likelihood.Y.shape[0] self.N, self.D = self.likelihood.Y.shape @@ -67,18 +63,16 @@ class GP(model): self.likelihood._set_params(p[self.kern.Nparam:]) self.K = self.kern.K(self.X,slices1=self.Xslices) - self.K += np.diag(self.likelihood_variance) + self.K += self.likelihood.variance self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) #the gradient of the likelihood wrt the covariance matrix - if self.YYT is None: - self._alpha = np.dot(self.Ki,self.Y) - self._alpha2 = np.square(self._alpha) - self.dL_dK = 0.5*(np.dot(self._alpha,self._alpha.T)-self.D*self.Ki) + if self.likelihood.YYT is None: + alpha = np.dot(self.Ki,self.likelihood.Y) + self.dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) else: - tmp = mdot(self.Ki, self.YYT, self.Ki) - self._alpha2 = np.diag(tmp) + tmp = mdot(self.Ki, self.likelihood.YYT, self.Ki) self.dL_dK = 0.5*(tmp - self.D*self.Ki) def _get_params(self): @@ -95,16 +89,15 @@ class GP(model): this function does nothing """ self.likelihood.fit(self.K) - self.Y, self.YYT, self.likelihood_variance, self.likelihood_Z = self.likelihood.sufficient_stats() # TODO: just store these in the likelihood? def _model_fit_term(self): """ Computes the model fit using YYT if it's available """ - if self.YYT is None: - return -0.5*np.sum(np.square(np.dot(self.Li,self.Y))) + if self.likelihood.YYT is None: + return -0.5*np.sum(np.square(np.dot(self.Li,self.likelihood.Y))) else: - return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) + return -0.5*np.sum(np.multiply(self.Ki, self.likelihood.YYT)) def log_likelihood(self): """ @@ -114,7 +107,7 @@ class GP(model): model for a new variable Y* = v_tilde/tau_tilde, with a covariance matrix K* = K + diag(1./tau_tilde) plus a normalization term. """ - return -0.5*self.D*self.K_logdet + self.model_fit_term() + self.likelihood.Z + return -0.5*self.D*self.K_logdet + self._model_fit_term() + self.likelihood.Z def _log_likelihood_gradients(self): @@ -125,7 +118,7 @@ class GP(model): For the likelihood parameters, pass in alpha = K^-1 y """ - return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X), self.likelihood._gradients(self.alpha2))) + return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK,X=self.X), self.likelihood._gradients(partial=self.dL_dK))) def _raw_predict(self,_Xnew,slices, full_cov=False): """ @@ -133,7 +126,7 @@ class GP(model): for normalisation or likelihood """ Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) - mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) + mu = np.dot(np.dot(Kx.T,self.Ki),self.likelihood.Y) KiKx = np.dot(self.Ki,Kx) if full_cov: Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) @@ -177,8 +170,10 @@ class GP(model): return mean, _5pc, _95pc - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + def raw_plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): """ + Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian + :param samples: the number of a posteriori samples to plot :param which_data: which if the training data to plot (default all) :type which_data: 'all' or a slice object to slice self.X, self.Y @@ -194,19 +189,17 @@ class GP(model): Can plot only part of the data and part of the posterior functions using which_data and which_functions """ + if which_functions=='all': which_functions = [True]*self.kern.Nparts if which_data=='all': which_data = slice(None) X = self.X[which_data,:] - Y = self.Y[which_data,:] - - Xorig = X*self._Xstd + self._Xmean - Yorig = Y*self._Ystd + self._Ymean #NOTE For EP this is v_tilde/beta + Y = self.likelihood.Y[which_data,:] if plot_limits is None: - xmin,xmax = Xorig.min(0),Xorig.max(0) + xmin,xmax = X.min(0),X.max(0) xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) elif len(plot_limits)==2: xmin, xmax = plot_limits @@ -215,27 +208,17 @@ class GP(model): if self.X.shape[1]==1: Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - m,v,phi = self.predict(Xnew,slices=which_functions,full_cov=full_cov) - if self.EP: - pb.subplot(211) - gpplot(Xnew,m,v) + m,v = self._raw_predict(Xnew,slices=which_functions,full_cov=False) + lower, upper = m.flatten() - 2.*np.sqrt(v) , m.flatten()+ 2.*np.sqrt(v) + gpplot(Xnew,m,lower,upper) - if samples: #NOTE why don't we put samples as a parameter of gpplot - s = np.random.multivariate_normal(m.flatten(),np.diag(v.flatten()),samples) - pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - pb.plot(Xorig,Yorig,'kx',mew=1.5) + pb.plot(X,Y,'kx',mew=1.5) pb.xlim(xmin,xmax) - - if self.EP: - pb.subplot(212) - self.likelihood.plot(Xnew,m,v,phi,self.X,samples=samples) - pb.xlim(xmin,xmax) - elif self.X.shape[1]==2: - resolution = 50 or resolution + resolution = resolution or 50 xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] Xtest = np.vstack((xx.flatten(),yy.flatten())).T - zz,vv,phi = self.predict(Xtest,slices=which_functions,full_cov=full_cov) + zz,vv = self._raw_predict(Xtest,slices=which_functions,full_cov=False) zz = zz.reshape(resolution,resolution) pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) @@ -244,3 +227,10 @@ class GP(model): else: raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" + + def plot(self): + """ + Plot the data's view of the world, with non-normalised values and GP predictions passed through the likelihood + """ + pass# TODO!!!!! + diff --git a/GPy/models/GP_regression.py b/GPy/models/GP_regression.py index 72a24307..916e5284 100644 --- a/GPy/models/GP_regression.py +++ b/GPy/models/GP_regression.py @@ -1,18 +1,18 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Copyright (c) 2012, James Hensman # Licensed under the BSD 3-clause license (see LICENSE.txt) import numpy as np -import pylab as pb +from GP import GP +from .. import likelihoods from .. import kern -from ..core import model -from ..util.linalg import pdinv,mdot -from ..util.plot import gpplot, Tango -class GP_regression(model): +class GP_regression(GP): """ Gaussian Process model for regression + This is a thin wrapper around the GP class, with a set of sensible defalts + :param X: input observations :param Y: observed values :param kernel: a GPy kernel, defaults to rbf+white @@ -29,199 +29,8 @@ class GP_regression(model): def __init__(self,X,Y,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None): if kernel is None: - kernel = kern.rbf(X.shape[1]) + kern.bias(X.shape[1]) + kern.white(X.shape[1]) + kernel = kern.rbf(X.shape[1]) - # parse arguments - self.Xslices = Xslices - assert isinstance(kernel, kern.kern) - self.kern = kernel - self.X = X - self.Y = Y - assert len(self.X.shape)==2 - assert len(self.Y.shape)==2 - assert self.X.shape[0] == self.Y.shape[0] - self.N, self.D = self.Y.shape - self.N, self.Q = self.X.shape + likelihood = likelihoods.Gaussian(Y,normalize=normalize_Y) - #here's some simple normalisation - if normalize_X: - self._Xmean = X.mean(0)[None,:] - self._Xstd = X.std(0)[None,:] - self.X = (X.copy() - self._Xmean) / self._Xstd - if hasattr(self,'Z'): - self.Z = (self.Z - self._Xmean) / self._Xstd - else: - self._Xmean = np.zeros((1,self.X.shape[1])) - self._Xstd = np.ones((1,self.X.shape[1])) - - if normalize_Y: - self._Ymean = Y.mean(0)[None,:] - self._Ystd = Y.std(0)[None,:] - self.Y = (Y.copy()- self._Ymean) / self._Ystd - else: - self._Ymean = np.zeros((1,self.Y.shape[1])) - self._Ystd = np.ones((1,self.Y.shape[1])) - - if self.D > self.N: - # then it's more efficient to store YYT - self.YYT = np.dot(self.Y, self.Y.T) - else: - self.YYT = None - - model.__init__(self) - - def _set_params(self,p): - self.kern._set_params_transformed(p) - self.K = self.kern.K(self.X,slices1=self.Xslices) - self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) - - def _get_params(self): - return self.kern._get_params_transformed() - - def _get_param_names(self): - return self.kern._get_param_names_transformed() - - def _model_fit_term(self): - """ - Computes the model fit using YYT if it's available - """ - if self.YYT is None: - return -0.5*np.sum(np.square(np.dot(self.Li,self.Y))) - else: - return -0.5*np.sum(np.multiply(self.Ki, self.YYT)) - - def log_likelihood(self): - complexity_term = -0.5*self.N*self.D*np.log(2.*np.pi) - 0.5*self.D*self.K_logdet - return complexity_term + self._model_fit_term() - - def dL_dK(self): - if self.YYT is None: - alpha = np.dot(self.Ki,self.Y) - dL_dK = 0.5*(np.dot(alpha,alpha.T)-self.D*self.Ki) - else: - dL_dK = 0.5*(mdot(self.Ki, self.YYT, self.Ki) - self.D*self.Ki) - - return dL_dK - - def _log_likelihood_gradients(self): - return self.kern.dK_dtheta(partial=self.dL_dK(),X=self.X) - - def predict(self,Xnew, slices=None, full_cov=False): - """ - - Predict the function(s) at the new point(s) Xnew. - - Arguments - --------- - :param Xnew: The points at which to make a prediction - :type Xnew: np.ndarray, Nnew x self.Q - :param slices: specifies which outputs kernel(s) the Xnew correspond to (see below) - :type slices: (None, list of slice objects, list of ints) - :param full_cov: whether to return the folll covariance matrix, or just the diagonal - :type full_cov: bool - :rtype: posterior mean, a Numpy array, Nnew x self.D - :rtype: posterior variance, a Numpy array, Nnew x Nnew x (self.D) - - .. Note:: "slices" specifies how the the points X_new co-vary wich the training points. - - - If None, the new points covary throigh every kernel part (default) - - If a list of slices, the i^th slice specifies which data are affected by the i^th kernel part - - If a list of booleans, specifying which kernel parts are active - - If full_cov and self.D > 1, the return shape of var is Nnew x Nnew x self.D. If self.D == 1, the return shape is Nnew x Nnew. - This is to allow for different normalisations of the output dimensions. - - - """ - - #normalise X values - Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var = self._raw_predict(Xnew, slices, full_cov) - - #un-normalise - mu = mu*self._Ystd + self._Ymean - if full_cov: - if self.D==1: - var *= np.square(self._Ystd) - else: - var = var[:,:,None] * np.square(self._Ystd) - else: - if self.D==1: - var *= np.square(np.squeeze(self._Ystd)) - else: - var = var[:,None] * np.square(self._Ystd) - - return mu,var - - def _raw_predict(self,_Xnew,slices, full_cov=False): - """Internal helper function for making predictions, does not account for normalisation""" - Kx = self.kern.K(self.X,_Xnew, slices1=self.Xslices,slices2=slices) - mu = np.dot(np.dot(Kx.T,self.Ki),self.Y) - KiKx = np.dot(self.Ki,Kx) - if full_cov: - Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) - var = Kxx - np.dot(KiKx.T,Kx) - else: - Kxx = self.kern.Kdiag(_Xnew, slices=slices) - var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - return mu, var - - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): - """ - :param samples: the number of a posteriori samples to plot - :param which_data: which if the training data to plot (default all) - :type which_data: 'all' or a slice object to slice self.X, self.Y - :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits - :param which_functions: which of the kernel functions to plot (additively) - :type which_functions: list of bools - :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D - - Plot the posterior of the GP. - - In one dimension, the function is plotted with a shaded region identifying two standard deviations. - - In two dimsensions, a contour-plot shows the mean predicted function - - In higher dimensions, we've no implemented this yet !TODO! - - Can plot only part of the data and part of the posterior functions using which_data and which_functions - """ - if which_functions=='all': - which_functions = [True]*self.kern.Nparts - if which_data=='all': - which_data = slice(None) - - X = self.X[which_data,:] - Y = self.Y[which_data,:] - - Xorig = X*self._Xstd + self._Xmean - Yorig = Y*self._Ystd + self._Ymean - if plot_limits is None: - xmin,xmax = Xorig.min(0),Xorig.max(0) - xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) - elif len(plot_limits)==2: - xmin, xmax = plot_limits - else: - raise ValueError, "Bad limits for plotting" - - - if self.X.shape[1]==1: - Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - m,v = self.predict(Xnew,slices=which_functions) - gpplot(Xnew,m,v) - if samples: - s = np.random.multivariate_normal(m.flatten(),v,samples) - pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - pb.plot(Xorig,Yorig,'kx',mew=1.5) - pb.xlim(xmin,xmax) - - elif self.X.shape[1]==2: - resolution = 50 or resolution - xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] - Xtest = np.vstack((xx.flatten(),yy.flatten())).T - zz,vv = self.predict(Xtest,slices=which_functions) - zz = zz.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) - pb.xlim(xmin[0],xmax[0]) - pb.ylim(xmin[1],xmax[1]) - - else: - raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" + GP.__init__(self, X, kernel, likelihood, normalize_X=normalize_X, Xslices=Xslices) diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index d2de84aa..1175eb71 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -2,14 +2,14 @@ # Licensed under the BSD 3-clause license (see LICENSE.txt) -#from GP_regression import GP_regression #from sparse_GP_regression import sparse_GP_regression -# ^^ remove these? +# TODO ^^ remove these? from GPLVM import GPLVM from warped_GP import warpedGP -from generalized_FITC import generalized_FITC -from sparse_GPLVM import sparse_GPLVM -from uncollapsed_sparse_GP import uncollapsed_sparse_GP +# TODO: from generalized_FITC import generalized_FITC +#from sparse_GPLVM import sparse_GPLVM +#from uncollapsed_sparse_GP import uncollapsed_sparse_GP from GP import GP -from sparse_GP import sparse_GP -from BGPLVM import Bayesian_GPLVM +from GP_regression import GP_regression +#from sparse_GP import sparse_GP +#from BGPLVM import Bayesian_GPLVM diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 7f287174..7b043209 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -7,8 +7,6 @@ from ..util.linalg import mdot, jitchol, chol_inv, pdinv from ..util.plot import gpplot from .. import kern from GP import GP -from ..inference.EP import Full,DTC,FITC -from ..inference.likelihoods import likelihood,probit,poisson,gaussian #Still TODO: diff --git a/GPy/util/plot.py b/GPy/util/plot.py index 8c06633e..3b4682e4 100644 --- a/GPy/util/plot.py +++ b/GPy/util/plot.py @@ -6,7 +6,7 @@ import Tango import pylab as pb import numpy as np -def gpplot(x,mu,var,edgecol=Tango.coloursHex['darkBlue'],fillcol=Tango.coloursHex['lightBlue'],axes=None,**kwargs): +def gpplot(x,mu,lower,upper,edgecol=Tango.coloursHex['darkBlue'],fillcol=Tango.coloursHex['lightBlue'],axes=None,**kwargs): if axes is None: axes = pb.gca() mu = mu.flatten() @@ -15,21 +15,15 @@ def gpplot(x,mu,var,edgecol=Tango.coloursHex['darkBlue'],fillcol=Tango.coloursHe #here's the mean axes.plot(x,mu,color=edgecol,linewidth=2) - #ensure variance is a vector - if len(var.shape)>1: - err = 2*np.sqrt(np.diag(var)) - else: - err = 2*np.sqrt(var) - - #here's the 2*std box + #here's the box kwargs['linewidth']=0.5 if not 'alpha' in kwargs.keys(): kwargs['alpha'] = 0.3 - axes.fill(np.hstack((x,x[::-1])),np.hstack((mu+err,mu[::-1]-err[::-1])),color=fillcol,**kwargs) + axes.fill(np.hstack((x,x[::-1])),np.hstack((upper,lower[::-1])),color=fillcol,**kwargs) #this is the edge: - axes.plot(x,mu+err,color=edgecol,linewidth=0.2) - axes.plot(x,mu-err,color=edgecol,linewidth=0.2) + axes.plot(x,upper,color=edgecol,linewidth=0.2) + axes.plot(x,lower,color=edgecol,linewidth=0.2) def removeRightTicks(ax=None): ax = ax or pb.gca() From 0763efcad4e87e953fbd0aec1bac2bcb22cb0bac Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:19:53 +0000 Subject: [PATCH 071/197] Added extensions for inline doc plotting --- doc/conf.py | 12 ++++++++++-- doc/tuto_GP_regression.rst | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 54135023..6a6eedfe 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -126,7 +126,7 @@ for mod_name in MOCK_MODULES: # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath('sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -140,7 +140,15 @@ for mod_name in MOCK_MODULES: # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', + 'matplotlib.sphinxext.mathmpl', + 'matplotlib.sphinxext.only_directives', + 'matplotlib.sphinxext.plot_directive', + 'matplotlib.sphinxext.ipython_directive', + 'sphinx.ext.doctest', + 'ipython_console_highlighting', + 'inheritance_diagram', + 'numpydoc'] # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 7d1a43df..a713eb4c 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -146,3 +146,13 @@ The flag ``ARD=True`` in the definition of the Matern kernel specifies that we w :height: 350px Contour plot of the best predictor (posterior mean). + +.. plot:: + + import matplotlib.pyplot as plt + import numpy as np + x = np.random.randn(1000) + plt.hist( x, 20) + plt.grid() + plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) + plt.show() From 62fe0f1ccad4aabbe55805282f61b3f67e198077 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:22:13 +0000 Subject: [PATCH 072/197] Got rid of some extensions we're not sure we're using --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 6a6eedfe..e48e286a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -141,10 +141,10 @@ sys.path.append(os.path.abspath('sphinxext')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'matplotlib.sphinxext.mathmpl', - 'matplotlib.sphinxext.only_directives', + #'matplotlib.sphinxext.mathmpl', + #'matplotlib.sphinxext.only_directives', 'matplotlib.sphinxext.plot_directive', - 'matplotlib.sphinxext.ipython_directive', + #'matplotlib.sphinxext.ipython_directive', 'sphinx.ext.doctest', 'ipython_console_highlighting', 'inheritance_diagram', From 2b40ee6f7e952b11405d6e2434995c68c9ac71da Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Thu, 31 Jan 2013 15:30:57 +0000 Subject: [PATCH 073/197] predictive_values implemented in EP --- GPy/likelihoods/EP.py | 11 ++++++++++- GPy/likelihoods/Gaussian.py | 10 ++++++++++ GPy/likelihoods/likelihood_functions.py | 8 ++++---- GPy/models/GP.py | 3 ++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index 3e975436..b557a62f 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -18,7 +18,6 @@ class EP: self.likelihood_function = likelihood_function self.epsilon = epsilon self.eta, self.delta = power_ep - self.jitter = 1e-12 # TODO: is this needed? """ Initial values - Likelihood approximation parameters: @@ -27,6 +26,16 @@ class EP: self.tau_tilde = np.zeros(self.N) self.v_tilde = np.zeros(self.N) + def predictive_values(self,mu,var): + return self.likelihood_function.predictive_values(mu,var) + + def _get_params(self): + return np.zeros(0) + def _get_param_names(self): + return [] + def _set_params(self,p): + pass # TODO: the EP likelihood might want to take some parameters... + def _compute_GP_variables(self): #Variables to be called from GP mu_tilde = self.v_tilde/self.tau_tilde #When calling EP, this variable is used instead of Y in the GP model diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index fe954b78..37132cf0 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -29,6 +29,16 @@ class Gaussian: self._variance = x self.variance = np.eye(self.N)*self._variance + def predictive_values(self,mu,var): + """ + Un-normalise the prediction and add the likelihood variance, then return the 5%, 95% interval + """ + mean = mu*self._std + self._mean + true_var = (var + self._variance)*self._std**2 + _5pc = mean + mean - 2.*np.sqrt(var) + _95pc = mean + 2.*np.sqrt(var) + return mean, _5pc, _95pc + def fit(self): """ No approximations needed diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index b94929d3..68fd276a 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -49,7 +49,7 @@ class probit(likelihood): def predictive_values(self,mu,var,all=False): """ - Compute mean, variance, and conficence interval (percentiles 5 and 95) of the prediction + Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mu = mu.flatten() var = var.flatten() @@ -57,7 +57,7 @@ class probit(likelihood): if all: p_05 = np.zeros([mu.size]) p_95 = np.ones([mu.size]) - return mean, mean*(1-mean),p_05,p_95 + return mean, p_05, p_95 else: return mean @@ -136,14 +136,14 @@ class poisson(likelihood): def predictive_values(self,mu,var,all=False): """ - Compute mean, variance, and conficence interval (percentiles 5 and 95) of the prediction + Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mean = np.exp(mu*self.scale + self.location) if all: tmp = stats.poisson.ppf(np.array([.05,.95]),mu) p_05 = tmp[:,0] p_95 = tmp[:,1] - return mean,mean,p_05,p_95 + return mean,p_05,p_95 else: return mean diff --git a/GPy/models/GP.py b/GPy/models/GP.py index b663ad3e..d20aa290 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -164,9 +164,10 @@ class GP(model): """ #normalise X values Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var, phi = self._raw_predict(Xnew, slices, full_cov=full_cov) + mu, var = self._raw_predict(Xnew, slices, full_cov=full_cov) #now push through likelihood TODO + mean, _5pc, _95pc = self.likelihood.predictive_values(mu, var) return mean, _5pc, _95pc From 3a558d8244cc3a46a088d61cee3a7a1c743a875c Mon Sep 17 00:00:00 2001 From: James Hensman Date: Thu, 31 Jan 2013 15:30:58 +0000 Subject: [PATCH 074/197] merged conflict --- GPy/models/GP.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index b663ad3e..5964570a 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -29,7 +29,6 @@ class GP(model): .. Note:: Multiple independent outputs are allowed using columns of Y """ - #TODO: when using EP, predict needs to return 3 values otherwise it just needs 2. At the moment predict returns 3 values in any case. def __init__(self, X, kernel, likelihood, normalize_X=False, Xslices=None): @@ -164,7 +163,7 @@ class GP(model): """ #normalise X values Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var, phi = self._raw_predict(Xnew, slices, full_cov=full_cov) + mu, var = self._raw_predict(Xnew, slices, full_cov=full_cov) #now push through likelihood TODO @@ -208,25 +207,9 @@ class GP(model): if self.X.shape[1]==1: Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] -<<<<<<< HEAD m,v = self._raw_predict(Xnew,slices=which_functions,full_cov=False) lower, upper = m.flatten() - 2.*np.sqrt(v) , m.flatten()+ 2.*np.sqrt(v) gpplot(Xnew,m,lower,upper) -======= - m,v = self.predict(Xnew,slices=which_functions,full_cov=full_cov) - if self.EP: - pb.subplot(211) - gpplot(Xnew,m,v) - if samples: #NOTE why don't we put samples as a parameter of gpplot - s = np.random.multivariate_normal(m.flatten(),np.diag(v.flatten()),samples) - pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - pb.plot(Xorig,Yorig,'kx',mew=1.5) - pb.xlim(xmin,xmax) - - if self.EP: - phi_m, phi_v, phi_l, phi_u = self.likelihood.predictive_values(m,v) ->>>>>>> 9feae765dc2253edaa37b25e3417a364e5b9acdc - pb.plot(X,Y,'kx',mew=1.5) pb.xlim(xmin,xmax) elif self.X.shape[1]==2: From 73f132fe19d77bccc8187d6ad093c73726de9a70 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:33:39 +0000 Subject: [PATCH 075/197] Added plot_directive and mathmpl extensions --- doc/sphinxext/__init__.py | 2 + doc/sphinxext/apigen.py | 427 +++++++++ doc/sphinxext/docscrape.py | 497 +++++++++++ doc/sphinxext/docscrape_sphinx.py | 136 +++ doc/sphinxext/inheritance_diagram.py | 407 +++++++++ doc/sphinxext/ipython_console_highlighting.py | 115 +++ doc/sphinxext/ipython_directive.py | 830 ++++++++++++++++++ doc/sphinxext/mathmpl.py | 120 +++ doc/sphinxext/numpydoc.py | 116 +++ doc/sphinxext/only_directives.py | 64 ++ doc/sphinxext/plot_directive.py | 819 +++++++++++++++++ 11 files changed, 3533 insertions(+) create mode 100644 doc/sphinxext/__init__.py create mode 100644 doc/sphinxext/apigen.py create mode 100644 doc/sphinxext/docscrape.py create mode 100644 doc/sphinxext/docscrape_sphinx.py create mode 100644 doc/sphinxext/inheritance_diagram.py create mode 100644 doc/sphinxext/ipython_console_highlighting.py create mode 100644 doc/sphinxext/ipython_directive.py create mode 100644 doc/sphinxext/mathmpl.py create mode 100644 doc/sphinxext/numpydoc.py create mode 100644 doc/sphinxext/only_directives.py create mode 100644 doc/sphinxext/plot_directive.py diff --git a/doc/sphinxext/__init__.py b/doc/sphinxext/__init__.py new file mode 100644 index 00000000..2caf15b1 --- /dev/null +++ b/doc/sphinxext/__init__.py @@ -0,0 +1,2 @@ +from __future__ import print_function + diff --git a/doc/sphinxext/apigen.py b/doc/sphinxext/apigen.py new file mode 100644 index 00000000..12374096 --- /dev/null +++ b/doc/sphinxext/apigen.py @@ -0,0 +1,427 @@ +"""Attempt to generate templates for module reference with Sphinx + +XXX - we exclude extension modules + +To include extension modules, first identify them as valid in the +``_uri2path`` method, then handle them in the ``_parse_module`` script. + +We get functions and classes by parsing the text of .py files. +Alternatively we could import the modules for discovery, and we'd have +to do that for extension modules. This would involve changing the +``_parse_module`` method to work via import and introspection, and +might involve changing ``discover_modules`` (which determines which +files are modules, and therefore which module URIs will be passed to +``_parse_module``). + +NOTE: this is a modified version of a script originally shipped with the +PyMVPA project, which we've adapted for NIPY use. PyMVPA is an MIT-licensed +project.""" + +# Stdlib imports +import os +import re + +# Functions and classes +class ApiDocWriter(object): + ''' Class for automatic detection and parsing of API docs + to Sphinx-parsable reST format''' + + # only separating first two levels + rst_section_levels = ['*', '=', '-', '~', '^'] + + def __init__(self, + package_name, + rst_extension='.rst', + package_skip_patterns=None, + module_skip_patterns=None, + ): + ''' Initialize package for parsing + + Parameters + ---------- + package_name : string + Name of the top-level package. *package_name* must be the + name of an importable package + rst_extension : string, optional + Extension for reST files, default '.rst' + package_skip_patterns : None or sequence of {strings, regexps} + Sequence of strings giving URIs of packages to be excluded + Operates on the package path, starting at (including) the + first dot in the package path, after *package_name* - so, + if *package_name* is ``sphinx``, then ``sphinx.util`` will + result in ``.util`` being passed for earching by these + regexps. If is None, gives default. Default is: + ['\.tests$'] + module_skip_patterns : None or sequence + Sequence of strings giving URIs of modules to be excluded + Operates on the module name including preceding URI path, + back to the first dot after *package_name*. For example + ``sphinx.util.console`` results in the string to search of + ``.util.console`` + If is None, gives default. Default is: + ['\.setup$', '\._'] + ''' + if package_skip_patterns is None: + package_skip_patterns = ['\\.tests$'] + if module_skip_patterns is None: + module_skip_patterns = ['\\.setup$', '\\._'] + self.package_name = package_name + self.rst_extension = rst_extension + self.package_skip_patterns = package_skip_patterns + self.module_skip_patterns = module_skip_patterns + + def get_package_name(self): + return self._package_name + + def set_package_name(self, package_name): + ''' Set package_name + + >>> docwriter = ApiDocWriter('sphinx') + >>> import sphinx + >>> docwriter.root_path == sphinx.__path__[0] + True + >>> docwriter.package_name = 'docutils' + >>> import docutils + >>> docwriter.root_path == docutils.__path__[0] + True + ''' + # It's also possible to imagine caching the module parsing here + self._package_name = package_name + self.root_module = __import__(package_name) + self.root_path = self.root_module.__path__[0] + self.written_modules = None + + package_name = property(get_package_name, set_package_name, None, + 'get/set package_name') + + def _get_object_name(self, line): + ''' Get second token in line + >>> docwriter = ApiDocWriter('sphinx') + >>> docwriter._get_object_name(" def func(): ") + 'func' + >>> docwriter._get_object_name(" class Klass(object): ") + 'Klass' + >>> docwriter._get_object_name(" class Klass: ") + 'Klass' + ''' + name = line.split()[1].split('(')[0].strip() + # in case we have classes which are not derived from object + # ie. old style classes + return name.rstrip(':') + + def _uri2path(self, uri): + ''' Convert uri to absolute filepath + + Parameters + ---------- + uri : string + URI of python module to return path for + + Returns + ------- + path : None or string + Returns None if there is no valid path for this URI + Otherwise returns absolute file system path for URI + + Examples + -------- + >>> docwriter = ApiDocWriter('sphinx') + >>> import sphinx + >>> modpath = sphinx.__path__[0] + >>> res = docwriter._uri2path('sphinx.builder') + >>> res == os.path.join(modpath, 'builder.py') + True + >>> res = docwriter._uri2path('sphinx') + >>> res == os.path.join(modpath, '__init__.py') + True + >>> docwriter._uri2path('sphinx.does_not_exist') + + ''' + if uri == self.package_name: + return os.path.join(self.root_path, '__init__.py') + path = uri.replace('.', os.path.sep) + path = path.replace(self.package_name + os.path.sep, '') + path = os.path.join(self.root_path, path) + # XXX maybe check for extensions as well? + if os.path.exists(path + '.py'): # file + path += '.py' + elif os.path.exists(os.path.join(path, '__init__.py')): + path = os.path.join(path, '__init__.py') + else: + return None + return path + + def _path2uri(self, dirpath): + ''' Convert directory path to uri ''' + relpath = dirpath.replace(self.root_path, self.package_name) + if relpath.startswith(os.path.sep): + relpath = relpath[1:] + return relpath.replace(os.path.sep, '.') + + def _parse_module(self, uri): + ''' Parse module defined in *uri* ''' + filename = self._uri2path(uri) + if filename is None: + # nothing that we could handle here. + return ([],[]) + f = open(filename, 'rt') + functions, classes = self._parse_lines(f) + f.close() + return functions, classes + + def _parse_lines(self, linesource): + ''' Parse lines of text for functions and classes ''' + functions = [] + classes = [] + for line in linesource: + if line.startswith('def ') and line.count('('): + # exclude private stuff + name = self._get_object_name(line) + if not name.startswith('_'): + functions.append(name) + elif line.startswith('class '): + # exclude private stuff + name = self._get_object_name(line) + if not name.startswith('_'): + classes.append(name) + else: + pass + functions.sort() + classes.sort() + return functions, classes + + def generate_api_doc(self, uri): + '''Make autodoc documentation template string for a module + + Parameters + ---------- + uri : string + python location of module - e.g 'sphinx.builder' + + Returns + ------- + S : string + Contents of API doc + ''' + # get the names of all classes and functions + functions, classes = self._parse_module(uri) + if not len(functions) and not len(classes): + print 'WARNING: Empty -',uri # dbg + return '' + + # Make a shorter version of the uri that omits the package name for + # titles + uri_short = re.sub(r'^%s\.' % self.package_name,'',uri) + + ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' + + chap_title = uri_short + ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) + + '\n\n') + + # Set the chapter title to read 'module' for all modules except for the + # main packages + if '.' in uri: + title = 'Module: :mod:`' + uri_short + '`' + else: + title = ':mod:`' + uri_short + '`' + ad += title + '\n' + self.rst_section_levels[2] * len(title) + + if len(classes): + ad += '\nInheritance diagram for ``%s``:\n\n' % uri + ad += '.. inheritance-diagram:: %s \n' % uri + ad += ' :parts: 3\n' + + ad += '\n.. automodule:: ' + uri + '\n' + ad += '\n.. currentmodule:: ' + uri + '\n' + multi_class = len(classes) > 1 + multi_fx = len(functions) > 1 + if multi_class: + ad += '\n' + 'Classes' + '\n' + \ + self.rst_section_levels[2] * 7 + '\n' + elif len(classes) and multi_fx: + ad += '\n' + 'Class' + '\n' + \ + self.rst_section_levels[2] * 5 + '\n' + for c in classes: + ad += '\n:class:`' + c + '`\n' \ + + self.rst_section_levels[multi_class + 2 ] * \ + (len(c)+9) + '\n\n' + ad += '\n.. autoclass:: ' + c + '\n' + # must NOT exclude from index to keep cross-refs working + ad += ' :members:\n' \ + ' :undoc-members:\n' \ + ' :show-inheritance:\n' \ + ' :inherited-members:\n' \ + '\n' \ + ' .. automethod:: __init__\n' + if multi_fx: + ad += '\n' + 'Functions' + '\n' + \ + self.rst_section_levels[2] * 9 + '\n\n' + elif len(functions) and multi_class: + ad += '\n' + 'Function' + '\n' + \ + self.rst_section_levels[2] * 8 + '\n\n' + for f in functions: + # must NOT exclude from index to keep cross-refs working + ad += '\n.. autofunction:: ' + uri + '.' + f + '\n\n' + return ad + + def _survives_exclude(self, matchstr, match_type): + ''' Returns True if *matchstr* does not match patterns + + ``self.package_name`` removed from front of string if present + + Examples + -------- + >>> dw = ApiDocWriter('sphinx') + >>> dw._survives_exclude('sphinx.okpkg', 'package') + True + >>> dw.package_skip_patterns.append('^\\.badpkg$') + >>> dw._survives_exclude('sphinx.badpkg', 'package') + False + >>> dw._survives_exclude('sphinx.badpkg', 'module') + True + >>> dw._survives_exclude('sphinx.badmod', 'module') + True + >>> dw.module_skip_patterns.append('^\\.badmod$') + >>> dw._survives_exclude('sphinx.badmod', 'module') + False + ''' + if match_type == 'module': + patterns = self.module_skip_patterns + elif match_type == 'package': + patterns = self.package_skip_patterns + else: + raise ValueError('Cannot interpret match type "%s"' + % match_type) + # Match to URI without package name + L = len(self.package_name) + if matchstr[:L] == self.package_name: + matchstr = matchstr[L:] + for pat in patterns: + try: + pat.search + except AttributeError: + pat = re.compile(pat) + if pat.search(matchstr): + return False + return True + + def discover_modules(self): + ''' Return module sequence discovered from ``self.package_name`` + + + Parameters + ---------- + None + + Returns + ------- + mods : sequence + Sequence of module names within ``self.package_name`` + + Examples + -------- + >>> dw = ApiDocWriter('sphinx') + >>> mods = dw.discover_modules() + >>> 'sphinx.util' in mods + True + >>> dw.package_skip_patterns.append('\.util$') + >>> 'sphinx.util' in dw.discover_modules() + False + >>> + ''' + modules = [self.package_name] + # raw directory parsing + for dirpath, dirnames, filenames in os.walk(self.root_path): + # Check directory names for packages + root_uri = self._path2uri(os.path.join(self.root_path, + dirpath)) + for dirname in dirnames[:]: # copy list - we modify inplace + package_uri = '.'.join((root_uri, dirname)) + if (self._uri2path(package_uri) and + self._survives_exclude(package_uri, 'package')): + modules.append(package_uri) + else: + dirnames.remove(dirname) + # Check filenames for modules + for filename in filenames: + module_name = filename[:-3] + module_uri = '.'.join((root_uri, module_name)) + if (self._uri2path(module_uri) and + self._survives_exclude(module_uri, 'module')): + modules.append(module_uri) + return sorted(modules) + + def write_modules_api(self, modules,outdir): + # write the list + written_modules = [] + for m in modules: + api_str = self.generate_api_doc(m) + if not api_str: + continue + # write out to file + outfile = os.path.join(outdir, + m + self.rst_extension) + fileobj = open(outfile, 'wt') + fileobj.write(api_str) + fileobj.close() + written_modules.append(m) + self.written_modules = written_modules + + def write_api_docs(self, outdir): + """Generate API reST files. + + Parameters + ---------- + outdir : string + Directory name in which to store files + We create automatic filenames for each module + + Returns + ------- + None + + Notes + ----- + Sets self.written_modules to list of written modules + """ + if not os.path.exists(outdir): + os.mkdir(outdir) + # compose list of modules + modules = self.discover_modules() + self.write_modules_api(modules,outdir) + + def write_index(self, outdir, froot='gen', relative_to=None): + """Make a reST API index file from written files + + Parameters + ---------- + path : string + Filename to write index to + outdir : string + Directory to which to write generated index file + froot : string, optional + root (filename without extension) of filename to write to + Defaults to 'gen'. We add ``self.rst_extension``. + relative_to : string + path to which written filenames are relative. This + component of the written file path will be removed from + outdir, in the generated index. Default is None, meaning, + leave path as it is. + """ + if self.written_modules is None: + raise ValueError('No modules written') + # Get full filename path + path = os.path.join(outdir, froot+self.rst_extension) + # Path written into index is relative to rootpath + if relative_to is not None: + relpath = outdir.replace(relative_to + os.path.sep, '') + else: + relpath = outdir + idx = open(path,'wt') + w = idx.write + w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') + w('.. toctree::\n\n') + for f in self.written_modules: + w(' %s\n' % os.path.join(relpath,f)) + idx.close() diff --git a/doc/sphinxext/docscrape.py b/doc/sphinxext/docscrape.py new file mode 100644 index 00000000..f374b3dd --- /dev/null +++ b/doc/sphinxext/docscrape.py @@ -0,0 +1,497 @@ +"""Extract reference documentation from the NumPy source tree. + +""" + +import inspect +import textwrap +import re +import pydoc +from StringIO import StringIO +from warnings import warn +4 +class Reader(object): + """A line-based string reader. + + """ + def __init__(self, data): + """ + Parameters + ---------- + data : str + String with lines separated by '\n'. + + """ + if isinstance(data,list): + self._str = data + else: + self._str = data.split('\n') # store string as list of lines + + self.reset() + + def __getitem__(self, n): + return self._str[n] + + def reset(self): + self._l = 0 # current line nr + + def read(self): + if not self.eof(): + out = self[self._l] + self._l += 1 + return out + else: + return '' + + def seek_next_non_empty_line(self): + for l in self[self._l:]: + if l.strip(): + break + else: + self._l += 1 + + def eof(self): + return self._l >= len(self._str) + + def read_to_condition(self, condition_func): + start = self._l + for line in self[start:]: + if condition_func(line): + return self[start:self._l] + self._l += 1 + if self.eof(): + return self[start:self._l+1] + return [] + + def read_to_next_empty_line(self): + self.seek_next_non_empty_line() + def is_empty(line): + return not line.strip() + return self.read_to_condition(is_empty) + + def read_to_next_unindented_line(self): + def is_unindented(line): + return (line.strip() and (len(line.lstrip()) == len(line))) + return self.read_to_condition(is_unindented) + + def peek(self,n=0): + if self._l + n < len(self._str): + return self[self._l + n] + else: + return '' + + def is_empty(self): + return not ''.join(self._str).strip() + + +class NumpyDocString(object): + def __init__(self,docstring): + docstring = textwrap.dedent(docstring).split('\n') + + self._doc = Reader(docstring) + self._parsed_data = { + 'Signature': '', + 'Summary': [''], + 'Extended Summary': [], + 'Parameters': [], + 'Returns': [], + 'Raises': [], + 'Warns': [], + 'Other Parameters': [], + 'Attributes': [], + 'Methods': [], + 'See Also': [], + 'Notes': [], + 'Warnings': [], + 'References': '', + 'Examples': '', + 'index': {} + } + + self._parse() + + def __getitem__(self,key): + return self._parsed_data[key] + + def __setitem__(self,key,val): + if not self._parsed_data.has_key(key): + warn("Unknown section %s" % key) + else: + self._parsed_data[key] = val + + def _is_at_section(self): + self._doc.seek_next_non_empty_line() + + if self._doc.eof(): + return False + + l1 = self._doc.peek().strip() # e.g. Parameters + + if l1.startswith('.. index::'): + return True + + l2 = self._doc.peek(1).strip() # ---------- or ========== + return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) + + def _strip(self,doc): + i = 0 + j = 0 + for i,line in enumerate(doc): + if line.strip(): break + + for j,line in enumerate(doc[::-1]): + if line.strip(): break + + return doc[i:len(doc)-j] + + def _read_to_next_section(self): + section = self._doc.read_to_next_empty_line() + + while not self._is_at_section() and not self._doc.eof(): + if not self._doc.peek(-1).strip(): # previous line was empty + section += [''] + + section += self._doc.read_to_next_empty_line() + + return section + + def _read_sections(self): + while not self._doc.eof(): + data = self._read_to_next_section() + name = data[0].strip() + + if name.startswith('..'): # index section + yield name, data[1:] + elif len(data) < 2: + yield StopIteration + else: + yield name, self._strip(data[2:]) + + def _parse_param_list(self,content): + r = Reader(content) + params = [] + while not r.eof(): + header = r.read().strip() + if ' : ' in header: + arg_name, arg_type = header.split(' : ')[:2] + else: + arg_name, arg_type = header, '' + + desc = r.read_to_next_unindented_line() + desc = dedent_lines(desc) + + params.append((arg_name,arg_type,desc)) + + return params + + + _name_rgx = re.compile(r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" + r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) + def _parse_see_also(self, content): + """ + func_name : Descriptive text + continued text + another_func_name : Descriptive text + func_name1, func_name2, :meth:`func_name`, func_name3 + + """ + items = [] + + def parse_item_name(text): + """Match ':role:`name`' or 'name'""" + m = self._name_rgx.match(text) + if m: + g = m.groups() + if g[1] is None: + return g[3], None + else: + return g[2], g[1] + raise ValueError("%s is not a item name" % text) + + def push_item(name, rest): + if not name: + return + name, role = parse_item_name(name) + items.append((name, list(rest), role)) + del rest[:] + + current_func = None + rest = [] + + for line in content: + if not line.strip(): continue + + m = self._name_rgx.match(line) + if m and line[m.end():].strip().startswith(':'): + push_item(current_func, rest) + current_func, line = line[:m.end()], line[m.end():] + rest = [line.split(':', 1)[1].strip()] + if not rest[0]: + rest = [] + elif not line.startswith(' '): + push_item(current_func, rest) + current_func = None + if ',' in line: + for func in line.split(','): + push_item(func, []) + elif line.strip(): + current_func = line + elif current_func is not None: + rest.append(line.strip()) + push_item(current_func, rest) + return items + + def _parse_index(self, section, content): + """ + .. index: default + :refguide: something, else, and more + + """ + def strip_each_in(lst): + return [s.strip() for s in lst] + + out = {} + section = section.split('::') + if len(section) > 1: + out['default'] = strip_each_in(section[1].split(','))[0] + for line in content: + line = line.split(':') + if len(line) > 2: + out[line[1]] = strip_each_in(line[2].split(',')) + return out + + def _parse_summary(self): + """Grab signature (if given) and summary""" + if self._is_at_section(): + return + + summary = self._doc.read_to_next_empty_line() + summary_str = " ".join([s.strip() for s in summary]).strip() + if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): + self['Signature'] = summary_str + if not self._is_at_section(): + self['Summary'] = self._doc.read_to_next_empty_line() + else: + self['Summary'] = summary + + if not self._is_at_section(): + self['Extended Summary'] = self._read_to_next_section() + + def _parse(self): + self._doc.reset() + self._parse_summary() + + for (section,content) in self._read_sections(): + if not section.startswith('..'): + section = ' '.join([s.capitalize() for s in section.split(' ')]) + if section in ('Parameters', 'Attributes', 'Methods', + 'Returns', 'Raises', 'Warns'): + self[section] = self._parse_param_list(content) + elif section.startswith('.. index::'): + self['index'] = self._parse_index(section, content) + elif section == 'See Also': + self['See Also'] = self._parse_see_also(content) + else: + self[section] = content + + # string conversion routines + + def _str_header(self, name, symbol='-'): + return [name, len(name)*symbol] + + def _str_indent(self, doc, indent=4): + out = [] + for line in doc: + out += [' '*indent + line] + return out + + def _str_signature(self): + if self['Signature']: + return [self['Signature'].replace('*','\*')] + [''] + else: + return [''] + + def _str_summary(self): + if self['Summary']: + return self['Summary'] + [''] + else: + return [] + + def _str_extended_summary(self): + if self['Extended Summary']: + return self['Extended Summary'] + [''] + else: + return [] + + def _str_param_list(self, name): + out = [] + if self[name]: + out += self._str_header(name) + for param,param_type,desc in self[name]: + out += ['%s : %s' % (param, param_type)] + out += self._str_indent(desc) + out += [''] + return out + + def _str_section(self, name): + out = [] + if self[name]: + out += self._str_header(name) + out += self[name] + out += [''] + return out + + def _str_see_also(self, func_role): + if not self['See Also']: return [] + out = [] + out += self._str_header("See Also") + last_had_desc = True + for func, desc, role in self['See Also']: + if role: + link = ':%s:`%s`' % (role, func) + elif func_role: + link = ':%s:`%s`' % (func_role, func) + else: + link = "`%s`_" % func + if desc or last_had_desc: + out += [''] + out += [link] + else: + out[-1] += ", %s" % link + if desc: + out += self._str_indent([' '.join(desc)]) + last_had_desc = True + else: + last_had_desc = False + out += [''] + return out + + def _str_index(self): + idx = self['index'] + out = [] + out += ['.. index:: %s' % idx.get('default','')] + for section, references in idx.iteritems(): + if section == 'default': + continue + out += [' :%s: %s' % (section, ', '.join(references))] + return out + + def __str__(self, func_role=''): + out = [] + out += self._str_signature() + out += self._str_summary() + out += self._str_extended_summary() + for param_list in ('Parameters','Returns','Raises'): + out += self._str_param_list(param_list) + out += self._str_section('Warnings') + out += self._str_see_also(func_role) + for s in ('Notes','References','Examples'): + out += self._str_section(s) + out += self._str_index() + return '\n'.join(out) + + +def indent(str,indent=4): + indent_str = ' '*indent + if str is None: + return indent_str + lines = str.split('\n') + return '\n'.join(indent_str + l for l in lines) + +def dedent_lines(lines): + """Deindent a list of lines maximally""" + return textwrap.dedent("\n".join(lines)).split("\n") + +def header(text, style='-'): + return text + '\n' + style*len(text) + '\n' + + +class FunctionDoc(NumpyDocString): + def __init__(self, func, role='func', doc=None): + self._f = func + self._role = role # e.g. "func" or "meth" + if doc is None: + doc = inspect.getdoc(func) or '' + try: + NumpyDocString.__init__(self, doc) + except ValueError, e: + print '*'*78 + print "ERROR: '%s' while parsing `%s`" % (e, self._f) + print '*'*78 + #print "Docstring follows:" + #print doclines + #print '='*78 + + if not self['Signature']: + func, func_name = self.get_func() + try: + # try to read signature + argspec = inspect.getargspec(func) + argspec = inspect.formatargspec(*argspec) + argspec = argspec.replace('*','\*') + signature = '%s%s' % (func_name, argspec) + except TypeError, e: + signature = '%s()' % func_name + self['Signature'] = signature + + def get_func(self): + func_name = getattr(self._f, '__name__', self.__class__.__name__) + if inspect.isclass(self._f): + func = getattr(self._f, '__call__', self._f.__init__) + else: + func = self._f + return func, func_name + + def __str__(self): + out = '' + + func, func_name = self.get_func() + signature = self['Signature'].replace('*', '\*') + + roles = {'func': 'function', + 'meth': 'method'} + + if self._role: + if not roles.has_key(self._role): + print "Warning: invalid role %s" % self._role + out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), + func_name) + + out += super(FunctionDoc, self).__str__(func_role=self._role) + return out + + +class ClassDoc(NumpyDocString): + def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None): + if not inspect.isclass(cls): + raise ValueError("Initialise using a class. Got %r" % cls) + self._cls = cls + + if modulename and not modulename.endswith('.'): + modulename += '.' + self._mod = modulename + self._name = cls.__name__ + self._func_doc = func_doc + + if doc is None: + doc = pydoc.getdoc(cls) + + NumpyDocString.__init__(self, doc) + + @property + def methods(self): + return [name for name,func in inspect.getmembers(self._cls) + if not name.startswith('_') and callable(func)] + + def __str__(self): + out = '' + out += super(ClassDoc, self).__str__() + out += "\n\n" + + #for m in self.methods: + # print "Parsing `%s`" % m + # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n' + # out += '.. index::\n single: %s; %s\n\n' % (self._name, m) + + return out + + diff --git a/doc/sphinxext/docscrape_sphinx.py b/doc/sphinxext/docscrape_sphinx.py new file mode 100644 index 00000000..77ed271b --- /dev/null +++ b/doc/sphinxext/docscrape_sphinx.py @@ -0,0 +1,136 @@ +import re, inspect, textwrap, pydoc +from docscrape import NumpyDocString, FunctionDoc, ClassDoc + +class SphinxDocString(NumpyDocString): + # string conversion routines + def _str_header(self, name, symbol='`'): + return ['.. rubric:: ' + name, ''] + + def _str_field_list(self, name): + return [':' + name + ':'] + + def _str_indent(self, doc, indent=4): + out = [] + for line in doc: + out += [' '*indent + line] + return out + + def _str_signature(self): + return [''] + if self['Signature']: + return ['``%s``' % self['Signature']] + [''] + else: + return [''] + + def _str_summary(self): + return self['Summary'] + [''] + + def _str_extended_summary(self): + return self['Extended Summary'] + [''] + + def _str_param_list(self, name): + out = [] + if self[name]: + out += self._str_field_list(name) + out += [''] + for param,param_type,desc in self[name]: + out += self._str_indent(['**%s** : %s' % (param.strip(), + param_type)]) + out += [''] + out += self._str_indent(desc,8) + out += [''] + return out + + def _str_section(self, name): + out = [] + if self[name]: + out += self._str_header(name) + out += [''] + content = textwrap.dedent("\n".join(self[name])).split("\n") + out += content + out += [''] + return out + + def _str_see_also(self, func_role): + out = [] + if self['See Also']: + see_also = super(SphinxDocString, self)._str_see_also(func_role) + out = ['.. seealso::', ''] + out += self._str_indent(see_also[2:]) + return out + + def _str_warnings(self): + out = [] + if self['Warnings']: + out = ['.. warning::', ''] + out += self._str_indent(self['Warnings']) + return out + + def _str_index(self): + idx = self['index'] + out = [] + if len(idx) == 0: + return out + + out += ['.. index:: %s' % idx.get('default','')] + for section, references in idx.iteritems(): + if section == 'default': + continue + elif section == 'refguide': + out += [' single: %s' % (', '.join(references))] + else: + out += [' %s: %s' % (section, ','.join(references))] + return out + + def _str_references(self): + out = [] + if self['References']: + out += self._str_header('References') + if isinstance(self['References'], str): + self['References'] = [self['References']] + out.extend(self['References']) + out += [''] + return out + + def __str__(self, indent=0, func_role="obj"): + out = [] + out += self._str_signature() + out += self._str_index() + [''] + out += self._str_summary() + out += self._str_extended_summary() + for param_list in ('Parameters', 'Attributes', 'Methods', + 'Returns','Raises'): + out += self._str_param_list(param_list) + out += self._str_warnings() + out += self._str_see_also(func_role) + out += self._str_section('Notes') + out += self._str_references() + out += self._str_section('Examples') + out = self._str_indent(out,indent) + return '\n'.join(out) + +class SphinxFunctionDoc(SphinxDocString, FunctionDoc): + pass + +class SphinxClassDoc(SphinxDocString, ClassDoc): + pass + +def get_doc_object(obj, what=None, doc=None): + if what is None: + if inspect.isclass(obj): + what = 'class' + elif inspect.ismodule(obj): + what = 'module' + elif callable(obj): + what = 'function' + else: + what = 'object' + if what == 'class': + return SphinxClassDoc(obj, '', func_doc=SphinxFunctionDoc, doc=doc) + elif what in ('function', 'method'): + return SphinxFunctionDoc(obj, '', doc=doc) + else: + if doc is None: + doc = pydoc.getdoc(obj) + return SphinxDocString(doc) + diff --git a/doc/sphinxext/inheritance_diagram.py b/doc/sphinxext/inheritance_diagram.py new file mode 100644 index 00000000..407fc13f --- /dev/null +++ b/doc/sphinxext/inheritance_diagram.py @@ -0,0 +1,407 @@ +""" +Defines a docutils directive for inserting inheritance diagrams. + +Provide the directive with one or more classes or modules (separated +by whitespace). For modules, all of the classes in that module will +be used. + +Example:: + + Given the following classes: + + class A: pass + class B(A): pass + class C(A): pass + class D(B, C): pass + class E(B): pass + + .. inheritance-diagram: D E + + Produces a graph like the following: + + A + / \ + B C + / \ / + E D + +The graph is inserted as a PNG+image map into HTML and a PDF in +LaTeX. +""" + +import inspect +import os +import re +import subprocess +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +from docutils.nodes import Body, Element +from docutils.parsers.rst import directives +from sphinx.roles import xfileref_role + +def my_import(name): + """Module importer - taken from the python documentation. + + This function allows importing names with dots in them.""" + + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +class DotException(Exception): + pass + +class InheritanceGraph(object): + """ + Given a list of classes, determines the set of classes that + they inherit from all the way to the root "object", and then + is able to generate a graphviz dot graph from them. + """ + def __init__(self, class_names, show_builtins=False): + """ + *class_names* is a list of child classes to show bases from. + + If *show_builtins* is True, then Python builtins will be shown + in the graph. + """ + self.class_names = class_names + self.classes = self._import_classes(class_names) + self.all_classes = self._all_classes(self.classes) + if len(self.all_classes) == 0: + raise ValueError("No classes found for inheritance diagram") + self.show_builtins = show_builtins + + py_sig_re = re.compile(r'''^([\w.]*\.)? # class names + (\w+) \s* $ # optionally arguments + ''', re.VERBOSE) + + def _import_class_or_module(self, name): + """ + Import a class using its fully-qualified *name*. + """ + try: + path, base = self.py_sig_re.match(name).groups() + except: + raise ValueError( + "Invalid class or module '%s' specified for inheritance diagram" % name) + fullname = (path or '') + base + path = (path and path.rstrip('.')) + if not path: + path = base + try: + module = __import__(path, None, None, []) + # We must do an import of the fully qualified name. Otherwise if a + # subpackage 'a.b' is requested where 'import a' does NOT provide + # 'a.b' automatically, then 'a.b' will not be found below. This + # second call will force the equivalent of 'import a.b' to happen + # after the top-level import above. + my_import(fullname) + + except ImportError: + raise ValueError( + "Could not import class or module '%s' specified for inheritance diagram" % name) + + try: + todoc = module + for comp in fullname.split('.')[1:]: + todoc = getattr(todoc, comp) + except AttributeError: + raise ValueError( + "Could not find class or module '%s' specified for inheritance diagram" % name) + + # If a class, just return it + if inspect.isclass(todoc): + return [todoc] + elif inspect.ismodule(todoc): + classes = [] + for cls in todoc.__dict__.values(): + if inspect.isclass(cls) and cls.__module__ == todoc.__name__: + classes.append(cls) + return classes + raise ValueError( + "'%s' does not resolve to a class or module" % name) + + def _import_classes(self, class_names): + """ + Import a list of classes. + """ + classes = [] + for name in class_names: + classes.extend(self._import_class_or_module(name)) + return classes + + def _all_classes(self, classes): + """ + Return a list of all classes that are ancestors of *classes*. + """ + all_classes = {} + + def recurse(cls): + all_classes[cls] = None + for c in cls.__bases__: + if c not in all_classes: + recurse(c) + + for cls in classes: + recurse(cls) + + return all_classes.keys() + + def class_name(self, cls, parts=0): + """ + Given a class object, return a fully-qualified name. This + works for things I've tested in matplotlib so far, but may not + be completely general. + """ + module = cls.__module__ + if module == '__builtin__': + fullname = cls.__name__ + else: + fullname = "%s.%s" % (module, cls.__name__) + if parts == 0: + return fullname + name_parts = fullname.split('.') + return '.'.join(name_parts[-parts:]) + + def get_all_class_names(self): + """ + Get all of the class names involved in the graph. + """ + return [self.class_name(x) for x in self.all_classes] + + # These are the default options for graphviz + default_graph_options = { + "rankdir": "LR", + "size": '"8.0, 12.0"' + } + default_node_options = { + "shape": "box", + "fontsize": 10, + "height": 0.25, + "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans", + "style": '"setlinewidth(0.5)"' + } + default_edge_options = { + "arrowsize": 0.5, + "style": '"setlinewidth(0.5)"' + } + + def _format_node_options(self, options): + return ','.join(["%s=%s" % x for x in options.items()]) + def _format_graph_options(self, options): + return ''.join(["%s=%s;\n" % x for x in options.items()]) + + def generate_dot(self, fd, name, parts=0, urls={}, + graph_options={}, node_options={}, + edge_options={}): + """ + Generate a graphviz dot graph from the classes that + were passed in to __init__. + + *fd* is a Python file-like object to write to. + + *name* is the name of the graph + + *urls* is a dictionary mapping class names to http urls + + *graph_options*, *node_options*, *edge_options* are + dictionaries containing key/value pairs to pass on as graphviz + properties. + """ + g_options = self.default_graph_options.copy() + g_options.update(graph_options) + n_options = self.default_node_options.copy() + n_options.update(node_options) + e_options = self.default_edge_options.copy() + e_options.update(edge_options) + + fd.write('digraph %s {\n' % name) + fd.write(self._format_graph_options(g_options)) + + for cls in self.all_classes: + if not self.show_builtins and cls in __builtins__.values(): + continue + + name = self.class_name(cls, parts) + + # Write the node + this_node_options = n_options.copy() + url = urls.get(self.class_name(cls)) + if url is not None: + this_node_options['URL'] = '"%s"' % url + fd.write(' "%s" [%s];\n' % + (name, self._format_node_options(this_node_options))) + + # Write the edges + for base in cls.__bases__: + if not self.show_builtins and base in __builtins__.values(): + continue + + base_name = self.class_name(base, parts) + fd.write(' "%s" -> "%s" [%s];\n' % + (base_name, name, + self._format_node_options(e_options))) + fd.write('}\n') + + def run_dot(self, args, name, parts=0, urls={}, + graph_options={}, node_options={}, edge_options={}): + """ + Run graphviz 'dot' over this graph, returning whatever 'dot' + writes to stdout. + + *args* will be passed along as commandline arguments. + + *name* is the name of the graph + + *urls* is a dictionary mapping class names to http urls + + Raises DotException for any of the many os and + installation-related errors that may occur. + """ + try: + dot = subprocess.Popen(['dot'] + list(args), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + close_fds=True) + except OSError: + raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?") + except ValueError: + raise DotException("'dot' called with invalid arguments") + except: + raise DotException("Unexpected error calling 'dot'") + + self.generate_dot(dot.stdin, name, parts, urls, graph_options, + node_options, edge_options) + dot.stdin.close() + result = dot.stdout.read() + returncode = dot.wait() + if returncode != 0: + raise DotException("'dot' returned the errorcode %d" % returncode) + return result + +class inheritance_diagram(Body, Element): + """ + A docutils node to use as a placeholder for the inheritance + diagram. + """ + pass + +def inheritance_diagram_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, + state_machine): + """ + Run when the inheritance_diagram directive is first encountered. + """ + node = inheritance_diagram() + + class_names = arguments + + # Create a graph starting with the list of classes + graph = InheritanceGraph(class_names) + + # Create xref nodes for each target of the graph's image map and + # add them to the doc tree so that Sphinx can resolve the + # references to real URLs later. These nodes will eventually be + # removed from the doctree after we're done with them. + for name in graph.get_all_class_names(): + refnodes, x = xfileref_role( + 'class', ':class:`%s`' % name, name, 0, state) + node.extend(refnodes) + # Store the graph object so we can use it to generate the + # dot file later + node['graph'] = graph + # Store the original content for use as a hash + node['parts'] = options.get('parts', 0) + node['content'] = " ".join(class_names) + return [node] + +def get_graph_hash(node): + return md5(node['content'] + str(node['parts'])).hexdigest()[-10:] + +def html_output_graph(self, node): + """ + Output the graph for HTML. This will insert a PNG with clickable + image map. + """ + graph = node['graph'] + parts = node['parts'] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + path = '_images' + dest_path = os.path.join(setup.app.builder.outdir, path) + if not os.path.exists(dest_path): + os.makedirs(dest_path) + png_path = os.path.join(dest_path, name + ".png") + path = setup.app.builder.imgpath + + # Create a mapping from fully-qualified class names to URLs. + urls = {} + for child in node: + if child.get('refuri') is not None: + urls[child['reftitle']] = child.get('refuri') + elif child.get('refid') is not None: + urls[child['reftitle']] = '#' + child.get('refid') + + # These arguments to dot will save a PNG file to disk and write + # an HTML image map to stdout. + image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'], + name, parts, urls) + return ('%s' % + (path, name, name, image_map)) + +def latex_output_graph(self, node): + """ + Output the graph for LaTeX. This will insert a PDF. + """ + graph = node['graph'] + parts = node['parts'] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images')) + if not os.path.exists(dest_path): + os.makedirs(dest_path) + pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf")) + + graph.run_dot(['-Tpdf', '-o%s' % pdf_path], + name, parts, graph_options={'size': '"6.0,6.0"'}) + return '\n\\includegraphics{%s}\n\n' % pdf_path + +def visit_inheritance_diagram(inner_func): + """ + This is just a wrapper around html/latex_output_graph to make it + easier to handle errors and insert warnings. + """ + def visitor(self, node): + try: + content = inner_func(self, node) + except DotException, e: + # Insert the exception as a warning in the document + warning = self.document.reporter.warning(str(e), line=node.line) + warning.parent = node + node.children = [warning] + else: + source = self.document.attributes['source'] + self.body.append(content) + node.children = [] + return visitor + +def do_nothing(self, node): + pass + +def setup(app): + setup.app = app + setup.confdir = app.confdir + + app.add_node( + inheritance_diagram, + latex=(visit_inheritance_diagram(latex_output_graph), do_nothing), + html=(visit_inheritance_diagram(html_output_graph), do_nothing)) + app.add_directive( + 'inheritance-diagram', inheritance_diagram_directive, + False, (1, 100, 0), parts = directives.nonnegative_int) diff --git a/doc/sphinxext/ipython_console_highlighting.py b/doc/sphinxext/ipython_console_highlighting.py new file mode 100644 index 00000000..c9bf1c15 --- /dev/null +++ b/doc/sphinxext/ipython_console_highlighting.py @@ -0,0 +1,115 @@ +"""reST directive for syntax-highlighting ipython interactive sessions. + +XXX - See what improvements can be made based on the new (as of Sept 2009) +'pycon' lexer for the python console. At the very least it will give better +highlighted tracebacks. +""" +from __future__ import print_function + +#----------------------------------------------------------------------------- +# Needed modules + +# Standard library +import re + +# Third party +from pygments.lexer import Lexer, do_insertions +from pygments.lexers.agile import (PythonConsoleLexer, PythonLexer, + PythonTracebackLexer) +from pygments.token import Comment, Generic + +from sphinx import highlighting + +#----------------------------------------------------------------------------- +# Global constants +line_re = re.compile('.*?\n') + +#----------------------------------------------------------------------------- +# Code begins - classes and functions + +class IPythonConsoleLexer(Lexer): + """ + For IPython console output or doctests, such as: + + .. sourcecode:: ipython + + In [1]: a = 'foo' + + In [2]: a + Out[2]: 'foo' + + In [3]: print a + foo + + In [4]: 1 / 0 + + Notes: + + - Tracebacks are not currently supported. + + - It assumes the default IPython prompts, not customized ones. + """ + + name = 'IPython console session' + aliases = ['ipython'] + mimetypes = ['text/x-ipython-console'] + input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") + output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") + continue_prompt = re.compile(" \.\.\.+:") + tb_start = re.compile("\-+") + + def get_tokens_unprocessed(self, text): + pylexer = PythonLexer(**self.options) + tblexer = PythonTracebackLexer(**self.options) + + curcode = '' + insertions = [] + for match in line_re.finditer(text): + line = match.group() + input_prompt = self.input_prompt.match(line) + continue_prompt = self.continue_prompt.match(line.rstrip()) + output_prompt = self.output_prompt.match(line) + if line.startswith("#"): + insertions.append((len(curcode), + [(0, Comment, line)])) + elif input_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, input_prompt.group())])) + curcode += line[input_prompt.end():] + elif continue_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, continue_prompt.group())])) + curcode += line[continue_prompt.end():] + elif output_prompt is not None: + # Use the 'error' token for output. We should probably make + # our own token, but error is typicaly in a bright color like + # red, so it works fine for our output prompts. + insertions.append((len(curcode), + [(0, Generic.Error, output_prompt.group())])) + curcode += line[output_prompt.end():] + else: + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + curcode = '' + insertions = [] + yield match.start(), Generic.Output, line + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + + +def setup(app): + """Setup as a sphinx extension.""" + + # This is only a lexer, so adding it below to pygments appears sufficient. + # But if somebody knows that the right API usage should be to do that via + # sphinx, by all means fix it here. At least having this setup.py + # suppresses the sphinx warning we'd get without it. + pass + +#----------------------------------------------------------------------------- +# Register the extension as a valid pygments lexer +highlighting.lexers['ipython'] = IPythonConsoleLexer() diff --git a/doc/sphinxext/ipython_directive.py b/doc/sphinxext/ipython_directive.py new file mode 100644 index 00000000..79cd2aed --- /dev/null +++ b/doc/sphinxext/ipython_directive.py @@ -0,0 +1,830 @@ +# -*- coding: utf-8 -*- +"""Sphinx directive to support embedded IPython code. + +This directive allows pasting of entire interactive IPython sessions, prompts +and all, and their code will actually get re-executed at doc build time, with +all prompts renumbered sequentially. It also allows you to input code as a pure +python input by giving the argument python to the directive. The output looks +like an interactive ipython section. + +To enable this directive, simply list it in your Sphinx ``conf.py`` file +(making sure the directory where you placed it is visible to sphinx, as is +needed for all Sphinx directives). + +By default this directive assumes that your prompts are unchanged IPython ones, +but this can be customized. The configurable options that can be placed in +conf.py are + +ipython_savefig_dir: + The directory in which to save the figures. This is relative to the + Sphinx source directory. The default is `html_static_path`. +ipython_rgxin: + The compiled regular expression to denote the start of IPython input + lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You + shouldn't need to change this. +ipython_rgxout: + The compiled regular expression to denote the start of IPython output + lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You + shouldn't need to change this. +ipython_promptin: + The string to represent the IPython input prompt in the generated ReST. + The default is 'In [%d]:'. This expects that the line numbers are used + in the prompt. +ipython_promptout: + + The string to represent the IPython prompt in the generated ReST. The + default is 'Out [%d]:'. This expects that the line numbers are used + in the prompt. + +ToDo +---- + +- Turn the ad-hoc test() function into a real test suite. +- Break up ipython-specific functionality from matplotlib stuff into better + separated code. + +Authors +------- + +- John D Hunter: orignal author. +- Fernando Perez: refactoring, documentation, cleanups, port to 0.11. +- VáclavŠmilauer : Prompt generalizations. +- Skipper Seabold, refactoring, cleanups, pure python addition +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Stdlib +import cStringIO +import os +import re +import sys +import tempfile +import ast + +# To keep compatibility with various python versions +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +# Third-party +import matplotlib +import sphinx +from docutils.parsers.rst import directives +from docutils import nodes +from sphinx.util.compat import Directive + +matplotlib.use('Agg') + +# Our own +from IPython import Config, InteractiveShell +from IPython.core.profiledir import ProfileDir +from IPython.utils import io + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +# for tokenizing blocks +COMMENT, INPUT, OUTPUT = range(3) + +#----------------------------------------------------------------------------- +# Functions and class declarations +#----------------------------------------------------------------------------- +def block_parser(part, rgxin, rgxout, fmtin, fmtout): + """ + part is a string of ipython text, comprised of at most one + input, one ouput, comments, and blank lines. The block parser + parses the text into a list of:: + + blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...] + + where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and + data is, depending on the type of token:: + + COMMENT : the comment string + + INPUT: the (DECORATOR, INPUT_LINE, REST) where + DECORATOR: the input decorator (or None) + INPUT_LINE: the input as string (possibly multi-line) + REST : any stdout generated by the input line (not OUTPUT) + + + OUTPUT: the output string, possibly multi-line + """ + + block = [] + lines = part.split('\n') + N = len(lines) + i = 0 + decorator = None + while 1: + + if i==N: + # nothing left to parse -- the last line + break + + line = lines[i] + i += 1 + line_stripped = line.strip() + if line_stripped.startswith('#'): + block.append((COMMENT, line)) + continue + + if line_stripped.startswith('@'): + # we're assuming at most one decorator -- may need to + # rethink + decorator = line_stripped + continue + + # does this look like an input line? + matchin = rgxin.match(line) + if matchin: + lineno, inputline = int(matchin.group(1)), matchin.group(2) + + # the ....: continuation string + continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) + Nc = len(continuation) + # input lines can continue on for more than one line, if + # we have a '\' line continuation char or a function call + # echo line 'print'. The input line can only be + # terminated by the end of the block or an output line, so + # we parse out the rest of the input line if it is + # multiline as well as any echo text + + rest = [] + while i 1: + if input_lines[-1] != "": + input_lines.append('') # make sure there's a blank line + # so splitter buffer gets reset + + continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) + Nc = len(continuation) + + if is_savefig: + image_file, image_directive = self.process_image(decorator) + + ret = [] + is_semicolon = False + + for i, line in enumerate(input_lines): + if line.endswith(';'): + is_semicolon = True + + if i==0: + # process the first input line + if is_verbatim: + self.process_input_line('') + self.IP.execution_count += 1 # increment it anyway + else: + # only submit the line in non-verbatim mode + self.process_input_line(line, store_history=True) + formatted_line = '%s %s'%(input_prompt, line) + else: + # process a continuation line + if not is_verbatim: + self.process_input_line(line, store_history=True) + + formatted_line = '%s %s'%(continuation, line) + + if not is_suppress: + ret.append(formatted_line) + + if not is_suppress and len(rest.strip()) and is_verbatim: + # the "rest" is the standard output of the + # input, which needs to be added in + # verbatim mode + ret.append(rest) + + self.cout.seek(0) + output = self.cout.read() + if not is_suppress and not is_semicolon: + ret.append(output) + elif is_semicolon: # get spacing right + ret.append('') + + self.cout.truncate(0) + return (ret, input_lines, output, is_doctest, image_file, + image_directive) + #print 'OUTPUT', output # dbg + + def process_output(self, data, output_prompt, + input_lines, output, is_doctest, image_file): + """Process data block for OUTPUT token.""" + if is_doctest: + submitted = data.strip() + found = output + if found is not None: + found = found.strip() + + # XXX - fperez: in 0.11, 'output' never comes with the prompt + # in it, just the actual output text. So I think all this code + # can be nuked... + + # the above comment does not appear to be accurate... (minrk) + + ind = found.find(output_prompt) + if ind<0: + e='output prompt="%s" does not match out line=%s' % \ + (output_prompt, found) + raise RuntimeError(e) + found = found[len(output_prompt):].strip() + + if found!=submitted: + e = ('doctest failure for input_lines="%s" with ' + 'found_output="%s" and submitted output="%s"' % + (input_lines, found, submitted) ) + raise RuntimeError(e) + #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted) + + def process_comment(self, data): + """Process data fPblock for COMMENT token.""" + if not self.is_suppress: + return [data] + + def save_image(self, image_file): + """ + Saves the image file to disk. + """ + self.ensure_pyplot() + command = 'plt.gcf().savefig("%s")'%image_file + #print 'SAVEFIG', command # dbg + self.process_input_line('bookmark ipy_thisdir', store_history=False) + self.process_input_line('cd -b ipy_savedir', store_history=False) + self.process_input_line(command, store_history=False) + self.process_input_line('cd -b ipy_thisdir', store_history=False) + self.process_input_line('bookmark -d ipy_thisdir', store_history=False) + self.clear_cout() + + + def process_block(self, block): + """ + process block from the block_parser and return a list of processed lines + """ + ret = [] + output = None + input_lines = None + lineno = self.IP.execution_count + + input_prompt = self.promptin%lineno + output_prompt = self.promptout%lineno + image_file = None + image_directive = None + + for token, data in block: + if token==COMMENT: + out_data = self.process_comment(data) + elif token==INPUT: + (out_data, input_lines, output, is_doctest, image_file, + image_directive) = \ + self.process_input(data, input_prompt, lineno) + elif token==OUTPUT: + out_data = \ + self.process_output(data, output_prompt, + input_lines, output, is_doctest, + image_file) + if out_data: + ret.extend(out_data) + + # save the image files + if image_file is not None: + self.save_image(image_file) + + return ret, image_directive + + def ensure_pyplot(self): + if self._pyplot_imported: + return + self.process_input_line('import matplotlib.pyplot as plt', + store_history=False) + + def process_pure_python(self, content): + """ + content is a list of strings. it is unedited directive conent + + This runs it line by line in the InteractiveShell, prepends + prompts as needed capturing stderr and stdout, then returns + the content as a list as if it were ipython code + """ + output = [] + savefig = False # keep up with this to clear figure + multiline = False # to handle line continuation + multiline_start = None + fmtin = self.promptin + + ct = 0 + + for lineno, line in enumerate(content): + + line_stripped = line.strip() + if not len(line): + output.append(line) + continue + + # handle decorators + if line_stripped.startswith('@'): + output.extend([line]) + if 'savefig' in line: + savefig = True # and need to clear figure + continue + + # handle comments + if line_stripped.startswith('#'): + output.extend([line]) + continue + + # deal with lines checking for multiline + continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2)) + if not multiline: + modified = u"%s %s" % (fmtin % ct, line_stripped) + output.append(modified) + ct += 1 + try: + ast.parse(line_stripped) + output.append(u'') + except Exception: # on a multiline + multiline = True + multiline_start = lineno + else: # still on a multiline + modified = u'%s %s' % (continuation, line) + output.append(modified) + try: + mod = ast.parse( + '\n'.join(content[multiline_start:lineno+1])) + if isinstance(mod.body[0], ast.FunctionDef): + # check to see if we have the whole function + for element in mod.body[0].body: + if isinstance(element, ast.Return): + multiline = False + else: + output.append(u'') + multiline = False + except Exception: + pass + + if savefig: # clear figure if plotted + self.ensure_pyplot() + self.process_input_line('plt.clf()', store_history=False) + self.clear_cout() + savefig = False + + return output + +class IpythonDirective(Directive): + + has_content = True + required_arguments = 0 + optional_arguments = 4 # python, suppress, verbatim, doctest + final_argumuent_whitespace = True + option_spec = { 'python': directives.unchanged, + 'suppress' : directives.flag, + 'verbatim' : directives.flag, + 'doctest' : directives.flag, + } + + shell = EmbeddedSphinxShell() + + def get_config_options(self): + # contains sphinx configuration variables + config = self.state.document.settings.env.config + + # get config variables to set figure output directory + confdir = self.state.document.settings.env.app.confdir + savefig_dir = config.ipython_savefig_dir + source_dir = os.path.dirname(self.state.document.current_source) + if savefig_dir is None: + savefig_dir = config.html_static_path + if isinstance(savefig_dir, list): + savefig_dir = savefig_dir[0] # safe to assume only one path? + savefig_dir = os.path.join(confdir, savefig_dir) + + # get regex and prompt stuff + rgxin = config.ipython_rgxin + rgxout = config.ipython_rgxout + promptin = config.ipython_promptin + promptout = config.ipython_promptout + + return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout + + def setup(self): + # reset the execution count if we haven't processed this doc + #NOTE: this may be borked if there are multiple seen_doc tmp files + #check time stamp? + seen_docs = [i for i in os.listdir(tempfile.tempdir) + if i.startswith('seen_doc')] + if seen_docs: + fname = os.path.join(tempfile.tempdir, seen_docs[0]) + docs = open(fname).read().split('\n') + if not self.state.document.current_source in docs: + self.shell.IP.history_manager.reset() + self.shell.IP.execution_count = 1 + else: # haven't processed any docs yet + docs = [] + + + # get config values + (savefig_dir, source_dir, rgxin, + rgxout, promptin, promptout) = self.get_config_options() + + # and attach to shell so we don't have to pass them around + self.shell.rgxin = rgxin + self.shell.rgxout = rgxout + self.shell.promptin = promptin + self.shell.promptout = promptout + self.shell.savefig_dir = savefig_dir + self.shell.source_dir = source_dir + + # setup bookmark for saving figures directory + + self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir, + store_history=False) + self.shell.clear_cout() + + # write the filename to a tempfile because it's been "seen" now + if not self.state.document.current_source in docs: + fd, fname = tempfile.mkstemp(prefix="seen_doc", text=True) + fout = open(fname, 'a') + fout.write(self.state.document.current_source+'\n') + fout.close() + + return rgxin, rgxout, promptin, promptout + + + def teardown(self): + # delete last bookmark + self.shell.process_input_line('bookmark -d ipy_savedir', + store_history=False) + self.shell.clear_cout() + + def run(self): + debug = False + + #TODO, any reason block_parser can't be a method of embeddable shell + # then we wouldn't have to carry these around + rgxin, rgxout, promptin, promptout = self.setup() + + options = self.options + self.shell.is_suppress = 'suppress' in options + self.shell.is_doctest = 'doctest' in options + self.shell.is_verbatim = 'verbatim' in options + + + # handle pure python code + if 'python' in self.arguments: + content = self.content + self.content = self.shell.process_pure_python(content) + + parts = '\n'.join(self.content).split('\n\n') + + lines = ['.. code-block:: ipython',''] + figures = [] + + for part in parts: + + block = block_parser(part, rgxin, rgxout, promptin, promptout) + + if len(block): + rows, figure = self.shell.process_block(block) + for row in rows: + lines.extend([' %s'%line for line in row.split('\n')]) + + if figure is not None: + figures.append(figure) + + #text = '\n'.join(lines) + #figs = '\n'.join(figures) + + for figure in figures: + lines.append('') + lines.extend(figure.split('\n')) + lines.append('') + + #print lines + if len(lines)>2: + if debug: + print '\n'.join(lines) + else: #NOTE: this raises some errors, what's it for? + #print 'INSERTING %d lines'%len(lines) + self.state_machine.insert_input( + lines, self.state_machine.input_lines.source(0)) + + text = '\n'.join(lines) + txtnode = nodes.literal_block(text, text) + txtnode['language'] = 'ipython' + #imgnode = nodes.image(figs) + + # cleanup + self.teardown() + + return []#, imgnode] + +# Enable as a proper Sphinx directive +def setup(app): + setup.app = app + + app.add_directive('ipython', IpythonDirective) + app.add_config_value('ipython_savefig_dir', None, True) + app.add_config_value('ipython_rgxin', + re.compile('In \[(\d+)\]:\s?(.*)\s*'), True) + app.add_config_value('ipython_rgxout', + re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True) + app.add_config_value('ipython_promptin', 'In [%d]:', True) + app.add_config_value('ipython_promptout', 'Out[%d]:', True) + + +# Simple smoke test, needs to be converted to a proper automatic test. +def test(): + + examples = [ + r""" +In [9]: pwd +Out[9]: '/home/jdhunter/py4science/book' + +In [10]: cd bookdata/ +/home/jdhunter/py4science/book/bookdata + +In [2]: from pylab import * + +In [2]: ion() + +In [3]: im = imread('stinkbug.png') + +@savefig mystinkbug.png width=4in +In [4]: imshow(im) +Out[4]: + +""", + r""" + +In [1]: x = 'hello world' + +# string methods can be +# used to alter the string +@doctest +In [2]: x.upper() +Out[2]: 'HELLO WORLD' + +@verbatim +In [3]: x.st +x.startswith x.strip +""", + r""" + +In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\ + .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv' + +In [131]: print url.split('&') +['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv'] + +In [60]: import urllib + +""", + r"""\ + +In [133]: import numpy.random + +@suppress +In [134]: numpy.random.seed(2358) + +@doctest +In [135]: numpy.random.rand(10,2) +Out[135]: +array([[ 0.64524308, 0.59943846], + [ 0.47102322, 0.8715456 ], + [ 0.29370834, 0.74776844], + [ 0.99539577, 0.1313423 ], + [ 0.16250302, 0.21103583], + [ 0.81626524, 0.1312433 ], + [ 0.67338089, 0.72302393], + [ 0.7566368 , 0.07033696], + [ 0.22591016, 0.77731835], + [ 0.0072729 , 0.34273127]]) + +""", + + r""" +In [106]: print x +jdh + +In [109]: for i in range(10): + .....: print i + .....: + .....: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +""", + + r""" + +In [144]: from pylab import * + +In [145]: ion() + +# use a semicolon to suppress the output +@savefig test_hist.png width=4in +In [151]: hist(np.random.randn(10000), 100); + + +@savefig test_plot.png width=4in +In [151]: plot(np.random.randn(10000), 'o'); + """, + + r""" +# use a semicolon to suppress the output +In [151]: plt.clf() + +@savefig plot_simple.png width=4in +In [151]: plot([1,2,3]) + +@savefig hist_simple.png width=4in +In [151]: hist(np.random.randn(10000), 100); + +""", + r""" +# update the current fig +In [151]: ylabel('number') + +In [152]: title('normal distribution') + + +@savefig hist_with_text.png +In [153]: grid(True) + + """, + ] + # skip local-file depending first example: + examples = examples[1:] + + #ipython_directive.DEBUG = True # dbg + #options = dict(suppress=True) # dbg + options = dict() + for example in examples: + content = example.split('\n') + ipython_directive('debug', arguments=None, options=options, + content=content, lineno=0, + content_offset=None, block_text=None, + state=None, state_machine=None, + ) + +# Run test suite as a script +if __name__=='__main__': + if not os.path.isdir('_static'): + os.mkdir('_static') + test() + print 'All OK? Check figures in _static/' diff --git a/doc/sphinxext/mathmpl.py b/doc/sphinxext/mathmpl.py new file mode 100644 index 00000000..0c126a66 --- /dev/null +++ b/doc/sphinxext/mathmpl.py @@ -0,0 +1,120 @@ +from __future__ import print_function +import os +import sys +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +from docutils import nodes +from docutils.parsers.rst import directives +import warnings + +from matplotlib import rcParams +from matplotlib.mathtext import MathTextParser +rcParams['mathtext.fontset'] = 'cm' +mathtext_parser = MathTextParser("Bitmap") + +# Define LaTeX math node: +class latex_math(nodes.General, nodes.Element): + pass + +def fontset_choice(arg): + return directives.choice(arg, ['cm', 'stix', 'stixsans']) + +options_spec = {'fontset': fontset_choice} + +def math_role(role, rawtext, text, lineno, inliner, + options={}, content=[]): + i = rawtext.find('`') + latex = rawtext[i+1:-1] + node = latex_math(rawtext) + node['latex'] = latex + node['fontset'] = options.get('fontset', 'cm') + return [node], [] +math_role.options = options_spec + +def math_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + latex = ''.join(content) + node = latex_math(block_text) + node['latex'] = latex + node['fontset'] = options.get('fontset', 'cm') + return [node] + +# This uses mathtext to render the expression +def latex2png(latex, filename, fontset='cm'): + latex = "$%s$" % latex + orig_fontset = rcParams['mathtext.fontset'] + rcParams['mathtext.fontset'] = fontset + if os.path.exists(filename): + depth = mathtext_parser.get_depth(latex, dpi=100) + else: + try: + depth = mathtext_parser.to_png(filename, latex, dpi=100) + except: + warnings.warn("Could not render math expression %s" % latex, + Warning) + depth = 0 + rcParams['mathtext.fontset'] = orig_fontset + sys.stdout.write("#") + sys.stdout.flush() + return depth + +# LaTeX to HTML translation stuff: +def latex2html(node, source): + inline = isinstance(node.parent, nodes.TextElement) + latex = node['latex'] + name = 'math-%s' % md5(latex).hexdigest()[-10:] + + destdir = os.path.join(setup.app.builder.outdir, '_images', 'mathmpl') + if not os.path.exists(destdir): + os.makedirs(destdir) + dest = os.path.join(destdir, '%s.png' % name) + path = os.path.join(setup.app.builder.imgpath, 'mathmpl') + + depth = latex2png(latex, dest, node['fontset']) + + if inline: + cls = '' + else: + cls = 'class="center" ' + if inline and depth != 0: + style = 'style="position: relative; bottom: -%dpx"' % (depth + 1) + else: + style = '' + + return '' % (path, name, cls, style) + +def setup(app): + setup.app = app + + app.add_node(latex_math) + app.add_role('math', math_role) + + # Add visit/depart methods to HTML-Translator: + def visit_latex_math_html(self, node): + source = self.document.attributes['source'] + self.body.append(latex2html(node, source)) + def depart_latex_math_html(self, node): + pass + + # Add visit/depart methods to LaTeX-Translator: + def visit_latex_math_latex(self, node): + inline = isinstance(node.parent, nodes.TextElement) + if inline: + self.body.append('$%s$' % node['latex']) + else: + self.body.extend(['\\begin{equation}', + node['latex'], + '\\end{equation}']) + def depart_latex_math_latex(self, node): + pass + + app.add_node(latex_math, html=(visit_latex_math_html, + depart_latex_math_html)) + app.add_node(latex_math, latex=(visit_latex_math_latex, + depart_latex_math_latex)) + app.add_role('math', math_role) + app.add_directive('math', math_directive, + True, (0, 0, 0), **options_spec) diff --git a/doc/sphinxext/numpydoc.py b/doc/sphinxext/numpydoc.py new file mode 100644 index 00000000..ff6c44c5 --- /dev/null +++ b/doc/sphinxext/numpydoc.py @@ -0,0 +1,116 @@ +""" +======== +numpydoc +======== + +Sphinx extension that handles docstrings in the Numpy standard format. [1] + +It will: + +- Convert Parameters etc. sections to field lists. +- Convert See Also section to a See also entry. +- Renumber references. +- Extract the signature from the docstring, if it can't be determined otherwise. + +.. [1] http://projects.scipy.org/scipy/numpy/wiki/CodingStyleGuidelines#docstring-standard + +""" + +import os, re, pydoc +from docscrape_sphinx import get_doc_object, SphinxDocString +import inspect + +def mangle_docstrings(app, what, name, obj, options, lines, + reference_offset=[0]): + if what == 'module': + # Strip top title + title_re = re.compile(r'^\s*[#*=]{4,}\n[a-z0-9 -]+\n[#*=]{4,}\s*', + re.I|re.S) + lines[:] = title_re.sub('', "\n".join(lines)).split("\n") + else: + doc = get_doc_object(obj, what, "\n".join(lines)) + lines[:] = str(doc).split("\n") + + if app.config.numpydoc_edit_link and hasattr(obj, '__name__') and \ + obj.__name__: + if hasattr(obj, '__module__'): + v = dict(full_name="%s.%s" % (obj.__module__, obj.__name__)) + else: + v = dict(full_name=obj.__name__) + lines += ['', '.. htmlonly::', ''] + lines += [' %s' % x for x in + (app.config.numpydoc_edit_link % v).split("\n")] + + # replace reference numbers so that there are no duplicates + references = [] + for l in lines: + l = l.strip() + if l.startswith('.. ['): + try: + references.append(int(l[len('.. ['):l.index(']')])) + except ValueError: + print "WARNING: invalid reference in %s docstring" % name + + # Start renaming from the biggest number, otherwise we may + # overwrite references. + references.sort() + if references: + for i, line in enumerate(lines): + for r in references: + new_r = reference_offset[0] + r + lines[i] = lines[i].replace('[%d]_' % r, + '[%d]_' % new_r) + lines[i] = lines[i].replace('.. [%d]' % r, + '.. [%d]' % new_r) + + reference_offset[0] += len(references) + +def mangle_signature(app, what, name, obj, options, sig, retann): + # Do not try to inspect classes that don't define `__init__` + if (inspect.isclass(obj) and + 'initializes x; see ' in pydoc.getdoc(obj.__init__)): + return '', '' + + if not (callable(obj) or hasattr(obj, '__argspec_is_invalid_')): return + if not hasattr(obj, '__doc__'): return + + doc = SphinxDocString(pydoc.getdoc(obj)) + if doc['Signature']: + sig = re.sub("^[^(]*", "", doc['Signature']) + return sig, '' + +def initialize(app): + try: + app.connect('autodoc-process-signature', mangle_signature) + except: + monkeypatch_sphinx_ext_autodoc() + +def setup(app, get_doc_object_=get_doc_object): + global get_doc_object + get_doc_object = get_doc_object_ + + app.connect('autodoc-process-docstring', mangle_docstrings) + app.connect('builder-inited', initialize) + app.add_config_value('numpydoc_edit_link', None, True) + +#------------------------------------------------------------------------------ +# Monkeypatch sphinx.ext.autodoc to accept argspecless autodocs (Sphinx < 0.5) +#------------------------------------------------------------------------------ + +def monkeypatch_sphinx_ext_autodoc(): + global _original_format_signature + import sphinx.ext.autodoc + + if sphinx.ext.autodoc.format_signature is our_format_signature: + return + + print "[numpydoc] Monkeypatching sphinx.ext.autodoc ..." + _original_format_signature = sphinx.ext.autodoc.format_signature + sphinx.ext.autodoc.format_signature = our_format_signature + +def our_format_signature(what, obj): + r = mangle_signature(None, what, None, obj, None, None, None) + if r is not None: + return r[0] + else: + return _original_format_signature(what, obj) diff --git a/doc/sphinxext/only_directives.py b/doc/sphinxext/only_directives.py new file mode 100644 index 00000000..9d8d0bb0 --- /dev/null +++ b/doc/sphinxext/only_directives.py @@ -0,0 +1,64 @@ +# +# A pair of directives for inserting content that will only appear in +# either html or latex. +# + +from __future__ import print_function +from docutils.nodes import Body, Element +from docutils.parsers.rst import directives + +class only_base(Body, Element): + def dont_traverse(self, *args, **kwargs): + return [] + +class html_only(only_base): + pass + +class latex_only(only_base): + pass + +def run(content, node_class, state, content_offset): + text = '\n'.join(content) + node = node_class(text) + state.nested_parse(content, content_offset, node) + return [node] + +def html_only_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(content, html_only, state, content_offset) + +def latex_only_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(content, latex_only, state, content_offset) + +def builder_inited(app): + if app.builder.name == 'html': + latex_only.traverse = only_base.dont_traverse + else: + html_only.traverse = only_base.dont_traverse + +def setup(app): + app.add_directive('htmlonly', html_only_directive, True, (0, 0, 0)) + app.add_directive('latexonly', latex_only_directive, True, (0, 0, 0)) + app.add_node(html_only) + app.add_node(latex_only) + + # This will *really* never see the light of day As it turns out, + # this results in "broken" image nodes since they never get + # processed, so best not to do this. + # app.connect('builder-inited', builder_inited) + + # Add visit/depart methods to HTML-Translator: + def visit_perform(self, node): + pass + def depart_perform(self, node): + pass + def visit_ignore(self, node): + node.children = [] + def depart_ignore(self, node): + node.children = [] + + app.add_node(html_only, html=(visit_perform, depart_perform)) + app.add_node(html_only, latex=(visit_ignore, depart_ignore)) + app.add_node(latex_only, latex=(visit_perform, depart_perform)) + app.add_node(latex_only, html=(visit_ignore, depart_ignore)) diff --git a/doc/sphinxext/plot_directive.py b/doc/sphinxext/plot_directive.py new file mode 100644 index 00000000..ac96d5fa --- /dev/null +++ b/doc/sphinxext/plot_directive.py @@ -0,0 +1,819 @@ +""" +A directive for including a matplotlib plot in a Sphinx document. + +By default, in HTML output, `plot` will include a .png file with a +link to a high-res .png and .pdf. In LaTeX output, it will include a +.pdf. + +The source code for the plot may be included in one of three ways: + + 1. **A path to a source file** as the argument to the directive:: + + .. plot:: path/to/plot.py + + When a path to a source file is given, the content of the + directive may optionally contain a caption for the plot:: + + .. plot:: path/to/plot.py + + This is the caption for the plot + + Additionally, one my specify the name of a function to call (with + no arguments) immediately after importing the module:: + + .. plot:: path/to/plot.py plot_function1 + + 2. Included as **inline content** to the directive:: + + .. plot:: + + import matplotlib.pyplot as plt + import matplotlib.image as mpimg + import numpy as np + img = mpimg.imread('_static/stinkbug.png') + imgplot = plt.imshow(img) + + 3. Using **doctest** syntax:: + + .. plot:: + A plotting example: + >>> import matplotlib.pyplot as plt + >>> plt.plot([1,2,3], [4,5,6]) + +Options +------- + +The ``plot`` directive supports the following options: + + format : {'python', 'doctest'} + Specify the format of the input + + include-source : bool + Whether to display the source code. The default can be changed + using the `plot_include_source` variable in conf.py + + encoding : str + If this source file is in a non-UTF8 or non-ASCII encoding, + the encoding must be specified using the `:encoding:` option. + The encoding will not be inferred using the ``-*- coding -*-`` + metacomment. + + context : bool + If provided, the code will be run in the context of all + previous plot directives for which the `:context:` option was + specified. This only applies to inline code plot directives, + not those run from files. + + nofigs : bool + If specified, the code block will be run, but no figures will + be inserted. This is usually useful with the ``:context:`` + option. + +Additionally, this directive supports all of the options of the +`image` directive, except for `target` (since plot will add its own +target). These include `alt`, `height`, `width`, `scale`, `align` and +`class`. + +Configuration options +--------------------- + +The plot directive has the following configuration options: + + plot_include_source + Default value for the include-source option + + plot_pre_code + Code that should be executed before each plot. + + plot_basedir + Base directory, to which ``plot::`` file names are relative + to. (If None or empty, file names are relative to the + directoly where the file containing the directive is.) + + plot_formats + File formats to generate. List of tuples or strings:: + + [(suffix, dpi), suffix, ...] + + that determine the file format and the DPI. For entries whose + DPI was omitted, sensible defaults are chosen. + + plot_html_show_formats + Whether to show links to the files in HTML. + + plot_rcparams + A dictionary containing any non-standard rcParams that should + be applied before each plot. + + plot_apply_rcparams + By default, rcParams are applied when `context` option is not used in + a plot directive. This configuration option overrides this behaviour + and applies rcParams before each plot. + + plot_working_directory + By default, the working directory will be changed to the directory of + the example, so the code can get at its data files, if any. Also its + path will be added to `sys.path` so it can import any helper modules + sitting beside it. This configuration option can be used to specify + a central directory (also added to `sys.path`) where data files and + helper modules for all code are located. + + plot_template + Provide a customized template for preparing resturctured text. + + +""" +from __future__ import print_function + +import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap, \ + traceback, exceptions + +from docutils.parsers.rst import directives +from docutils import nodes +from docutils.parsers.rst.directives.images import Image +align = Image.align +import sphinx + +sphinx_version = sphinx.__version__.split(".") +# The split is necessary for sphinx beta versions where the string is +# '6b1' +sphinx_version = tuple([int(re.split('[a-z]', x)[0]) + for x in sphinx_version[:2]]) + +try: + # Sphinx depends on either Jinja or Jinja2 + import jinja2 + def format_template(template, **kw): + return jinja2.Template(template).render(**kw) +except ImportError: + import jinja + def format_template(template, **kw): + return jinja.from_string(template, **kw) + +import matplotlib +import matplotlib.cbook as cbook +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib import _pylab_helpers + +__version__ = 2 + +#------------------------------------------------------------------------------ +# Relative pathnames +#------------------------------------------------------------------------------ + +# os.path.relpath is new in Python 2.6 +try: + from os.path import relpath +except ImportError: + # Copied from Python 2.7 + if 'posix' in sys.builtin_module_names: + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import sep, curdir, join, abspath, commonprefix, \ + pardir + + if not path: + raise ValueError("no path specified") + + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + + # Work out how much of the filepath is shared by start and path. + i = len(commonprefix([start_list, path_list])) + + rel_list = [pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + elif 'nt' in sys.builtin_module_names: + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import sep, curdir, join, abspath, commonprefix, \ + pardir, splitunc + + if not path: + raise ValueError("no path specified") + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + if start_list[0].lower() != path_list[0].lower(): + unc_path, rest = splitunc(path) + unc_start, rest = splitunc(start) + if bool(unc_path) ^ bool(unc_start): + raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" + % (path, start)) + else: + raise ValueError("path is on drive %s, start on drive %s" + % (path_list[0], start_list[0])) + # Work out how much of the filepath is shared by start and path. + for i in range(min(len(start_list), len(path_list))): + if start_list[i].lower() != path_list[i].lower(): + break + else: + i += 1 + + rel_list = [pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + else: + raise RuntimeError("Unsupported platform (no relpath available!)") + +#------------------------------------------------------------------------------ +# Registration hook +#------------------------------------------------------------------------------ + +def plot_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(arguments, content, options, state_machine, state, lineno) +plot_directive.__doc__ = __doc__ + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ('no', '0', 'false'): + return False + elif arg.strip().lower() in ('yes', '1', 'true'): + return True + else: + raise ValueError('"%s" unknown boolean' % arg) + +def _option_format(arg): + return directives.choice(arg, ('python', 'doctest')) + +def _option_align(arg): + return directives.choice(arg, ("top", "middle", "bottom", "left", "center", + "right")) + +def mark_plot_labels(app, document): + """ + To make plots referenceable, we need to move the reference from + the "htmlonly" (or "latexonly") node to the actual figure node + itself. + """ + for name, explicit in document.nametypes.iteritems(): + if not explicit: + continue + labelid = document.nameids[name] + if labelid is None: + continue + node = document.ids[labelid] + if node.tagname in ('html_only', 'latex_only'): + for n in node: + if n.tagname == 'figure': + sectname = name + for c in n: + if c.tagname == 'caption': + sectname = c.astext() + break + + node['ids'].remove(labelid) + node['names'].remove(name) + n['ids'].append(labelid) + n['names'].append(name) + document.settings.env.labels[name] = \ + document.settings.env.docname, labelid, sectname + break + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + options = {'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': _option_align, + 'class': directives.class_option, + 'include-source': _option_boolean, + 'format': _option_format, + 'context': directives.flag, + 'nofigs': directives.flag, + 'encoding': directives.encoding + } + + app.add_directive('plot', plot_directive, True, (0, 2, False), **options) + app.add_config_value('plot_pre_code', None, True) + app.add_config_value('plot_include_source', False, True) + app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) + app.add_config_value('plot_basedir', None, True) + app.add_config_value('plot_html_show_formats', True, True) + app.add_config_value('plot_rcparams', {}, True) + app.add_config_value('plot_apply_rcparams', False, True) + app.add_config_value('plot_working_directory', None, True) + app.add_config_value('plot_template', None, True) + + app.connect('doctree-read', mark_plot_labels) + +#------------------------------------------------------------------------------ +# Doctest handling +#------------------------------------------------------------------------------ + +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, '', 'exec') + return False + except SyntaxError: + pass + r = re.compile(r'^\s*>>>', re.M) + m = r.search(text) + return bool(m) + +def unescape_doctest(text): + """ + Extract code from a piece of text, which contains either Python code + or doctests. + + """ + if not contains_doctest(text): + return text + + code = "" + for line in text.split("\n"): + m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) + if m: + code += m.group(2) + "\n" + elif line.strip(): + code += "# " + line.strip() + "\n" + else: + code += "\n" + return code + +def split_code_at_show(text): + """ + Split code at plt.show() + + """ + + parts = [] + is_doctest = contains_doctest(text) + + part = [] + for line in text.split("\n"): + if (not is_doctest and line.strip() == 'plt.show()') or \ + (is_doctest and line.strip() == '>>> plt.show()'): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): + parts.append("\n".join(part)) + return parts + +#------------------------------------------------------------------------------ +# Template +#------------------------------------------------------------------------------ + + +TEMPLATE = """ +{{ source_code }} + +{{ only_html }} + + {% if source_link or (html_show_formats and not multi_image) %} + ( + {%- if source_link -%} + `Source code <{{ source_link }}>`__ + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if source_link or not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.png + {%- for option in options %} + {{ option }} + {% endfor %} + + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + ) + {%- endif -%} + + {{ caption }} + {% endfor %} + +{{ only_latex }} + + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.pdf + {% endfor %} + +{{ only_texinfo }} + + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.png + {%- for option in options %} + {{ option }} + {% endfor %} + + {% endfor %} + +""" + +exception_template = """ +.. htmlonly:: + + [`source code <%(linkdir)s/%(basename)s.py>`__] + +Exception occurred rendering plot. + +""" + +# the context of the plot for all directives specified with the +# :context: option +plot_context = dict() + +class ImageFile(object): + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, format): + return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + +def out_of_date(original, derived): + """ + Returns True if derivative is out-of-date wrt original, + both of which are full file paths. + """ + return (not os.path.exists(derived) or + (os.path.exists(original) and + os.stat(derived).st_mtime < os.stat(original).st_mtime)) + +class PlotError(RuntimeError): + pass + +def run_code(code, code_path, ns=None, function_name=None): + """ + Import a Python module from a path, and run the function given by + name, if function_name is not None. + """ + + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. Add its path to sys.path + # so it can import any helper modules sitting beside it. + + pwd = os.getcwd() + old_sys_path = list(sys.path) + if setup.config.plot_working_directory is not None: + try: + os.chdir(setup.config.plot_working_directory) + except OSError as err: + raise OSError(str(err) + '\n`plot_working_directory` option in' + 'Sphinx configuration file must be a valid ' + 'directory path') + except TypeError as err: + raise TypeError(str(err) + '\n`plot_working_directory` option in ' + 'Sphinx configuration file must be a string or ' + 'None') + sys.path.insert(0, setup.config.plot_working_directory) + elif code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + sys.path.insert(0, dirname) + + # Redirect stdout + stdout = sys.stdout + sys.stdout = cStringIO.StringIO() + + # Reset sys.argv + old_sys_argv = sys.argv + sys.argv = [code_path] + + try: + try: + code = unescape_doctest(code) + if ns is None: + ns = {} + if not ns: + if setup.config.plot_pre_code is None: + exec "import numpy as np\nfrom matplotlib import pyplot as plt\n" in ns + else: + exec setup.config.plot_pre_code in ns + if "__main__" in code: + exec "__name__ = '__main__'" in ns + exec code in ns + if function_name is not None: + exec function_name + "()" in ns + except (Exception, SystemExit), err: + raise PlotError(traceback.format_exc()) + finally: + os.chdir(pwd) + sys.argv = old_sys_argv + sys.path[:] = old_sys_path + sys.stdout = stdout + return ns + +def clear_state(plot_rcparams): + plt.close('all') + matplotlib.rc_file_defaults() + matplotlib.rcParams.update(plot_rcparams) + +def render_figures(code, code_path, output_dir, output_base, context, + function_name, config): + """ + Run a pyplot script and save the low and high res PNGs and a PDF + in outdir. + + Save the images under *output_dir* with file names derived from + *output_base* + """ + # -- Parse format list + default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} + formats = [] + plot_formats = config.plot_formats + if isinstance(plot_formats, (str, unicode)): + plot_formats = eval(plot_formats) + for fmt in plot_formats: + if isinstance(fmt, str): + formats.append((fmt, default_dpi.get(fmt, 80))) + elif type(fmt) in (tuple, list) and len(fmt)==2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise PlotError('invalid image format "%r" in plot_formats' % fmt) + + # -- Try to determine if all images already exist + + code_pieces = split_code_at_show(code) + + # Look for single-figure output files first + # Look for single-figure output files first + all_exists = True + img = ImageFile(output_base, output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + if all_exists: + return [(code, [img])] + + # Then look for multi-figure output files + results = [] + all_exists = True + for i, code_piece in enumerate(code_pieces): + images = [] + for j in xrange(1000): + if len(code_pieces) > 1: + img = ImageFile('%s_%02d_%02d' % (output_base, i, j), output_dir) + else: + img = ImageFile('%s_%02d' % (output_base, j), output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + # assume that if we have one, we have them all + if not all_exists: + all_exists = (j > 0) + break + images.append(img) + if not all_exists: + break + results.append((code_piece, images)) + + if all_exists: + return results + + # We didn't find the files, so build them + + results = [] + if context: + ns = plot_context + else: + ns = {} + + for i, code_piece in enumerate(code_pieces): + if not context or config.plot_apply_rcparams: + clear_state(config.plot_rcparams) + run_code(code_piece, code_path, ns, function_name) + + images = [] + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for j, figman in enumerate(fig_managers): + if len(fig_managers) == 1 and len(code_pieces) == 1: + img = ImageFile(output_base, output_dir) + elif len(code_pieces) == 1: + img = ImageFile("%s_%02d" % (output_base, j), output_dir) + else: + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), + output_dir) + images.append(img) + for format, dpi in formats: + try: + figman.canvas.figure.savefig(img.filename(format), dpi=dpi) + except Exception,err: + raise PlotError(traceback.format_exc()) + img.formats.append(format) + + results.append((code_piece, images)) + + if not context or config.plot_apply_rcparams: + clear_state(config.plot_rcparams) + + return results + +def run(arguments, content, options, state_machine, state, lineno): + # The user may provide a filename *or* Python code content, but not both + if arguments and content: + raise RuntimeError("plot:: directive can't have both args and content") + + document = state_machine.document + config = document.settings.env.config + nofigs = options.has_key('nofigs') + + options.setdefault('include-source', config.plot_include_source) + context = options.has_key('context') + + rst_file = document.attributes['source'] + rst_dir = os.path.dirname(rst_file) + + if len(arguments): + if not config.plot_basedir: + source_file_name = os.path.join(setup.app.builder.srcdir, + directives.uri(arguments[0])) + else: + source_file_name = os.path.join(setup.confdir, config.plot_basedir, + directives.uri(arguments[0])) + + # If there is content, it will be passed as a caption. + caption = '\n'.join(content) + + # If the optional function name is provided, use it + if len(arguments) == 2: + function_name = arguments[1] + else: + function_name = None + + with open(source_file_name, 'r') as fd: + code = fd.read() + output_base = os.path.basename(source_file_name) + else: + source_file_name = rst_file + code = textwrap.dedent("\n".join(map(str, content))) + counter = document.attributes.get('_plot_counter', 0) + 1 + document.attributes['_plot_counter'] = counter + base, ext = os.path.splitext(os.path.basename(source_file_name)) + output_base = '%s-%d.py' % (base, counter) + function_name = None + caption = '' + + base, source_ext = os.path.splitext(output_base) + if source_ext in ('.py', '.rst', '.txt'): + output_base = base + else: + source_ext = '' + + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace('.', '-') + + # is it in doctest format? + is_doctest = contains_doctest(code) + if options.has_key('format'): + if options['format'] == 'python': + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name) + while source_rel_dir.startswith(os.path.sep): + source_rel_dir = source_rel_dir[1:] + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), + 'plot_directive', + source_rel_dir) + # get rid of .. in paths, also changes pathsep + # see note in Python docs for warning about symbolic links on Windows. + # need to compare source and dest paths at end + build_dir = os.path.normpath(build_dir) + + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # output_dir: final location in the builder's directory + dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, + source_rel_dir)) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) # no problem here for me, but just use built-ins + + # how to link to files from the RST file + dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), + source_rel_dir).replace(os.path.sep, '/') + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') + source_link = dest_dir_link + '/' + output_base + source_ext + + # make figures + try: + results = render_figures(code, source_file_name, build_dir, output_base, + context, function_name, config) + errors = [] + except PlotError, err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting %s\n from %s:\n%s" % (output_base, + source_file_name, err), + line=lineno) + results = [(code, [])] + errors = [sm] + + # Properly indent the caption + caption = '\n'.join(' ' + line.strip() + for line in caption.split('\n')) + + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options['include-source']: + if is_doctest: + lines = [''] + lines += [row.rstrip() for row in code_piece.split('\n')] + else: + lines = ['.. code-block:: python', ''] + lines += [' %s' % row.rstrip() + for row in code_piece.split('\n')] + source_code = "\n".join(lines) + else: + source_code = "" + + if nofigs: + images = [] + + opts = [':%s: %s' % (key, val) for key, val in options.items() + if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] + + only_html = ".. only:: html" + only_latex = ".. only:: latex" + only_texinfo = ".. only:: texinfo" + + if j == 0: + src_link = source_link + else: + src_link = None + + result = format_template( + config.plot_template or TEMPLATE, + dest_dir=dest_dir_link, + build_dir=build_dir_link, + source_link=src_link, + multi_image=len(images) > 1, + only_html=only_html, + only_latex=only_latex, + only_texinfo=only_texinfo, + options=opts, + images=images, + source_code=source_code, + html_show_formats=config.plot_html_show_formats, + caption=caption) + + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + # copy image files to builder's output directory, if necessary + if not os.path.exists(dest_dir): + cbook.mkdirs(dest_dir) + + for code_piece, images in results: + for img in images: + for fn in img.filenames(): + destimg = os.path.join(dest_dir, os.path.basename(fn)) + if fn != destimg: + shutil.copyfile(fn, destimg) + + # copy script (if necessary) + target_name = os.path.join(dest_dir, output_base + source_ext) + with open(target_name, 'w') as f: + if source_file_name == rst_file: + code_escaped = unescape_doctest(code) + else: + code_escaped = code + f.write(code_escaped) + + return errors From a1746a6ab9e7f31ced28a05e5bd7a5a1d5f61363 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:35:32 +0000 Subject: [PATCH 076/197] removed some mocks --- doc/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index e48e286a..4925815d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab']#, 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() @@ -141,10 +141,10 @@ sys.path.append(os.path.abspath('sphinxext')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - #'matplotlib.sphinxext.mathmpl', - #'matplotlib.sphinxext.only_directives', + 'matplotlib.sphinxext.mathmpl', + 'matplotlib.sphinxext.only_directives', 'matplotlib.sphinxext.plot_directive', - #'matplotlib.sphinxext.ipython_directive', + 'matplotlib.sphinxext.ipython_directive', 'sphinx.ext.doctest', 'ipython_console_highlighting', 'inheritance_diagram', From 3a6aab1060f9c883e9053cdad77a6ae9bfab27cd Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:37:05 +0000 Subject: [PATCH 077/197] Removed soem more extensions --- doc/conf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 4925815d..78480de4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -144,11 +144,12 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'matplotlib.sphinxext.mathmpl', 'matplotlib.sphinxext.only_directives', 'matplotlib.sphinxext.plot_directive', - 'matplotlib.sphinxext.ipython_directive', - 'sphinx.ext.doctest', - 'ipython_console_highlighting', - 'inheritance_diagram', - 'numpydoc'] + 'matplotlib.sphinxext.ipython_directive' + ] + #'sphinx.ext.doctest', + #'ipython_console_highlighting', + #'inheritance_diagram', + #'numpydoc'] # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' From 235b390ec8ccece42d05e3d1ef426ac1d31557a4 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:40:29 +0000 Subject: [PATCH 078/197] Added some debugging statements --- doc/conf.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 78480de4..11f065ad 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,7 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock -MOCK_MODULES = ['pylab']#, 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() @@ -140,16 +140,18 @@ sys.path.append(os.path.abspath('sphinxext')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +print "Importing extensions" extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'matplotlib.sphinxext.mathmpl', - 'matplotlib.sphinxext.only_directives', + #'matplotlib.sphinxext.mathmpl', + #'matplotlib.sphinxext.only_directives', 'matplotlib.sphinxext.plot_directive', - 'matplotlib.sphinxext.ipython_directive' + #'matplotlib.sphinxext.ipython_directive' ] #'sphinx.ext.doctest', #'ipython_console_highlighting', #'inheritance_diagram', #'numpydoc'] +print "finished importing" # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' @@ -160,6 +162,7 @@ if on_rtd: os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") #os.system("cd ./docs") +print "Compiled files # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 137a14c5e513b49ff9d264bf667fddee55caf025 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:41:55 +0000 Subject: [PATCH 079/197] more debugging --- doc/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 11f065ad..fceebf5b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,6 +114,7 @@ class Mock(object): #sys.path.append("../GPy") #import mock +print "Mocking" MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() @@ -123,6 +124,7 @@ for mod_name in MOCK_MODULES: # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('..')) +print "Adding path" # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. @@ -162,7 +164,8 @@ if on_rtd: os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") #os.system("cd ./docs") -print "Compiled files + +print "Compiled files" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 8c2adc1dce89541031c15193224a4e9bddb5e20b Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:45:43 +0000 Subject: [PATCH 080/197] Trying ipython extension instead --- doc/tuto_GP_regression.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index a713eb4c..f0d11ac1 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -147,12 +147,9 @@ The flag ``ARD=True`` in the definition of the Matern kernel specifies that we w Contour plot of the best predictor (posterior mean). -.. plot:: +.. ipython:: - import matplotlib.pyplot as plt - import numpy as np - x = np.random.randn(1000) - plt.hist( x, 20) - plt.grid() - plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) - plt.show() + In [136]: x = 2 + + In [137]: x**3 + Out[137]: 8 From 7f5200e4ba1bf52ca00697a20a398ba440d3e8b9 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:46:34 +0000 Subject: [PATCH 081/197] Added other ipython directive extension --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index fceebf5b..5e87d0b0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -146,8 +146,9 @@ print "Importing extensions" extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', - 'matplotlib.sphinxext.plot_directive', - #'matplotlib.sphinxext.ipython_directive' + #'matplotlib.sphinxext.plot_directive', + 'matplotlib.sphinxext.ipython_directive', + 'ipython_directive' ] #'sphinx.ext.doctest', #'ipython_console_highlighting', From 87f7a8bd4fa230bc61e5c32bb2ff43fdad788ffe Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:47:58 +0000 Subject: [PATCH 082/197] Checking before any extensions --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 5e87d0b0..1f31b3b6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -128,7 +128,7 @@ print "Adding path" # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -sys.path.append(os.path.abspath('sphinxext')) +#sys.path.append(os.path.abspath('sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -147,8 +147,8 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', - 'matplotlib.sphinxext.ipython_directive', - 'ipython_directive' + #'matplotlib.sphinxext.ipython_directive', + #'ipython_directive' ] #'sphinx.ext.doctest', #'ipython_console_highlighting', From 5f35f1553ceb6517acdc0b9f8847ab5ae7b50939 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:52:56 +0000 Subject: [PATCH 083/197] Still working? --- doc/conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 1f31b3b6..1432d6be 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -128,7 +128,7 @@ print "Adding path" # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.append(os.path.abspath('sphinxext')) +sys.path.append(os.path.abspath('./sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -143,11 +143,12 @@ print "Adding path" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', + +extensions = [#'ipython_directive', + 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', - #'matplotlib.sphinxext.ipython_directive', #'ipython_directive' ] #'sphinx.ext.doctest', From 507e5df9bca2a56ca9c1cb315487e23bcd1d7700 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:54:02 +0000 Subject: [PATCH 084/197] Still working? --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 1432d6be..eca47319 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -144,7 +144,7 @@ sys.path.append(os.path.abspath('./sphinxext')) # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -extensions = [#'ipython_directive', +extensions = ['ipython_directive', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', From ea014e4b86ad503088201938312e4f307794d229 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:55:11 +0000 Subject: [PATCH 085/197] Added sphinxext package to ipython_directive --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index eca47319..acca9622 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -144,7 +144,7 @@ sys.path.append(os.path.abspath('./sphinxext')) # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -extensions = ['ipython_directive', +extensions = ['sphinxext.ipython_directive', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', From baa39a1c0558502e89c0c2fd40eb7791fd01afb3 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:57:35 +0000 Subject: [PATCH 086/197] Changed to matplotlib sphinxext --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index acca9622..89647201 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -144,7 +144,8 @@ sys.path.append(os.path.abspath('./sphinxext')) # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -extensions = ['sphinxext.ipython_directive', +print sys.path +extensions = ['matplotlib.sphinxext.ipython_directive', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', From dad399d0128c1c967a0c67f1f273628fbed741c8 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 15:58:56 +0000 Subject: [PATCH 087/197] Changed docs --- doc/tuto_GP_regression.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index f0d11ac1..d468c6aa 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -147,9 +147,4 @@ The flag ``ARD=True`` in the definition of the Matern kernel specifies that we w Contour plot of the best predictor (posterior mean). -.. ipython:: - - In [136]: x = 2 - - In [137]: x**3 - Out[137]: 8 +Changed it From a82605e6f9a134c908ab9f508b7d43407747e52e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 16:08:22 +0000 Subject: [PATCH 088/197] Moved mock below extension loading --- doc/conf.py | 57 +++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 89647201..5121cedb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -90,34 +90,6 @@ import sys, os #dtype=lambda _: Mock(_mock_repr='np.dtype(\'float32\')')) #sys.modules['scipy.constants'] = Mock(pi=math.pi, G=6.67364e-11) -############################################################################## -## -## Mock out imports with C dependencies because ReadTheDocs can't build them. -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - -#sys.path.append("../GPy") -#import mock - -print "Mocking" -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -158,6 +130,35 @@ extensions = ['matplotlib.sphinxext.ipython_directive', #'numpydoc'] print "finished importing" +############################################################################## +## +## Mock out imports with C dependencies because ReadTheDocs can't build them. +class Mock(object): + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return Mock() + + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + mockType = type(name, (), {}) + mockType.__module__ = __name__ + return mockType + else: + return Mock() + +#sys.path.append("../GPy") +#import mock + +print "Mocking" +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' From 36535286bf1362223e6a76192fd7c29608089685 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 16:10:04 +0000 Subject: [PATCH 089/197] Insert to beginning of path --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 5121cedb..ff8f5969 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -100,12 +100,13 @@ print "Adding path" # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -sys.path.append(os.path.abspath('./sphinxext')) +#sys.path.append(os.path.abspath('./sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('./sphinxext')) # -- General configuration ----------------------------------------------------- @@ -117,7 +118,7 @@ sys.path.append(os.path.abspath('./sphinxext')) print "Importing extensions" print sys.path -extensions = ['matplotlib.sphinxext.ipython_directive', +extensions = ['ipython_directive', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', From 3a052cd5ebeb89dc1244bd25be5b4b6c3f49cfa6 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Thu, 31 Jan 2013 16:13:27 +0000 Subject: [PATCH 090/197] Reverted back to working docs --- doc/conf.py | 5 ++--- doc/tuto_GP_regression.rst | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ff8f5969..2a02e18d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -106,7 +106,7 @@ print "Adding path" # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('./sphinxext')) +#sys.path.insert(0, os.path.abspath('./sphinxext')) # -- General configuration ----------------------------------------------------- @@ -117,8 +117,7 @@ sys.path.insert(0, os.path.abspath('./sphinxext')) # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -print sys.path -extensions = ['ipython_directive', +extensions = [#'ipython_directive', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index d468c6aa..7d1a43df 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -146,5 +146,3 @@ The flag ``ARD=True`` in the definition of the Matern kernel specifies that we w :height: 350px Contour plot of the best predictor (posterior mean). - -Changed it From a510524620a7c47f0cb8494452b678310f9d9ec3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 31 Jan 2013 17:19:15 +0000 Subject: [PATCH 091/197] small changes in the way covariance functions handle lengthscale as input --- GPy/kern/Matern32.py | 19 ++++++++++--------- GPy/kern/Matern52.py | 14 ++++++++------ GPy/kern/exponential.py | 25 +++++++++++-------------- GPy/kern/linear.py | 17 ++++++++--------- GPy/kern/rbf.py | 4 ++-- 5 files changed, 39 insertions(+), 40 deletions(-) diff --git a/GPy/kern/Matern32.py b/GPy/kern/Matern32.py index cfad17c9..eacb70ab 100644 --- a/GPy/kern/Matern32.py +++ b/GPy/kern/Matern32.py @@ -14,14 +14,14 @@ class Matern32(kernpart): .. math:: - k(r) = \sigma^2 (1 + \sqrt{3} r) \exp(- \sqrt{3} r) \qquad \qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 (1 + \sqrt{3} r) \exp(- \sqrt{3} r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int :param variance: the variance :math:`\sigma^2` :type variance: float :param lengthscale: the vector of lengthscale :math:`\ell_i` - :type lengthscale: np.ndarray of size (1,) or (D,) depending on ARD + :type lengthscale: array or list of the appropriate size (or float if there is only one lengthscale parameter) :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single lengthscale parameter \ell), otherwise there is one lengthscale parameter per dimension. :type ARD: Boolean :rtype: kernel object @@ -35,17 +35,19 @@ class Matern32(kernpart): self.Nparam = 2 self.name = 'Mat32' if lengthscale is not None: - assert lengthscale.shape == (1,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == 1, "Only one lengthscale needed for non-ARD kernel" else: lengthscale = np.ones(1) else: self.Nparam = self.D + 1 - self.name = 'Mat32_ARD' + self.name = 'Mat32' if lengthscale is not None: - assert lengthscale.shape == (self.D,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == self.D, "bad number of lengthscales" else: lengthscale = np.ones(self.D) - self._set_params(np.hstack((variance,lengthscale))) + self._set_params(np.hstack((variance,lengthscale.flatten()))) def _get_params(self): """return the value of the parameters.""" @@ -116,9 +118,9 @@ class Matern32(kernpart): :param F1: vector of derivatives of F :type F1: np.array :param F2: vector of second derivatives of F - :type F2: np.array + :type F2: np.array :param lower,upper: boundaries of the input domain - :type lower,upper: floats + :type lower,upper: floats """ assert self.D == 1 def L(x,i): @@ -133,4 +135,3 @@ class Matern32(kernpart): #print "OLD \n", np.dot(F1lower,F1lower.T), "\n \n" #return(G) return(self.lengthscale**3/(12.*np.sqrt(3)*self.variance) * G + 1./self.variance*np.dot(Flower,Flower.T) + self.lengthscale**2/(3.*self.variance)*np.dot(F1lower,F1lower.T)) - diff --git a/GPy/kern/Matern52.py b/GPy/kern/Matern52.py index cbe02c83..c7478653 100644 --- a/GPy/kern/Matern52.py +++ b/GPy/kern/Matern52.py @@ -13,14 +13,14 @@ class Matern52(kernpart): .. math:: - k(r) = \sigma^2 (1 + \sqrt{5} r + \\frac53 r^2) \exp(- \sqrt{5} r) \qquad \qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 (1 + \sqrt{5} r + \\frac53 r^2) \exp(- \sqrt{5} r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int :param variance: the variance :math:`\sigma^2` :type variance: float :param lengthscale: the vector of lengthscale :math:`\ell_i` - :type lengthscale: np.ndarray of size (1,) or (D,) depending on ARD + :type lengthscale: array or list of the appropriate size (or float if there is only one lengthscale parameter) :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single lengthscale parameter \ell), otherwise there is one lengthscale parameter per dimension. :type ARD: Boolean :rtype: kernel object @@ -33,17 +33,19 @@ class Matern52(kernpart): self.Nparam = 2 self.name = 'Mat52' if lengthscale is not None: - assert lengthscale.shape == (1,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == 1, "Only one lengthscale needed for non-ARD kernel" else: lengthscale = np.ones(1) else: self.Nparam = self.D + 1 - self.name = 'Mat52_ARD' + self.name = 'Mat52' if lengthscale is not None: - assert lengthscale.shape == (self.D,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == self.D, "bad number of lengthscales" else: lengthscale = np.ones(self.D) - self._set_params(np.hstack((variance,lengthscale))) + self._set_params(np.hstack((variance,lengthscale.flatten()))) def _get_params(self): """return the value of the parameters.""" diff --git a/GPy/kern/exponential.py b/GPy/kern/exponential.py index 6c463a63..c812dc79 100644 --- a/GPy/kern/exponential.py +++ b/GPy/kern/exponential.py @@ -13,14 +13,14 @@ class exponential(kernpart): .. math:: - k(r) = \sigma^2 \exp(- r) \qquad \qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 \exp(- r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int :param variance: the variance :math:`\sigma^2` :type variance: float :param lengthscale: the vector of lengthscale :math:`\ell_i` - :type lengthscale: np.ndarray of size (1,) or (D,) depending on ARD + :type lengthscale: array or list of the appropriate size (or float if there is only one lengthscale parameter) :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single lengthscale parameter \ell), otherwise there is one lengthscale parameter per dimension. :type ARD: Boolean :rtype: kernel object @@ -33,17 +33,19 @@ class exponential(kernpart): self.Nparam = 2 self.name = 'exp' if lengthscale is not None: - assert lengthscale.shape == (1,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == 1, "Only one lengthscale needed for non-ARD kernel" else: lengthscale = np.ones(1) else: self.Nparam = self.D + 1 - self.name = 'exp_ARD' + self.name = 'exp' if lengthscale is not None: - assert lengthscale.shape == (self.D,) + lengthscale = np.asarray(lengthscale) + assert lengthscale.size == self.D, "bad number of lengthscales" else: lengthscale = np.ones(self.D) - self._set_params(np.hstack((variance,lengthscale))) + self._set_params(np.hstack((variance,lengthscale.flatten()))) def _get_params(self): """return the value of the parameters.""" @@ -87,7 +89,7 @@ class exponential(kernpart): dl = self.variance*dvar*dist2M.sum(-1)*invdist target[1] += np.sum(dl*partial) - def dKdiag_dtheta(self,partial,X,target): + def dKdiag_dtheta(self,partial,X,target): """derivative of the diagonal of the covariance matrix with respect to the parameters.""" #NB: derivative of diagonal elements wrt lengthscale is 0 target[0] += np.sum(partial) @@ -110,9 +112,9 @@ class exponential(kernpart): :param F: vector of functions :type F: np.array :param F1: vector of derivatives of F - :type F1: np.array + :type F1: np.array :param lower,upper: boundaries of the input domain - :type lower,upper: floats + :type lower,upper: floats """ assert self.D == 1 def L(x,i): @@ -124,8 +126,3 @@ class exponential(kernpart): G[i,j] = G[j,i] = integrate.quad(lambda x : L(x,i)*L(x,j),lower,upper)[0] Flower = np.array([f(lower) for f in F])[:,None] return(self.lengthscale/2./self.variance * G + 1./self.variance * np.dot(Flower,Flower.T)) - - - - - diff --git a/GPy/kern/linear.py b/GPy/kern/linear.py index d36e40b7..459b5a71 100644 --- a/GPy/kern/linear.py +++ b/GPy/kern/linear.py @@ -15,8 +15,8 @@ class linear(kernpart): :param D: the number of input dimensions :type D: int :param variances: the vector of variances :math:`\sigma^2_i` - :type variances: np.ndarray of size (1,) or (D,) depending on ARD - :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single variance parameter \sigma^2), otherwise there is one variance parameter per dimension. + :type variances: array or list of the appropriate size (or float if there is only one variance parameter) + :param ARD: Auto Relevance Determination. If equal to "False", the kernel has only one variance parameter \sigma^2, otherwise there is one variance parameter per dimension. :type ARD: Boolean :rtype: kernel object """ @@ -28,21 +28,20 @@ class linear(kernpart): self.Nparam = 1 self.name = 'linear' if variances is not None: - if isinstance(variances, float): - variances = np.array([variances]) - - assert variances.shape == (1,) + variances = np.asarray(variances) + assert variances.size == 1, "Only one variance needed for non-ARD kernel" else: variances = np.ones(1) self._Xcache, self._X2cache = np.empty(shape=(2,)) else: self.Nparam = self.D - self.name = 'linear_ARD' + self.name = 'linear' if variances is not None: - assert variances.shape == (self.D,) + variances = np.asarray(variances) + assert variances.size == self.D, "bad number of lengthscales" else: variances = np.ones(self.D) - self._set_params(variances) + self._set_params(variances.flatten()) def _get_params(self): return self.variances diff --git a/GPy/kern/rbf.py b/GPy/kern/rbf.py index 1506b323..68fa5f55 100644 --- a/GPy/kern/rbf.py +++ b/GPy/kern/rbf.py @@ -12,7 +12,7 @@ class rbf(kernpart): .. math:: - k(r) = \sigma^2 \exp(- \frac{1}{2}r^2) \qquad \qquad \\text{ where } r^2 = \sum_{i=1}^d \frac{ (x_i-x^\prime_i)^2}{\ell_i^2}} + k(r) = \sigma^2 \exp(- \frac{1}{2}r^2) \\qquad \\qquad \\text{ where } r^2 = \sum_{i=1}^d \frac{ (x_i-x^\prime_i)^2}{\ell_i^2}} where \ell_i is the lengthscale, \sigma^2 the variance and d the dimensionality of the input. @@ -21,7 +21,7 @@ class rbf(kernpart): :param variance: the variance of the kernel :type variance: float :param lengthscale: the vector of lengthscale of the kernel - :type lengthscale: np.ndarray od size (1,) or (D,) depending on ARD + :type lengthscale: array or list of the appropriate size (or float if there is only one lengthscale parameter) :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single lengthscale parameter \ell), otherwise there is one lengthscale parameter per dimension. :type ARD: Boolean :rtype: kernel object From b25e123113745c2b8e2df58f6975e743cf27ee26 Mon Sep 17 00:00:00 2001 From: Nicolo Fusi Date: Thu, 31 Jan 2013 17:24:53 +0000 Subject: [PATCH 092/197] changed default ARD setting in linear --- GPy/kern/linear.py | 2 +- GPy/kern/rbf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/GPy/kern/linear.py b/GPy/kern/linear.py index d36e40b7..52bc1757 100644 --- a/GPy/kern/linear.py +++ b/GPy/kern/linear.py @@ -21,7 +21,7 @@ class linear(kernpart): :rtype: kernel object """ - def __init__(self,D,variances=None,ARD=True): + def __init__(self,D,variances=None,ARD=False): self.D = D self.ARD = ARD if ARD == False: diff --git a/GPy/kern/rbf.py b/GPy/kern/rbf.py index 1506b323..3143c244 100644 --- a/GPy/kern/rbf.py +++ b/GPy/kern/rbf.py @@ -75,6 +75,7 @@ class rbf(kernpart): def K(self,X,X2,target): if X2 is None: X2 = X + self._K_computations(X,X2) np.add(self.variance*self._K_dvar, target,target) From 666f334a67b6747390ed32b4a8501f2fcccfa226 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 31 Jan 2013 17:36:01 +0000 Subject: [PATCH 093/197] small fixes in the kernel documentation --- GPy/kern/Matern32.py | 2 +- GPy/kern/Matern52.py | 2 +- GPy/kern/exponential.py | 2 +- GPy/kern/rbf.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GPy/kern/Matern32.py b/GPy/kern/Matern32.py index eacb70ab..3af7ec05 100644 --- a/GPy/kern/Matern32.py +++ b/GPy/kern/Matern32.py @@ -14,7 +14,7 @@ class Matern32(kernpart): .. math:: - k(r) = \sigma^2 (1 + \sqrt{3} r) \exp(- \sqrt{3} r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 (1 + \sqrt{3} r) \exp(- \sqrt{3} r) \ \ \ \ \ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int diff --git a/GPy/kern/Matern52.py b/GPy/kern/Matern52.py index c7478653..2994fc45 100644 --- a/GPy/kern/Matern52.py +++ b/GPy/kern/Matern52.py @@ -13,7 +13,7 @@ class Matern52(kernpart): .. math:: - k(r) = \sigma^2 (1 + \sqrt{5} r + \\frac53 r^2) \exp(- \sqrt{5} r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 (1 + \sqrt{5} r + \\frac53 r^2) \exp(- \sqrt{5} r) \ \ \ \ \ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int diff --git a/GPy/kern/exponential.py b/GPy/kern/exponential.py index c812dc79..3c9cb192 100644 --- a/GPy/kern/exponential.py +++ b/GPy/kern/exponential.py @@ -13,7 +13,7 @@ class exponential(kernpart): .. math:: - k(r) = \sigma^2 \exp(- r) \\qquad \\qquad \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \sigma^2 \exp(- r) \ \ \ \ \ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int diff --git a/GPy/kern/rbf.py b/GPy/kern/rbf.py index 082bbe7f..5babfa4f 100644 --- a/GPy/kern/rbf.py +++ b/GPy/kern/rbf.py @@ -12,7 +12,7 @@ class rbf(kernpart): .. math:: - k(r) = \sigma^2 \exp(- \frac{1}{2}r^2) \\qquad \\qquad \\text{ where } r^2 = \sum_{i=1}^d \frac{ (x_i-x^\prime_i)^2}{\ell_i^2}} + k(r) = \sigma^2 \exp(- \frac{1}{2}r^2) \ \ \ \ \ \\text{ where } r^2 = \sum_{i=1}^d \frac{ (x_i-x^\prime_i)^2}{\ell_i^2}} where \ell_i is the lengthscale, \sigma^2 the variance and d the dimensionality of the input. From e995fb1f890e6ea2ea039343dbd32adb6f0edbc4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 31 Jan 2013 17:52:19 +0000 Subject: [PATCH 094/197] trying to fix doc --- GPy/kern/Matern32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/kern/Matern32.py b/GPy/kern/Matern32.py index 3af7ec05..87f402f2 100644 --- a/GPy/kern/Matern32.py +++ b/GPy/kern/Matern32.py @@ -14,7 +14,7 @@ class Matern32(kernpart): .. math:: - k(r) = \sigma^2 (1 + \sqrt{3} r) \exp(- \sqrt{3} r) \ \ \ \ \ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \\sigma^2 (1 + \\sqrt{3} r) \exp(- \sqrt{3} r) \\ \\ \\ \\ \\ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int From 19eb6907c679e7717050bcba5160d378f0dc9bb0 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 31 Jan 2013 17:57:56 +0000 Subject: [PATCH 095/197] trying to fix doc 2 --- GPy/kern/Matern32.py | 2 +- doc/conf.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/GPy/kern/Matern32.py b/GPy/kern/Matern32.py index 87f402f2..a5d70a62 100644 --- a/GPy/kern/Matern32.py +++ b/GPy/kern/Matern32.py @@ -14,7 +14,7 @@ class Matern32(kernpart): .. math:: - k(r) = \\sigma^2 (1 + \\sqrt{3} r) \exp(- \sqrt{3} r) \\ \\ \\ \\ \\ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \\sigma^2 (1 + \\sqrt{3} r) \exp(- \sqrt{3} r) \\ aa \\ \\ \\ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int diff --git a/doc/conf.py b/doc/conf.py index 2a02e18d..693a3197 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -59,7 +59,7 @@ import sys, os #return 1 #__len__ = __int__ = __long__ = __index__ = __hash__ - + #def __oct__(self): #return '01' @@ -116,9 +116,10 @@ print "Adding path" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" - + extensions = [#'ipython_directive', - 'sphinx.ext.autodoc', 'sphinx.ext.viewcode' + 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', + 'sphinx.ext.pngmath' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', @@ -127,7 +128,7 @@ extensions = [#'ipython_directive', #'sphinx.ext.doctest', #'ipython_console_highlighting', #'inheritance_diagram', - #'numpydoc'] + #'numpydoc'] print "finished importing" ############################################################################## @@ -443,5 +444,3 @@ epub_copyright = u'2013, Author' #def setup(app): #app.connect("autodoc-skip-member", skip) - - From 6656393441daab1ee285b763d7cbc51b4f476e0b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 31 Jan 2013 18:01:32 +0000 Subject: [PATCH 096/197] latex in doc is now beautiful --- GPy/kern/Matern32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/kern/Matern32.py b/GPy/kern/Matern32.py index a5d70a62..9831ae40 100644 --- a/GPy/kern/Matern32.py +++ b/GPy/kern/Matern32.py @@ -14,7 +14,7 @@ class Matern32(kernpart): .. math:: - k(r) = \\sigma^2 (1 + \\sqrt{3} r) \exp(- \sqrt{3} r) \\ aa \\ \\ \\ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } + k(r) = \\sigma^2 (1 + \\sqrt{3} r) \exp(- \sqrt{3} r) \\ \\ \\ \\ \\text{ where } r = \sqrt{\sum_{i=1}^D \\frac{(x_i-y_i)^2}{\ell_i^2} } :param D: the number of input dimensions :type D: int From d8ca31121ba9a6527eeacda5467a6329ff933416 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 1 Feb 2013 09:44:43 +0000 Subject: [PATCH 097/197] Fixed small bug in m.plot() when samples are shown --- GPy/models/GP_regression.py | 1 + 1 file changed, 1 insertion(+) diff --git a/GPy/models/GP_regression.py b/GPy/models/GP_regression.py index 72a24307..ed139ab3 100644 --- a/GPy/models/GP_regression.py +++ b/GPy/models/GP_regression.py @@ -207,6 +207,7 @@ class GP_regression(model): m,v = self.predict(Xnew,slices=which_functions) gpplot(Xnew,m,v) if samples: + m,v = self.predict(Xnew,slices=which_functions,full_cov=True) s = np.random.multivariate_normal(m.flatten(),v,samples) pb.plot(Xnew.flatten(),s.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) pb.plot(Xorig,Yorig,'kx',mew=1.5) From 7dfbcebb87edc6d752cd39b675d98deb5f005f54 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Fri, 1 Feb 2013 09:47:30 +0000 Subject: [PATCH 098/197] some tidying in the likelihood classes --- GPy/likelihoods/EP.py | 15 +- GPy/likelihoods/Gaussian.py | 5 +- GPy/likelihoods/likelihood_functions.py | 14 +- GPy/models/sparse_GP.py | 236 ++++++++-------------- GPy/models/sparse_GP_old.py | 258 ++++++++++++++++++++++++ GPy/models/sparse_GP_regression.py | 205 ------------------- 6 files changed, 364 insertions(+), 369 deletions(-) create mode 100644 GPy/models/sparse_GP_old.py delete mode 100644 GPy/models/sparse_GP_regression.py diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index 3e975436..c52fd8bf 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -18,12 +18,10 @@ class EP: self.likelihood_function = likelihood_function self.epsilon = epsilon self.eta, self.delta = power_ep - self.jitter = 1e-12 # TODO: is this needed? + self.is_heteroscedastic = True - """ - Initial values - Likelihood approximation parameters: - p(y|f) = t(f|tau_tilde,v_tilde) - """ + #Initial values - Likelihood approximation parameters: + #p(y|f) = t(f|tau_tilde,v_tilde) self.tau_tilde = np.zeros(self.N) self.v_tilde = np.zeros(self.N) @@ -32,8 +30,11 @@ class EP: mu_tilde = self.v_tilde/self.tau_tilde #When calling EP, this variable is used instead of Y in the GP model sigma_sum = 1./self.tau_ + 1./self.tau_tilde mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 - Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant - self.Y, self.beta, self.Z = self.tau_tilde[:,None], mu_tilde[:,None], Z_ep + self.Z = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant, aka Z_ep + + self.Y = mu_tilde[:,None] + self.precsion = self.tau_tilde + self.covariance_matrix = np.diag(1./self.precision) def fit_full(self,K): """ diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index fe954b78..5b461537 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -2,6 +2,7 @@ import numpy as np class Gaussian: def __init__(self,data,variance=1.,normalize=False): + self.is_heteroscedastic = False self.data = data self.N,D = data.shape self.Z = 0. # a correction factor which accounts for the approximation made @@ -19,6 +20,7 @@ class Gaussian: self.YYT = np.dot(self.Y,self.Y.T) self._set_params(np.asarray(variance)) + def _get_params(self): return np.asarray(self._variance) @@ -27,7 +29,8 @@ class Gaussian: def _set_params(self,x): self._variance = x - self.variance = np.eye(self.N)*self._variance + self.covariance_matrix = np.eye(self.N)*self._variance + self.precision = 1./self._variance def fit(self): """ diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index b94929d3..756d9eb0 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -8,18 +8,18 @@ import scipy as sp import pylab as pb from ..util.plot import gpplot -class likelihood: +class likelihood_function: """ Likelihood class for doing Expectation propagation :param Y: observed output (Nx1 numpy.darray) - ..Note:: Y values allowed depend on the likelihood used + ..Note:: Y values allowed depend on the likelihood_function used """ def __init__(self,location=0,scale=1): self.location = location self.scale = scale -class probit(likelihood): +class probit(likelihood_function): """ Probit likelihood Y is expected to take values in {-1,1} @@ -29,7 +29,7 @@ class probit(likelihood): $$ """ def __init__(self,location=0,scale=1): - likelihood.__init__(self,Y,location,scale) + likelihood_function.__init__(self,Y,location,scale) def moments_match(self,data_i,tau_i,v_i): """ @@ -64,7 +64,7 @@ class probit(likelihood): def _log_likelihood_gradients(): return np.zeros(0) # there are no parameters of whcih to compute the gradients -class poisson(likelihood): +class poisson(likelihood_function): """ Poisson likelihood Y is expected to take values in {0,1,2,...} @@ -75,7 +75,7 @@ class poisson(likelihood): """ def __init__(self,Y,location=0,scale=1): assert len(Y[Y<0]) == 0, "Output cannot have negative values" - likelihood.__init__(self,Y,location,scale) + likelihood_function.__init__(self,Y,location,scale) def moments_match(self,i,tau_i,v_i): """ @@ -160,7 +160,7 @@ class poisson(likelihood): if Z is not None: pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) -class gaussian(likelihood): +class gaussian(likelihood_function): """ Gaussian likelihood Y is expected to take values in (-inf,inf) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 7b043209..fe7bcc3b 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -6,37 +6,36 @@ import pylab as pb from ..util.linalg import mdot, jitchol, chol_inv, pdinv from ..util.plot import gpplot from .. import kern +from ..inference.likelihoods import likelihood from GP import GP - #Still TODO: # make use of slices properly (kernel can now do this) # enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) class sparse_GP(GP): """ - Variational sparse GP model (Regression) + Variational sparse GP model :param X: inputs :type X: np.ndarray (N x Q) - :param Y: observed data - :type Y: np.ndarray of observations (N x D) + :param likelihood: a likelihood instance, containing the observed data + :type likelihood: GPy.likelihood.(Gaussian | EP) :param kernel : the kernel/covariance function. See link kernels :type kernel: a GPy kernel - :param Z: inducing inputs (optional, see note) - :type Z: np.ndarray (M x Q) | None :param X_uncertainty: The uncertainty in the measurements of X (Gaussian variance) :type X_uncertainty: np.ndarray (N x Q) | None + :param Z: inducing inputs (optional, see note) + :type Z: np.ndarray (M x Q) | None :param Zslices: slices for the inducing inputs (see slicing TODO: link) :param M : Number of inducing points (optional, default 10. Ignored if Z is not None) :type M: int - :param beta: noise precision. TODO> ignore beta if doing EP - :type beta: float :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) :type normalize_(X|Y): bool """ - def __init__(self,X,Y=None,kernel=None,X_uncertainty=None,beta=100.,Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,power_ep=[1.,1.]): + def __init__(self,X,likelihood,kernel, X_uncertainty=None, Z=None,Zslices=None,M=10,normalize_X=False): + self.scale_factor = 1000.0# a scaling factor to help keep the algorithm stable if Z is None: self.Z = np.random.permutation(X.copy())[:M] @@ -52,140 +51,91 @@ class sparse_GP(GP): self.has_uncertain_inputs=True self.X_uncertainty = X_uncertainty - GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) + GP.__init__(self, X, Y, kernel=kernel, normalize_X=normalize_X, Xslices=Xslices) #normalise X uncertainty also if self.has_uncertain_inputs: self.X_uncertainty /= np.square(self._Xstd) - if not self.EP: - self.trYYT = np.sum(np.square(self.Y)) - else: - self.method_ep = method_ep - - #normalise X uncertainty also - if self.has_uncertain_inputs: - self.X_uncertainty /= np.square(self._Xstd) - - def _set_params(self, p): - self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) - if not self.EP: - self.beta = p[self.M*self.Q] - self.kern._set_params(p[self.Z.size + 1:]) - else: - self.kern._set_params(p[self.Z.size:]) - if self.Y is None: - self.Y = np.ones([self.N,1]) - self._compute_kernel_matrices() - self._computations() - - def _get_params(self): - if not self.EP: - return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) - else: - return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) - - def _get_param_names(self): - if not self.EP: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() - else: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() - - - def _compute_kernel_matrices(self): - # kernel computations, using BGPLVM notation - #TODO: slices for psi statistics (easy enough) - - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - if not self.EP: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() NOTE psi0 is now a vector - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - #self.psi2_beta_scaled = ? - else: - raise NotImplementedError, "uncertain_inputs not yet supported for EP" - else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() - self.psi1 = self.kern.K(self.Z,self.X) - self.psi2 = np.dot(self.psi1,self.psi1.T) - self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) - def _computations(self): # TODO find routine to multiply triangular matrices - self.V = self.beta*self.Y + #TODO: slices for psi statistics (easy enough) + + sf = self.scale_factor + sf2 = sf**2 + + # kernel computations, using BGPLVM notation + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) + self.psi2_beta_scaled = (self.psi2*(self.beta/sf2)).sum(0) + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() + self.psi1 = self.kern.K(self.Z,self.X) + tmp = self.psi1*(np.sqrt(self.likelihood.beta)/sf) + self.psi2_beta_scaled = np.dot(tmp,tmp.T) + + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm)#+np.eye(self.M)*1e-3) + + self.V = (self.likelihood.beta/self.scale_factor)*self.Y + self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) + self.B = np.eye(self.M)/sf2 + self.A + + self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) + self.psi1V = np.dot(self.psi1, self.V) self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) - self.B = np.eye(self.M) + self.A - self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) - self.LLambdai = np.dot(self.LBi, self.Lmi) - self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) - self.C = mdot(self.LLambdai, self.psi1V) - self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) - self.trace_K_beta_scaled = (self.psi0*self.beta).sum() - np.trace(self.A) - if not self.EP: - self.trace_K = self.psi0.sum() - np.trace(self.A)/self.beta + self.C = mdot(self.Lmi.T, self.Bi, self.Lmi) + self.E = mdot(self.C, self.psi1VVpsi1/sf2, self.C.T) - # Compute dL_dpsi - self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) - if not self.EP: - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) - if self.has_uncertain_inputs: - self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - else: - self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - else: - self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() - if not self.has_uncertain_inputs: - self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + # Compute dL_dpsi # FIXME + self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + self.dL_dpsi1 = mdot(self.V, self.psi1V.T,self.C).T + self.dL_dpsi2 = 0.5 * self.beta * self.D * self.Kmmi[None,:,:] # dB + self.dL_dpsi2 += - 0.5 * self.beta/sf2 * self.D * self.C[None,:,:] # dC + self.dL_dpsi2 += - 0.5 * self.beta * self.E[None,:,:] # dD # Compute dL_dKmm - self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB - self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*mdot(self.LBL_inv, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC - self.dL_dKmm += np.dot(np.dot(self.G,self.psi2_beta_scaled) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi)*sf2 # dB + self.dL_dKmm += -0.5 * self.D * (- self.C/sf2 - 2.*mdot(self.C, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC + self.dL_dKmm += np.dot(np.dot(self.E*sf2, self.psi2_beta_scaled) - np.dot(self.C, self.psi1VVpsi1), self.Kmmi) + 0.5*self.E # dD - def approximate_likelihood(self): - assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - if self.method_ep == 'DTC': - self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - elif self.method_ep == 'FITC': - self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.psi0,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - else: - self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() - self.trbetaYYT = np.sum(np.square(self.Y)*self.beta) + + def _set_params(self, p): + self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) + self.beta = p[self.M*self.Q] # FIXME + self.kern._set_params(p[self.Z.size + 1:]) self._computations() + def _get_params(self): + return np.hstack([self.Z.flatten(),GP._get_params(self)) + + def _get_param_names(self): + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + GP._get_param_names(self) + def log_likelihood(self): - """ - Compute the (lower bound on the) log marginal likelihood - """ - if not self.EP: - A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) - D = -0.5*self.beta*self.trYYT - else: - A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) - D = -0.5*self.trbetaYYT - B = -0.5*self.D*self.trace_K_beta_scaled - C = -0.5*self.D * self.B_logdet - E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) - return A+B+C+D+E + """ Compute the (lower bound on the) log marginal likelihood """ + sf2 = self.scale_factor**2 + A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) -0.5*self.beta*self.trYYT # FIXME + B = -0.5*self.D*(self.beta*self.psi0-np.trace(self.A)*sf2)# FIXME + C = -0.5*self.D * (self.B_logdet + self.M*np.log(sf2)) + D = +0.5*np.sum(self.psi1VVpsi1 * self.C) + return A+B+C+D + def _log_likelihood_gradients(self): + return np.hstack([self.dL_dZ().flatten(), GP._log_likelihood_gradients(self)]) + + # FIXME: move this into the lieklihood class def dL_dbeta(self): - """ - Compute the gradient of the log likelihood wrt beta. - """ - #TODO: suport heteroscedatic noise - dA_dbeta = 0.5 * self.N*self.D/self.beta - dB_dbeta = - 0.5 * self.D * self.trace_K + sf2 = self.scale_factor**2 + dA_dbeta = 0.5 * self.N*self.D/self.beta - 0.5 * self.trYYT + dB_dbeta = - 0.5 * self.D * (self.psi0 - np.trace(self.A)/self.beta*sf2) dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta - dD_dbeta = - 0.5 * self.trYYT - tmp = mdot(self.LBi.T, self.LLambdai, self.psi1V) - dE_dbeta = (np.sum(np.square(self.C)) - 0.5 * np.sum(self.A * np.dot(tmp, tmp.T)))/self.beta + dD_dbeta = np.sum((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) * self.psi1VVpsi1 )/self.beta - return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta + dE_dbeta) + return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta) def dL_dtheta(self): """ @@ -195,10 +145,10 @@ class sparse_GP(GP): if self.has_uncertain_inputs: dL_dtheta += self.kern.dpsi0_dtheta(self.dL_dpsi0, self.Z,self.X,self.X_uncertainty) dL_dtheta += self.kern.dpsi1_dtheta(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) - dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape + dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.dL_dpsi1.T, self.Z,self.X, self.X_uncertainty) else: #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1) #dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2.sum(0),self.psi1) dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) @@ -208,48 +158,36 @@ class sparse_GP(GP): """ The derivative of the bound wrt the inducing inputs Z """ - dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z,)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ + dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ if self.has_uncertain_inputs: - dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) - dL_dZ += self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) + dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1,self.Z,self.X, self.X_uncertainty) + dL_dZ += 2.*self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # 'stripes' else: #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1)#dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2.sum(0),self.psi1) dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) return dL_dZ - def _log_likelihood_gradients(self): - if not self.EP: - return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) - else: - return np.hstack([self.dL_dZ().flatten(), self.dL_dtheta()]) - def _raw_predict(self, Xnew, slices, full_cov=False): """Internal helper function for making predictions, does not account for normalisation""" + Kx = self.kern.K(self.Z, Xnew) - mu = mdot(Kx.T, self.LBL_inv, self.psi1V) - phi = None + mu = mdot(Kx.T, self.C/self.scale_factor, self.psi1V) + if full_cov: Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) - if not self.EP: - var += np.eye(Xnew.shape[0])/self.beta - else: - raise NotImplementedError, "full_cov = True not implemented for EP" + var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) else: Kxx = self.kern.Kdiag(Xnew) - var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) - if not self.EP: - var += 1./self.beta - else: - phi = self.likelihood.predictive_mean(mu,var) - return mu,var,phi + var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.C/self.scale_factor**2, Kx),0) + + return mu,var def plot(self, *args, **kwargs): """ Plot the fitted model: just call the GP_regression plot function and then add inducing inputs """ - GP.plot(self,*args,**kwargs) + GP_regression.plot(self,*args,**kwargs) if self.Q==1: pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) if self.has_uncertain_inputs: diff --git a/GPy/models/sparse_GP_old.py b/GPy/models/sparse_GP_old.py new file mode 100644 index 00000000..7b043209 --- /dev/null +++ b/GPy/models/sparse_GP_old.py @@ -0,0 +1,258 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + +import numpy as np +import pylab as pb +from ..util.linalg import mdot, jitchol, chol_inv, pdinv +from ..util.plot import gpplot +from .. import kern +from GP import GP + + +#Still TODO: +# make use of slices properly (kernel can now do this) +# enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) + +class sparse_GP(GP): + """ + Variational sparse GP model (Regression) + + :param X: inputs + :type X: np.ndarray (N x Q) + :param Y: observed data + :type Y: np.ndarray of observations (N x D) + :param kernel : the kernel/covariance function. See link kernels + :type kernel: a GPy kernel + :param Z: inducing inputs (optional, see note) + :type Z: np.ndarray (M x Q) | None + :param X_uncertainty: The uncertainty in the measurements of X (Gaussian variance) + :type X_uncertainty: np.ndarray (N x Q) | None + :param Zslices: slices for the inducing inputs (see slicing TODO: link) + :param M : Number of inducing points (optional, default 10. Ignored if Z is not None) + :type M: int + :param beta: noise precision. TODO> ignore beta if doing EP + :type beta: float + :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) + :type normalize_(X|Y): bool + """ + + def __init__(self,X,Y=None,kernel=None,X_uncertainty=None,beta=100.,Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,power_ep=[1.,1.]): + + if Z is None: + self.Z = np.random.permutation(X.copy())[:M] + self.M = M + else: + assert Z.shape[1]==X.shape[1] + self.Z = Z + self.M = Z.shape[0] + if X_uncertainty is None: + self.has_uncertain_inputs=False + else: + assert X_uncertainty.shape==X.shape + self.has_uncertain_inputs=True + self.X_uncertainty = X_uncertainty + + GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) + + #normalise X uncertainty also + if self.has_uncertain_inputs: + self.X_uncertainty /= np.square(self._Xstd) + + if not self.EP: + self.trYYT = np.sum(np.square(self.Y)) + else: + self.method_ep = method_ep + + #normalise X uncertainty also + if self.has_uncertain_inputs: + self.X_uncertainty /= np.square(self._Xstd) + + def _set_params(self, p): + self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) + if not self.EP: + self.beta = p[self.M*self.Q] + self.kern._set_params(p[self.Z.size + 1:]) + else: + self.kern._set_params(p[self.Z.size:]) + if self.Y is None: + self.Y = np.ones([self.N,1]) + self._compute_kernel_matrices() + self._computations() + + def _get_params(self): + if not self.EP: + return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) + else: + return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) + + def _get_param_names(self): + if not self.EP: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() + else: + return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() + + + def _compute_kernel_matrices(self): + # kernel computations, using BGPLVM notation + #TODO: slices for psi statistics (easy enough) + + self.Kmm = self.kern.K(self.Z) + if self.has_uncertain_inputs: + if not self.EP: + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() NOTE psi0 is now a vector + self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T + self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) + #self.psi2_beta_scaled = ? + else: + raise NotImplementedError, "uncertain_inputs not yet supported for EP" + else: + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() + self.psi1 = self.kern.K(self.Z,self.X) + self.psi2 = np.dot(self.psi1,self.psi1.T) + self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) + + def _computations(self): + # TODO find routine to multiply triangular matrices + self.V = self.beta*self.Y + self.psi1V = np.dot(self.psi1, self.V) + self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) + self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) + self.B = np.eye(self.M) + self.A + self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) + self.LLambdai = np.dot(self.LBi, self.Lmi) + self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) + self.C = mdot(self.LLambdai, self.psi1V) + self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) + self.trace_K_beta_scaled = (self.psi0*self.beta).sum() - np.trace(self.A) + if not self.EP: + self.trace_K = self.psi0.sum() - np.trace(self.A)/self.beta + + # Compute dL_dpsi + self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) + if not self.EP: + self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + if self.has_uncertain_inputs: + self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + else: + self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + else: + self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() + if not self.has_uncertain_inputs: + self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) + + # Compute dL_dKmm + self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB + self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*mdot(self.LBL_inv, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC + self.dL_dKmm += np.dot(np.dot(self.G,self.psi2_beta_scaled) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE + + def approximate_likelihood(self): + assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" + if self.method_ep == 'DTC': + self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + elif self.method_ep == 'FITC': + self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.psi0,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + else: + self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) + self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() + self.trbetaYYT = np.sum(np.square(self.Y)*self.beta) + self._computations() + + def log_likelihood(self): + """ + Compute the (lower bound on the) log marginal likelihood + """ + if not self.EP: + A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) + D = -0.5*self.beta*self.trYYT + else: + A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) + D = -0.5*self.trbetaYYT + B = -0.5*self.D*self.trace_K_beta_scaled + C = -0.5*self.D * self.B_logdet + E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) + return A+B+C+D+E + + def dL_dbeta(self): + """ + Compute the gradient of the log likelihood wrt beta. + """ + #TODO: suport heteroscedatic noise + dA_dbeta = 0.5 * self.N*self.D/self.beta + dB_dbeta = - 0.5 * self.D * self.trace_K + dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta + dD_dbeta = - 0.5 * self.trYYT + tmp = mdot(self.LBi.T, self.LLambdai, self.psi1V) + dE_dbeta = (np.sum(np.square(self.C)) - 0.5 * np.sum(self.A * np.dot(tmp, tmp.T)))/self.beta + + return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta + dE_dbeta) + + def dL_dtheta(self): + """ + Compute and return the derivative of the log marginal likelihood wrt the parameters of the kernel + """ + dL_dtheta = self.kern.dK_dtheta(self.dL_dKmm,self.Z) + if self.has_uncertain_inputs: + dL_dtheta += self.kern.dpsi0_dtheta(self.dL_dpsi0, self.Z,self.X,self.X_uncertainty) + dL_dtheta += self.kern.dpsi1_dtheta(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) + dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape + else: + #re-cast computations in psi2 back to psi1: + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1) #dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) + dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) + + return dL_dtheta + + def dL_dZ(self): + """ + The derivative of the bound wrt the inducing inputs Z + """ + dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z,)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ + if self.has_uncertain_inputs: + dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) + dL_dZ += self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) + else: + #re-cast computations in psi2 back to psi1: + dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1)#dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) + dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) + return dL_dZ + + def _log_likelihood_gradients(self): + if not self.EP: + return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) + else: + return np.hstack([self.dL_dZ().flatten(), self.dL_dtheta()]) + + def _raw_predict(self, Xnew, slices, full_cov=False): + """Internal helper function for making predictions, does not account for normalisation""" + Kx = self.kern.K(self.Z, Xnew) + mu = mdot(Kx.T, self.LBL_inv, self.psi1V) + phi = None + if full_cov: + Kxx = self.kern.K(Xnew) + var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) + if not self.EP: + var += np.eye(Xnew.shape[0])/self.beta + else: + raise NotImplementedError, "full_cov = True not implemented for EP" + else: + Kxx = self.kern.Kdiag(Xnew) + var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) + if not self.EP: + var += 1./self.beta + else: + phi = self.likelihood.predictive_mean(mu,var) + return mu,var,phi + + def plot(self, *args, **kwargs): + """ + Plot the fitted model: just call the GP_regression plot function and then add inducing inputs + """ + GP.plot(self,*args,**kwargs) + if self.Q==1: + pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) + if self.has_uncertain_inputs: + pb.errorbar(self.X[:,0], pb.ylim()[0]+np.zeros(self.N), xerr=2*np.sqrt(self.X_uncertainty.flatten())) + if self.Q==2: + pb.plot(self.Z[:,0],self.Z[:,1],'wo') diff --git a/GPy/models/sparse_GP_regression.py b/GPy/models/sparse_GP_regression.py deleted file mode 100644 index 07ce4d97..00000000 --- a/GPy/models/sparse_GP_regression.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - -import numpy as np -import pylab as pb -from ..util.linalg import mdot, jitchol, chol_inv, pdinv -from ..util.plot import gpplot -from .. import kern -from ..inference.likelihoods import likelihood -from GP_regression import GP_regression - -#Still TODO: -# make use of slices properly (kernel can now do this) -# enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) - -class sparse_GP_regression(GP_regression): - """ - Variational sparse GP model (Regression) - - :param X: inputs - :type X: np.ndarray (N x Q) - :param Y: observed data - :type Y: np.ndarray of observations (N x D) - :param kernel : the kernel/covariance function. See link kernels - :type kernel: a GPy kernel - :param Z: inducing inputs (optional, see note) - :type Z: np.ndarray (M x Q) | None - :param X_uncertainty: The uncertainty in the measurements of X (Gaussian variance) - :type X_uncertainty: np.ndarray (N x Q) | None - :param Zslices: slices for the inducing inputs (see slicing TODO: link) - :param M : Number of inducing points (optional, default 10. Ignored if Z is not None) - :type M: int - :param beta: noise precision. TODO> ignore beta if doing EP - :type beta: float - :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) - :type normalize_(X|Y): bool - """ - - def __init__(self,X,Y,kernel=None, X_uncertainty=None, beta=100., Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False): - self.scale_factor = 1000.0 - self.beta = beta - if Z is None: - self.Z = np.random.permutation(X.copy())[:M] - self.M = M - else: - assert Z.shape[1]==X.shape[1] - self.Z = Z - self.M = Z.shape[0] - if X_uncertainty is None: - self.has_uncertain_inputs=False - else: - assert X_uncertainty.shape==X.shape - self.has_uncertain_inputs=True - self.X_uncertainty = X_uncertainty - - GP_regression.__init__(self, X, Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y) - self.trYYT = np.sum(np.square(self.Y)) - - #normalise X uncertainty also - if self.has_uncertain_inputs: - self.X_uncertainty /= np.square(self._Xstd) - - def _computations(self): - # TODO find routine to multiply triangular matrices - #TODO: slices for psi statistics (easy enough) - - # kernel computations, using BGPLVM notation - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - self.psi2_beta_scaled = (self.psi2*(self.beta/self.scale_factor**2)).sum(0) - else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() - self.psi1 = self.kern.K(self.Z,self.X) - #self.psi2 = np.dot(self.psi1,self.psi1.T) - #self.psi2 = self.psi1.T[:,:,None]*self.psi1.T[:,None,:] - tmp = self.psi1/(self.scale_factor/np.sqrt(self.beta)) - self.psi2_beta_scaled = np.dot(tmp,tmp.T) - - sf = self.scale_factor - sf2 = sf**2 - - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm)#+np.eye(self.M)*1e-3) - - self.V = (self.beta/self.scale_factor)*self.Y - self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) - self.B = np.eye(self.M)/sf2 + self.A - - self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) - - self.psi1V = np.dot(self.psi1, self.V) - self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) - self.C = mdot(self.Lmi.T, self.Bi, self.Lmi) - self.E = mdot(self.C, self.psi1VVpsi1/sf2, self.C.T) - - # Compute dL_dpsi - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) - self.dL_dpsi1 = mdot(self.V, self.psi1V.T,self.C).T - self.dL_dpsi2 = 0.5 * self.beta * self.D * self.Kmmi[None,:,:] # dB - self.dL_dpsi2 += - 0.5 * self.beta/sf2 * self.D * self.C[None,:,:] # dC - self.dL_dpsi2 += - 0.5 * self.beta * self.E[None,:,:] # dD - - # Compute dL_dKmm - self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi)*sf2 # dB - self.dL_dKmm += -0.5 * self.D * (- self.C/sf2 - 2.*mdot(self.C, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC - self.dL_dKmm += np.dot(np.dot(self.E*sf2, self.psi2_beta_scaled) - np.dot(self.C, self.psi1VVpsi1), self.Kmmi) + 0.5*self.E # dD - - - def _set_params(self, p): - self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) - self.beta = p[self.M*self.Q] - self.kern._set_params(p[self.Z.size + 1:]) - self._computations() - - def _get_params(self): - return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) - - def _get_param_names(self): - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() - - - def log_likelihood(self): - """ Compute the (lower bound on the) log marginal likelihood """ - sf2 = self.scale_factor**2 - A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) -0.5*self.beta*self.trYYT - B = -0.5*self.D*(self.beta*self.psi0-np.trace(self.A)*sf2) - C = -0.5*self.D * (self.B_logdet + self.M*np.log(sf2)) - D = +0.5*np.sum(self.psi1VVpsi1 * self.C) - return A+B+C+D - - def _log_likelihood_gradients(self): - return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) - - def dL_dbeta(self): - """ - Compute the gradient of the log likelihood wrt beta. - """ - #TODO: suport heteroscedatic noise - sf2 = self.scale_factor**2 - dA_dbeta = 0.5 * self.N*self.D/self.beta - 0.5 * self.trYYT - dB_dbeta = - 0.5 * self.D * (self.psi0 - np.trace(self.A)/self.beta*sf2) - dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta - dD_dbeta = np.sum((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) * self.psi1VVpsi1 )/self.beta - - return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta) - - def dL_dtheta(self): - """ - Compute and return the derivative of the log marginal likelihood wrt the parameters of the kernel - """ - dL_dtheta = self.kern.dK_dtheta(self.dL_dKmm,self.Z) - if self.has_uncertain_inputs: - dL_dtheta += self.kern.dpsi0_dtheta(self.dL_dpsi0, self.Z,self.X,self.X_uncertainty) - dL_dtheta += self.kern.dpsi1_dtheta(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) - dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.dL_dpsi1.T, self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape - else: - #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2.sum(0),self.psi1) - dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) - dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) - - return dL_dtheta - - def dL_dZ(self): - """ - The derivative of the bound wrt the inducing inputs Z - """ - dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ - if self.has_uncertain_inputs: - dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1,self.Z,self.X, self.X_uncertainty) - dL_dZ += 2.*self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # 'stripes' - else: - #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2.sum(0),self.psi1) - dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) - return dL_dZ - - def _raw_predict(self, Xnew, slices, full_cov=False): - """Internal helper function for making predictions, does not account for normalisation""" - - Kx = self.kern.K(self.Z, Xnew) - mu = mdot(Kx.T, self.C/self.scale_factor, self.psi1V) - - if full_cov: - Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) + np.eye(Xnew.shape[0])/self.beta # TODO: This beta doesn't belong here in the EP case. - else: - Kxx = self.kern.Kdiag(Xnew) - var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.C/self.scale_factor**2, Kx),0) + 1./self.beta # TODO: This beta doesn't belong here in the EP case. - - return mu,var - - def plot(self, *args, **kwargs): - """ - Plot the fitted model: just call the GP_regression plot function and then add inducing inputs - """ - GP_regression.plot(self,*args,**kwargs) - if self.Q==1: - pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) - if self.has_uncertain_inputs: - pb.errorbar(self.X[:,0], pb.ylim()[0]+np.zeros(self.N), xerr=2*np.sqrt(self.X_uncertainty.flatten())) - if self.Q==2: - pb.plot(self.Z[:,0],self.Z[:,1],'wo') From 346f9dd8bd3207959b87ded258e55aeb094f1ea3 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Fri, 1 Feb 2013 10:05:22 +0000 Subject: [PATCH 099/197] added a likelihood atom class and also some import tidying in the EP.py file --- GPy/likelihoods/EP.py | 12 ++++-------- GPy/likelihoods/Gaussian.py | 3 ++- GPy/likelihoods/likelihood.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 GPy/likelihoods/likelihood.py diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index c52fd8bf..ff612a6d 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -1,11 +1,9 @@ import numpy as np -import random from scipy import stats, linalg -from ..core import model from ..util.linalg import pdinv,mdot,jitchol -from ..util.plot import gpplot +from likelihood import likelihood -class EP: +class EP(likelihood): def __init__(self,data,likelihood_function,epsilon=1e-3,power_ep=[1.,1.]): """ Expectation Propagation @@ -70,8 +68,7 @@ class EP: self.np1 = [self.tau_tilde.copy()] self.np2 = [self.v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) + update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] @@ -243,8 +240,7 @@ class EP: self.np1 = [self.tau_tilde.copy()] self.np2 = [self.v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) + update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 5b461537..cb040d50 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -1,6 +1,7 @@ import numpy as np +from likelihood import likelihood -class Gaussian: +class Gaussian(likelihood): def __init__(self,data,variance=1.,normalize=False): self.is_heteroscedastic = False self.data = data diff --git a/GPy/likelihoods/likelihood.py b/GPy/likelihoods/likelihood.py new file mode 100644 index 00000000..6ec57c07 --- /dev/null +++ b/GPy/likelihoods/likelihood.py @@ -0,0 +1,35 @@ +import numpy as np + +class likelihood: + """ + The atom for a likelihood class + + This object interfaces the GP and the data. The most basic likelihood + (Gaussian) inherits directly from this, as does the EP algorithm + + Some things must be defined for this to work properly: + self.Y : the effective Gaussian target of the GP + self.N, self.D : Y.shape + self.covariance_matrix : the effective (noise) covariance of the GP targets + self.Z : a factor which gets added to the likelihood (0 for a Gaussian, Z_EP for EP) + self.is_heteroscedastic : enables significant computational savings in GP + self.precision : a scalar or vector representation of the effective target precision + self.YYT : (optional) = np.dot(self.Y, self.Y.T) enables computational savings for D>N + """ + def __init__(self,data): + raise ValueError, "this class is not to be instantiated" + + def _get_params(self): + raise NotImplementedError + + def _get_param_names(self): + raise NotImplementedError + + def _set_params(self,x): + raise NotImplementedError + + def fit(self): + raise NotImplementedError + + def _gradients(self,partial): + raise NotImplementedError From d8785b534e9a0c2b5aa3b76f9b717e277e18135b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 1 Feb 2013 10:33:00 +0000 Subject: [PATCH 100/197] Fixed bug in the product of kernels --- GPy/kern/kern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/kern/kern.py b/GPy/kern/kern.py index 0433d1f4..89def0e5 100644 --- a/GPy/kern/kern.py +++ b/GPy/kern/kern.py @@ -155,7 +155,7 @@ class kern(parameterised): D = K1.D + K2.D - newkernparts = [product_orthogonal(k1,k2).parts[0] for k1, k2 in itertools.product(K1.parts,K2.parts)] + newkernparts = [product_orthogonal(k1,k2) for k1, k2 in itertools.product(K1.parts,K2.parts)] slices = [] for sl1, sl2 in itertools.product(K1.input_slices,K2.input_slices): From 182c4c7d64d9e85191f0aac45e67c77857b63cf7 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 13:17:17 +0000 Subject: [PATCH 101/197] So many changes --- GPy/core/model.py | 9 +-- GPy/examples/ep_fix.py | 50 +++++++------- GPy/likelihoods/EP.py | 31 ++++++--- GPy/likelihoods/Gaussian.py | 2 +- GPy/likelihoods/likelihood_functions.py | 74 ++++---------------- GPy/models/GP.py | 89 ++++++++++++++----------- GPy/util/plot.py | 2 + 7 files changed, 118 insertions(+), 139 deletions(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index c7b61a32..f26bf2ee 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,6 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization +from .. import likelihoods class model(parameterised): def __init__(self): @@ -401,7 +402,7 @@ class model(parameterised): :type optimzer: string TODO: valid strings? """ - assert self.EP, "EM is not available for gaussian likelihood" + assert isinstance(self.likelihood,likelihoods.EP), "EM is not available for Gaussian likelihoods" log_change = epsilon + 1. self.log_likelihood_record = [] self.gp_params_record = [] @@ -410,18 +411,18 @@ class model(parameterised): last_value = -np.exp(1000) while log_change > epsilon or not iteration: print 'EM iteration %s' %iteration - self.approximate_likelihood() + self.update_likelihood_approximation() self.optimize(**kwargs) new_value = self.log_likelihood() log_change = new_value - last_value if log_change > epsilon: self.log_likelihood_record.append(new_value) self.gp_params_record.append(self._get_params()) - self.ep_params_record.append((self.beta,self.Y,self.Z_ep)) + #self.ep_params_record.append((self.beta,self.Y,self.Z_ep)) last_value = new_value else: convergence = False - self.beta, self.Y, self.Z_ep = self.ep_params_record[-1] + #self.beta, self.Y, self.Z_ep = self.ep_params_record[-1] self._set_params(self.gp_params_record[-1]) print "Log-likelihood decrement: %s \nLast iteration discarded." %log_change iteration += 1 diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 8041cc91..83a58bf8 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -3,7 +3,8 @@ """ -Simple Gaussian Processes classification +Simple Gaussian Processes classification 1D +Probit likelihood """ import pylab as pb import numpy as np @@ -12,28 +13,31 @@ pb.ion() pb.close('all') -model_type='Full' -inducing=4 -"""Simple 1D classification example. -:param model_type: type of model to fit ['Full', 'FITC', 'DTC']. -:param seed : seed value for data generation (default is 4). -:type seed: int -:param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). -:type inducing: int -""" -data = GPy.util.datasets.toy_linear_1d_classification(seed=0) -likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) +# Inputs +N = 30 +X1 = np.random.normal(5,2,N/2) +X2 = np.random.normal(10,2,N/2) +X = np.hstack([X1,X2])[:,None] -m = GPy.models.GP(data['X'],likelihood=likelihood) -#m = GPy.models.GP(data['X'],likelihood.Y) +# Outputs +Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] + +# Kernel object +kernel = GPy.kern.rbf(1) + +# Define likelihood +distribution = GPy.likelihoods.likelihood_functions.Probit() +likelihood_object = GPy.likelihoods.EP(Y,distribution) + +# Model definition +m = GPy.models.GP(X,kernel,likelihood=likelihood_object) m.ensure_default_constraints() +m.update_likelihood_approximation() +#m.checkgrad(verbose=1) +m.optimize() +print "Round 2" +m.update_likelihood_approximation() -# Optimize and plot -#if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): -# m.approximate_likelihood() -#m.optimize() -m.EM() - -print m.log_likelihood() -m.plot(samples=3) -print(m) +#m.EPEM() +#m.plot() +#print(m) diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index b557a62f..bec81436 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -1,7 +1,7 @@ import numpy as np import random from scipy import stats, linalg -from ..core import model +#from ..core import model from ..util.linalg import pdinv,mdot,jitchol from ..util.plot import gpplot @@ -18,6 +18,8 @@ class EP: self.likelihood_function = likelihood_function self.epsilon = epsilon self.eta, self.delta = power_ep + self.data = data + self.N = self.data.size """ Initial values - Likelihood approximation parameters: @@ -26,6 +28,12 @@ class EP: self.tau_tilde = np.zeros(self.N) self.v_tilde = np.zeros(self.N) + #initial values for the GP variables + self.Y = np.zeros((self.N,1)) + self.variance = np.zeros((self.N,self.N))#np.eye(self.N) + self.Z = 0 + self.YYT = None + def predictive_values(self,mu,var): return self.likelihood_function.predictive_values(mu,var) @@ -35,6 +43,8 @@ class EP: return [] def _set_params(self,p): pass # TODO: the EP likelihood might want to take some parameters... + def _gradients(self,partial): + return np.zeros(0) # TODO: the EP likelihood might want to take some parameters... def _compute_GP_variables(self): #Variables to be called from GP @@ -42,7 +52,8 @@ class EP: sigma_sum = 1./self.tau_ + 1./self.tau_tilde mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 Z_ep = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant - self.Y, self.beta, self.Z = self.tau_tilde[:,None], mu_tilde[:,None], Z_ep + self.Y, self.beta, self.Z = mu_tilde[:,None],self.tau_tilde[:,None], Z_ep + self.variance = np.diag(1./self.beta.flatten()) def fit_full(self,K): """ @@ -53,7 +64,7 @@ class EP: #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) self.mu = np.zeros(self.N) - self.Sigma = K.copy() + self.Sigma = K.copy() - self.variance.copy() """ Initial values - Cavity distribution parameters: @@ -78,14 +89,14 @@ class EP: self.np1 = [self.tau_tilde.copy()] self.np2 = [self.v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) + update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] self.v_[i] = self.mu[i]/self.Sigma[i,i] - self.eta*self.v_tilde[i] + print 1./self.Sigma[i,i],self.tau_tilde[i] #Marginal moments - self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood_function.moments_match(self.data[i],self.tau_[i],self.v_[i]) #Site parameters update Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma[i,i]) Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma[i,i]) @@ -96,6 +107,7 @@ class EP: self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) self.mu = np.dot(self.Sigma,self.v_tilde) self.iterations += 1 + print self.tau_tilde[i] #Sigma recomptutation with Cholesky decompositon Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*K B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K @@ -116,7 +128,7 @@ class EP: For nomenclature see ... 2013. """ - #TODO: this doesn;t work with uncertain inputs! + #TODO: this doesn;t work with uncertain inputs! """ Prior approximation parameters: @@ -251,14 +263,13 @@ class EP: self.np1 = [self.tau_tilde.copy()] self.np2 = [self.v_tilde.copy()] while epsilon_np1 > self.epsilon or epsilon_np2 > self.epsilon: - update_order = np.arange(self.N) - random.shuffle(update_order) + update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] #Marginal moments - self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood.moments_match(i,self.tau_[i],self.v_[i]) + self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood_function.moments_match(data[i],self.tau_[i],self.v_[i]) #Site parameters update Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 37132cf0..eec833b8 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -39,7 +39,7 @@ class Gaussian: _95pc = mean + 2.*np.sqrt(var) return mean, _5pc, _95pc - def fit(self): + def fit_full(self): """ No approximations needed """ diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 68fd276a..e153ce15 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -7,6 +7,7 @@ from scipy import stats import scipy as sp import pylab as pb from ..util.plot import gpplot +#from . import EP class likelihood: """ @@ -19,7 +20,7 @@ class likelihood: self.location = location self.scale = scale -class probit(likelihood): +class Probit(likelihood): """ Probit likelihood Y is expected to take values in {-1,1} @@ -28,8 +29,6 @@ class probit(likelihood): L(x) = \\Phi (Y_i*f_i) $$ """ - def __init__(self,location=0,scale=1): - likelihood.__init__(self,Y,location,scale) def moments_match(self,data_i,tau_i,v_i): """ @@ -47,24 +46,18 @@ class probit(likelihood): sigma2_hat = 1./tau_i - (phi/((tau_i**2+tau_i)*Z_hat))*(z+phi/Z_hat) return Z_hat, mu_hat, sigma2_hat - def predictive_values(self,mu,var,all=False): + def predictive_values(self,mu,var): """ Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mu = mu.flatten() var = var.flatten() mean = stats.norm.cdf(mu/np.sqrt(1+var)) - if all: - p_05 = np.zeros([mu.size]) - p_95 = np.ones([mu.size]) - return mean, p_05, p_95 - else: - return mean + p_05 = np.zeros([mu.size]) + p_95 = np.ones([mu.size]) + return mean, p_05, p_95 - def _log_likelihood_gradients(): - return np.zeros(0) # there are no parameters of whcih to compute the gradients - -class poisson(likelihood): +class Poisson(likelihood): """ Poisson likelihood Y is expected to take values in {0,1,2,...} @@ -73,9 +66,6 @@ class poisson(likelihood): L(x) = \exp(\lambda) * \lambda**Y_i / Y_i! $$ """ - def __init__(self,Y,location=0,scale=1): - assert len(Y[Y<0]) == 0, "Output cannot have negative values" - likelihood.__init__(self,Y,location,scale) def moments_match(self,i,tau_i,v_i): """ @@ -134,52 +124,12 @@ class poisson(likelihood): sigma2_hat = m2 - mu_hat**2 # Second central moment return float(Z_hat), float(mu_hat), float(sigma2_hat) - def predictive_values(self,mu,var,all=False): + def predictive_values(self,mu,var): """ Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mean = np.exp(mu*self.scale + self.location) - if all: - tmp = stats.poisson.ppf(np.array([.05,.95]),mu) - p_05 = tmp[:,0] - p_95 = tmp[:,1] - return mean,p_05,p_95 - else: - return mean - - def _log_likelihood_gradients(): - raise NotImplementedError - - def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): - assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,phi,phi.flatten()) - pb.plot(X_obs,self.Y,'kx',mew=1.5) - if samples: - phi_samples = np.vstack([np.random.poisson(phi.flatten(),phi.size) for s in range(samples)]) - pb.plot(X,phi_samples.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - if Z is not None: - pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) - -class gaussian(likelihood): - """ - Gaussian likelihood - Y is expected to take values in (-inf,inf) - """ - def moments_match(self,i,tau_i,v_i): - """ - Moments match of the marginal approximation in EP algorithm - - :param i: number of observation (int) - :param tau_i: precision of the cavity distribution (float) - :param v_i: mean/variance of the cavity distribution (float) - """ - mu = v_i/tau_i - sigma = np.sqrt(1./tau_i) - s = 1. if self.Y[i] == 0 else 1./self.Y[i] - sigma2_hat = 1./(1./sigma**2 + 1./s**2) - mu_hat = sigma2_hat*(mu/sigma**2 + self.Y[i]/s**2) - Z_hat = 1./np.sqrt(2*np.pi) * 1./np.sqrt(sigma**2+s**2) * np.exp(-.5*(mu-self.Y[i])**2/(sigma**2 + s**2)) - return Z_hat, mu_hat, sigma2_hat - - def _log_likelihood_gradients(): - raise NotImplementedError + tmp = stats.poisson.ppf(np.array([.05,.95]),mu) + p_05 = tmp[:,0] + p_95 = tmp[:,1] + return mean,p_05,p_95 diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 793e2585..49c22364 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -8,6 +8,7 @@ from .. import kern from ..core import model from ..util.linalg import pdinv,mdot from ..util.plot import gpplot, Tango +from ..likelihoods import EP class GP(model): """ @@ -52,8 +53,10 @@ class GP(model): self._Xstd = np.ones((1,self.X.shape[1])) self.likelihood = likelihood - assert self.X.shape[0] == self.likelihood.Y.shape[0] - self.N, self.D = self.likelihood.Y.shape + #assert self.X.shape[0] == self.likelihood.Y.shape[0] + #self.N, self.D = self.likelihood.Y.shape + assert self.X.shape[0] == self.likelihood.data.shape[0] + self.N, self.D = self.likelihood.data.shape model.__init__(self) @@ -87,7 +90,11 @@ class GP(model): For a Gaussian (or direct: TODO) likelihood, no iteration is required: this function does nothing """ - self.likelihood.fit(self.K) + self.likelihood.fit_full(self.K) + # Recompute K + noise_term + self.K = self.kern.K(self.X,slices1=self.Xslices) + self.K += self.likelihood.variance + self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) def _model_fit_term(self): """ @@ -119,7 +126,7 @@ class GP(model): """ return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK,X=self.X), self.likelihood._gradients(partial=self.dL_dK))) - def _raw_predict(self,_Xnew,slices, full_cov=False): + def _raw_predict(self,_Xnew,slices=None, full_cov=False): """ Internal helper function for making predictions, does not account for normalisation or likelihood @@ -129,11 +136,11 @@ class GP(model): KiKx = np.dot(self.Ki,Kx) if full_cov: Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) - var = Kxx - np.dot(KiKx.T,Kx) + var = Kxx - np.dot(KiKx.T,Kx) #NOTE is the shape of v right? else: Kxx = self.kern.Kdiag(_Xnew, slices=slices) var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - return mu, var + return mu, var[:,None] def predict(self,Xnew, slices=None, full_cov=False): @@ -170,26 +177,11 @@ class GP(model): return mean, _5pc, _95pc - def raw_plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None): + + def _x_frame(self,plot_limits=None,which_data='all',which_functions='all',resolution=None): """ - Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian - - :param samples: the number of a posteriori samples to plot - :param which_data: which if the training data to plot (default all) - :type which_data: 'all' or a slice object to slice self.X, self.Y - :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits - :param which_functions: which of the kernel functions to plot (additively) - :type which_functions: list of bools - :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D - - Plot the posterior of the GP. - - In one dimension, the function is plotted with a shaded region identifying two standard deviations. - - In two dimsensions, a contour-plot shows the mean predicted function - - In higher dimensions, we've no implemented this yet !TODO! - - Can plot only part of the data and part of the posterior functions using which_data and which_functions + Internal helper function for making plots, return a set of new input values to plot as well as lower and upper limits """ - if which_functions=='all': which_functions = [True]*self.kern.Nparts if which_data=='all': @@ -208,28 +200,47 @@ class GP(model): if self.X.shape[1]==1: Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - m,v = self._raw_predict(Xnew,slices=which_functions,full_cov=False) - lower, upper = m.flatten() - 2.*np.sqrt(v) , m.flatten()+ 2.*np.sqrt(v) - gpplot(Xnew,m,lower,upper) - pb.plot(X,Y,'kx',mew=1.5) - pb.xlim(xmin,xmax) elif self.X.shape[1]==2: resolution = resolution or 50 xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] - Xtest = np.vstack((xx.flatten(),yy.flatten())).T - zz,vv = self._raw_predict(Xtest,slices=which_functions,full_cov=False) - zz = zz.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) - pb.xlim(xmin[0],xmax[0]) - pb.ylim(xmin[1],xmax[1]) - + Xnew = np.vstack((xx.flatten(),yy.flatten())).T else: raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" + return Xnew, xmin, xmax - def plot(self): + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + """ + Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian + + :param samples: the number of a posteriori samples to plot + :param which_data: which if the training data to plot (default all) + :type which_data: 'all' or a slice object to slice self.X, self.Y + :param plot_limits: The limits of the plot. If 1D [xmin,xmax], if 2D [[xmin,ymin],[xmax,ymax]]. Defaluts to data limits + :param which_functions: which of the kernel functions to plot (additively) + :type which_functions: list of bools + :param resolution: the number of intervals to sample the GP on. Defaults to 200 in 1D and 50 (a 50x50 grid) in 2D + + Plot the posterior of the GP. + - In one dimension, the function is plotted with a shaded region identifying two standard deviations. + - In two dimsensions, a contour-plot shows the mean predicted function + - In higher dimensions, we've no implemented this yet !TODO! + + Can plot only part of the data and part of the posterior functions using which_data and which_functions + """ """ Plot the data's view of the world, with non-normalised values and GP predictions passed through the likelihood """ - pass# TODO!!!!! + Xnew, xmin, xmax = self._x_frame() + m,v = self._raw_predict(Xnew) + if isinstance(self.likelihood,EP): + pb.subplot(211) + gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) + pb.plot(self.X,self.likelihood.Y,'kx',mew=1.5) + pb.xlim(xmin,xmax) + if isinstance(self.likelihood,EP): + pb.subplot(212) + phi_m,phi_l,phi_u = self.likelihood.predictive_values(m,v) + gpplot(Xnew,phi_m,phi_l,phi_u) + pb.plot(self.X,self.likelihood.data,'kx',mew=1.5) + pb.xlim(xmin,xmax) diff --git a/GPy/util/plot.py b/GPy/util/plot.py index 3b4682e4..bf372869 100644 --- a/GPy/util/plot.py +++ b/GPy/util/plot.py @@ -11,6 +11,8 @@ def gpplot(x,mu,lower,upper,edgecol=Tango.coloursHex['darkBlue'],fillcol=Tango.c axes = pb.gca() mu = mu.flatten() x = x.flatten() + lower = lower.flatten() + upper = upper.flatten() #here's the mean axes.plot(x,mu,color=edgecol,linewidth=2) From eb04cbed634712aeef55b40fb29ae283bfa4e480 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 13:32:13 +0000 Subject: [PATCH 102/197] merged changes in likelihood_functions (James) --- GPy/examples/ep_fix.py | 4 +- GPy/likelihoods/likelihood_functions.py | 69 +------------------------ GPy/models/GP.py | 7 +-- 3 files changed, 5 insertions(+), 75 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 83a58bf8..3cb35663 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -36,8 +36,8 @@ m.update_likelihood_approximation() #m.checkgrad(verbose=1) m.optimize() print "Round 2" -m.update_likelihood_approximation() +#rm.update_likelihood_approximation() #m.EPEM() -#m.plot() +m.plot() #print(m) diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 5e2f0b85..39428c70 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -20,11 +20,7 @@ class likelihood_function: self.location = location self.scale = scale -<<<<<<< HEAD -class Probit(likelihood): -======= class probit(likelihood_function): ->>>>>>> 346f9dd8bd3207959b87ded258e55aeb094f1ea3 """ Probit likelihood Y is expected to take values in {-1,1} @@ -33,11 +29,6 @@ class probit(likelihood_function): L(x) = \\Phi (Y_i*f_i) $$ """ -<<<<<<< HEAD -======= - def __init__(self,location=0,scale=1): - likelihood_function.__init__(self,Y,location,scale) ->>>>>>> 346f9dd8bd3207959b87ded258e55aeb094f1ea3 def moments_match(self,data_i,tau_i,v_i): """ @@ -66,11 +57,7 @@ class probit(likelihood_function): p_95 = np.ones([mu.size]) return mean, p_05, p_95 -<<<<<<< HEAD -class Poisson(likelihood): -======= -class poisson(likelihood_function): ->>>>>>> 346f9dd8bd3207959b87ded258e55aeb094f1ea3 +class Poisson(likelihood_function): """ Poisson likelihood Y is expected to take values in {0,1,2,...} @@ -79,13 +66,6 @@ class poisson(likelihood_function): L(x) = \exp(\lambda) * \lambda**Y_i / Y_i! $$ """ -<<<<<<< HEAD -======= - def __init__(self,Y,location=0,scale=1): - assert len(Y[Y<0]) == 0, "Output cannot have negative values" - likelihood_function.__init__(self,Y,location,scale) ->>>>>>> 346f9dd8bd3207959b87ded258e55aeb094f1ea3 - def moments_match(self,i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -148,54 +128,7 @@ class poisson(likelihood_function): Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mean = np.exp(mu*self.scale + self.location) -<<<<<<< HEAD tmp = stats.poisson.ppf(np.array([.05,.95]),mu) p_05 = tmp[:,0] p_95 = tmp[:,1] return mean,p_05,p_95 -======= - if all: - tmp = stats.poisson.ppf(np.array([.05,.95]),mu) - p_05 = tmp[:,0] - p_95 = tmp[:,1] - return mean,mean,p_05,p_95 - else: - return mean - - def _log_likelihood_gradients(): - raise NotImplementedError - - def plot(self,X,mu,var,phi,X_obs,Z=None,samples=0): - assert X_obs.shape[1] == 1, 'Number of dimensions must be 1' - gpplot(X,phi,phi.flatten()) - pb.plot(X_obs,self.Y,'kx',mew=1.5) - if samples: - phi_samples = np.vstack([np.random.poisson(phi.flatten(),phi.size) for s in range(samples)]) - pb.plot(X,phi_samples.T, alpha = 0.4, c='#3465a4', linewidth = 0.8) - if Z is not None: - pb.plot(Z,Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) - -class gaussian(likelihood_function): - """ - Gaussian likelihood - Y is expected to take values in (-inf,inf) - """ - def moments_match(self,i,tau_i,v_i): - """ - Moments match of the marginal approximation in EP algorithm - - :param i: number of observation (int) - :param tau_i: precision of the cavity distribution (float) - :param v_i: mean/variance of the cavity distribution (float) - """ - mu = v_i/tau_i - sigma = np.sqrt(1./tau_i) - s = 1. if self.Y[i] == 0 else 1./self.Y[i] - sigma2_hat = 1./(1./sigma**2 + 1./s**2) - mu_hat = sigma2_hat*(mu/sigma**2 + self.Y[i]/s**2) - Z_hat = 1./np.sqrt(2*np.pi) * 1./np.sqrt(sigma**2+s**2) * np.exp(-.5*(mu-self.Y[i])**2/(sigma**2 + s**2)) - return Z_hat, mu_hat, sigma2_hat - - def _log_likelihood_gradients(): - raise NotImplementedError ->>>>>>> 346f9dd8bd3207959b87ded258e55aeb094f1ea3 diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 49c22364..ae192618 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -90,11 +90,8 @@ class GP(model): For a Gaussian (or direct: TODO) likelihood, no iteration is required: this function does nothing """ - self.likelihood.fit_full(self.K) - # Recompute K + noise_term - self.K = self.kern.K(self.X,slices1=self.Xslices) - self.K += self.likelihood.variance - self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) + self.likelihood.fit_full(self.kern.compute(self.X)) + self._set_params(self._get_params()) # update the GP def _model_fit_term(self): """ From f941d629e68ac1611619d3455cc8c628ce59035e Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 13:45:55 +0000 Subject: [PATCH 103/197] James' debugging of the EP/GP interface It seems that the GP-EP algorithm works now. --- GPy/examples/ep_fix.py | 4 ++-- GPy/likelihoods/EP.py | 11 +++++++---- GPy/models/GP.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index 3cb35663..d1747025 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -4,7 +4,7 @@ """ Simple Gaussian Processes classification 1D -Probit likelihood +probit likelihood """ import pylab as pb import numpy as np @@ -26,7 +26,7 @@ Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] kernel = GPy.kern.rbf(1) # Define likelihood -distribution = GPy.likelihoods.likelihood_functions.Probit() +distribution = GPy.likelihoods.likelihood_functions.probit() likelihood_object = GPy.likelihoods.EP(Y,distribution) # Model definition diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index 420b138a..a88059b1 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -27,7 +27,7 @@ class EP(likelihood): #initial values for the GP variables self.Y = np.zeros((self.N,1)) - self.variance = np.zeros((self.N,self.N))#np.eye(self.N) + self.covariance_matrix = np.eye(self.N) self.Z = 0 self.YYT = None @@ -50,8 +50,9 @@ class EP(likelihood): mu_diff_2 = (self.v_/self.tau_ - mu_tilde)**2 self.Z = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant, aka Z_ep - self.Y = mu_tilde[:,None] - self.precsion = self.tau_tilde[:,None] + self.Y = mu_tilde[:,None] + self.YYT = np.dot(self.Y,self.Y.T) + self.precision = self.tau_tilde self.covariance_matrix = np.diag(1./self.precision) def fit_full(self,K): @@ -61,9 +62,11 @@ class EP(likelihood): """ #Prior distribution parameters: p(f|X) = N(f|0,K) + self.tau_tilde = np.zeros(self.N) + self.v_tilde = np.zeros(self.N) #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) self.mu = np.zeros(self.N) - self.Sigma = K.copy() - self.variance.copy() + self.Sigma = K.copy() """ Initial values - Cavity distribution parameters: diff --git a/GPy/models/GP.py b/GPy/models/GP.py index ae192618..e64da2c9 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -65,7 +65,7 @@ class GP(model): self.likelihood._set_params(p[self.kern.Nparam:]) self.K = self.kern.K(self.X,slices1=self.Xslices) - self.K += self.likelihood.variance + self.K += self.likelihood.covariance_matrix self.Ki, self.L, self.Li, self.K_logdet = pdinv(self.K) @@ -90,7 +90,7 @@ class GP(model): For a Gaussian (or direct: TODO) likelihood, no iteration is required: this function does nothing """ - self.likelihood.fit_full(self.kern.compute(self.X)) + self.likelihood.fit_full(self.kern.K(self.X)) self._set_params(self._get_params()) # update the GP def _model_fit_term(self): From c025e8b68b482048e29166922043b452d41328bb Mon Sep 17 00:00:00 2001 From: James Hensman Date: Fri, 1 Feb 2013 13:55:19 +0000 Subject: [PATCH 104/197] beginning of work to make sparse GP ork with RA's EP methods --- GPy/likelihoods/EP.py | 2 +- GPy/models/sparse_GP.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index ff612a6d..10b8828c 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -31,7 +31,7 @@ class EP(likelihood): self.Z = np.sum(np.log(self.Z_hat)) + 0.5*np.sum(np.log(sigma_sum)) + 0.5*np.sum(mu_diff_2/sigma_sum) #Normalization constant, aka Z_ep self.Y = mu_tilde[:,None] - self.precsion = self.tau_tilde + self.precision = self.tau_tilde[:,None] self.covariance_matrix = np.diag(1./self.precision) def fit_full(self,K): diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index fe7bcc3b..5048a174 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -70,16 +70,23 @@ class sparse_GP(GP): self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - self.psi2_beta_scaled = (self.psi2*(self.beta/sf2)).sum(0) + if self.likelihood.is_heteroscedastic: + self.psi2_beta_scaled = (self.psi2*(self.likelihood.precision.reshape(self.N,1,1)/sf2)).sum(0) + #TODO: what is the likelihood is heterscedatic and there are multiple independent outputs? + else: + self.psi2_beta_scaled = (self.psi2*(self.likelihood.precision/sf2)).sum(0) else: self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() self.psi1 = self.kern.K(self.Z,self.X) - tmp = self.psi1*(np.sqrt(self.likelihood.beta)/sf) + if self.likelihood.is_heteroscedastic: + tmp = self.psi1*(np.sqrt(self.likelihood.precision.reshape(self.N,1))/sf) + else: + tmp = self.psi1*(np.sqrt(self.likelihood.precision)/sf) self.psi2_beta_scaled = np.dot(tmp,tmp.T) - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm)#+np.eye(self.M)*1e-3) + self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.V = (self.likelihood.beta/self.scale_factor)*self.Y + self.V = (self.likelihood.precision/self.scale_factor)*self.Y self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) self.B = np.eye(self.M)/sf2 + self.A From 0a8686d7c0a96928f9eb5b3b773444dcbd08c859 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 15:14:11 +0000 Subject: [PATCH 105/197] EPEM is running. --- GPy/examples/ep_fix.py | 38 +++++---- GPy/likelihoods/EP.py | 102 ++++++++++++------------ GPy/likelihoods/likelihood_functions.py | 1 - GPy/models/GP.py | 68 ++++++---------- GPy/util/plot.py | 20 +++++ 5 files changed, 117 insertions(+), 112 deletions(-) diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py index d1747025..440d00aa 100644 --- a/GPy/examples/ep_fix.py +++ b/GPy/examples/ep_fix.py @@ -1,7 +1,6 @@ # Copyright (c) 2012, GPy authors (see AUTHORS.txt). # Licensed under the BSD 3-clause license (see LICENSE.txt) - """ Simple Gaussian Processes classification 1D probit likelihood @@ -19,25 +18,36 @@ X1 = np.random.normal(5,2,N/2) X2 = np.random.normal(10,2,N/2) X = np.hstack([X1,X2])[:,None] -# Outputs +# Output Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] # Kernel object kernel = GPy.kern.rbf(1) -# Define likelihood +# Likelihood object distribution = GPy.likelihoods.likelihood_functions.probit() -likelihood_object = GPy.likelihoods.EP(Y,distribution) +likelihood = GPy.likelihoods.EP(Y,distribution) # Model definition -m = GPy.models.GP(X,kernel,likelihood=likelihood_object) -m.ensure_default_constraints() -m.update_likelihood_approximation() -#m.checkgrad(verbose=1) -m.optimize() -print "Round 2" -#rm.update_likelihood_approximation() +m = GPy.models.GP(X,kernel,likelihood=likelihood) -#m.EPEM() -m.plot() -#print(m) +# Model constraints +m.ensure_default_constraints() + +# Optimize model +""" +EPEM runs a loop that consists of two steps: +1) EP likelihood approximation: + m.update_likelihood_approximation() +2) Parameters optimization: + m.optimize() +""" +m.EPEM() + +# Plot +pb.subplot(211) +m.plot_GP() +pb.subplot(212) +m.plot_output() + +print(m) diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index a88059b1..f01a5017 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -65,8 +65,8 @@ class EP(likelihood): self.tau_tilde = np.zeros(self.N) self.v_tilde = np.zeros(self.N) #Initial values - Posterior distribution parameters: q(f|X,Y) = N(f|mu,Sigma) - self.mu = np.zeros(self.N) - self.Sigma = K.copy() + mu = np.zeros(self.N) + Sigma = K.copy() """ Initial values - Cavity distribution parameters: @@ -94,29 +94,27 @@ class EP(likelihood): update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters - self.tau_[i] = 1./self.Sigma[i,i] - self.eta*self.tau_tilde[i] - self.v_[i] = self.mu[i]/self.Sigma[i,i] - self.eta*self.v_tilde[i] - print 1./self.Sigma[i,i],self.tau_tilde[i] + self.tau_[i] = 1./Sigma[i,i] - self.eta*self.tau_tilde[i] + self.v_[i] = mu[i]/Sigma[i,i] - self.eta*self.v_tilde[i] #Marginal moments self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood_function.moments_match(self.data[i],self.tau_[i],self.v_[i]) #Site parameters update - Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma[i,i]) - Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma[i,i]) + Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./Sigma[i,i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - mu[i]/Sigma[i,i]) self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau self.v_tilde[i] = self.v_tilde[i] + Delta_v #Posterior distribution parameters update - si=self.Sigma[:,i].reshape(self.N,1) - self.Sigma = self.Sigma - Delta_tau/(1.+ Delta_tau*self.Sigma[i,i])*np.dot(si,si.T) - self.mu = np.dot(self.Sigma,self.v_tilde) + si=Sigma[:,i].reshape(self.N,1) + Sigma = Sigma - Delta_tau/(1.+ Delta_tau*Sigma[i,i])*np.dot(si,si.T) + mu = np.dot(Sigma,self.v_tilde) self.iterations += 1 - print self.tau_tilde[i] #Sigma recomptutation with Cholesky decompositon Sroot_tilde_K = np.sqrt(self.tau_tilde)[:,None]*K B = np.eye(self.N) + np.sqrt(self.tau_tilde)[None,:]*Sroot_tilde_K L = jitchol(B) V,info = linalg.flapack.dtrtrs(L,Sroot_tilde_K,lower=1) - self.Sigma = K - np.dot(V.T,V) - self.mu = np.dot(self.Sigma,self.v_tilde) + Sigma = K - np.dot(V.T,V) + mu = np.dot(Sigma,self.v_tilde) epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) @@ -139,7 +137,7 @@ class EP(likelihood): """ Kmmi, Lm, Lmi, Kmm_logdet = pdinv(Kmm) KmnKnm = np.dot(Kmn, Kmn.T) - KmmiKmn = np.dot(Kmmi,self.Kmn) + KmmiKmn = np.dot(Kmmi,Kmn) Qnn_diag = np.sum(Kmn*KmmiKmn,-2) LLT0 = Kmm.copy() @@ -221,13 +219,13 @@ class EP(likelihood): q(f|X) = int_{df}{N(f|KfuKuu_invu,diag(Kff-Qff)*N(u|0,Kuu)} = N(f|0,Sigma0) Sigma0 = diag(Knn-Qnn) + Qnn, Qnn = Knm*Kmmi*Kmn """ - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.P0 = self.Kmn.T - self.KmnKnm = np.dot(self.P0.T, self.P0) - self.KmmiKmn = np.dot(self.Kmmi,self.P0.T) - self.Qnn_diag = np.sum(self.P0.T*self.KmmiKmn,-2) - self.Diag0 = self.Knn_diag - self.Qnn_diag - self.R0 = jitchol(self.Kmmi).T + Kmmi, self.Lm, self.Lmi, Kmm_logdet = pdinv(Kmm) + P0 = Kmn.T + KmnKnm = np.dot(P0.T, P0) + KmmiKmn = np.dot(Kmmi,P0.T) + Qnn_diag = np.sum(P0.T*KmmiKmn,-2) + Diag0 = Knn_diag - Qnn_diag + R0 = jitchol(Kmmi).T """ Posterior approximation: q(f|y) = N(f| mu, Sigma) @@ -236,11 +234,11 @@ class EP(likelihood): """ self.w = np.zeros(self.N) self.gamma = np.zeros(self.M) - self.mu = np.zeros(self.N) - self.P = self.P0.copy() - self.R = self.R0.copy() - self.Diag = self.Diag0.copy() - self.Sigma_diag = self.Knn_diag + mu = np.zeros(self.N) + P = P0.copy() + R = R0.copy() + Diag = Diag0.copy() + Sigma_diag = Knn_diag """ Initial values - Cavity distribution parameters: @@ -268,41 +266,41 @@ class EP(likelihood): update_order = np.random.permutation(self.N) for i in update_order: #Cavity distribution parameters - self.tau_[i] = 1./self.Sigma_diag[i] - self.eta*self.tau_tilde[i] - self.v_[i] = self.mu[i]/self.Sigma_diag[i] - self.eta*self.v_tilde[i] + self.tau_[i] = 1./Sigma_diag[i] - self.eta*self.tau_tilde[i] + self.v_[i] = mu[i]/Sigma_diag[i] - self.eta*self.v_tilde[i] #Marginal moments self.Z_hat[i], mu_hat[i], sigma2_hat[i] = self.likelihood_function.moments_match(data[i],self.tau_[i],self.v_[i]) #Site parameters update - Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./self.Sigma_diag[i]) - Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - self.mu[i]/self.Sigma_diag[i]) + Delta_tau = self.delta/self.eta*(1./sigma2_hat[i] - 1./Sigma_diag[i]) + Delta_v = self.delta/self.eta*(mu_hat[i]/sigma2_hat[i] - mu[i]/Sigma_diag[i]) self.tau_tilde[i] = self.tau_tilde[i] + Delta_tau self.v_tilde[i] = self.v_tilde[i] + Delta_v #Posterior distribution parameters update - dtd1 = Delta_tau*self.Diag[i] + 1. - dii = self.Diag[i] - self.Diag[i] = dii - (Delta_tau * dii**2.)/dtd1 - pi_ = self.P[i,:].reshape(1,self.M) - self.P[i,:] = pi_ - (Delta_tau*dii)/dtd1 * pi_ - Rp_i = np.dot(self.R,pi_.T) - RTR = np.dot(self.R.T,np.dot(np.eye(self.M) - Delta_tau/(1.+Delta_tau*self.Sigma_diag[i]) * np.dot(Rp_i,Rp_i.T),self.R)) - self.R = jitchol(RTR).T + dtd1 = Delta_tau*Diag[i] + 1. + dii = Diag[i] + Diag[i] = dii - (Delta_tau * dii**2.)/dtd1 + pi_ = P[i,:].reshape(1,self.M) + P[i,:] = pi_ - (Delta_tau*dii)/dtd1 * pi_ + Rp_i = np.dot(R,pi_.T) + RTR = np.dot(R.T,np.dot(np.eye(self.M) - Delta_tau/(1.+Delta_tau*Sigma_diag[i]) * np.dot(Rp_i,Rp_i.T),R)) + R = jitchol(RTR).T self.w[i] = self.w[i] + (Delta_v - Delta_tau*self.w[i])*dii/dtd1 - self.gamma = self.gamma + (Delta_v - Delta_tau*self.mu[i])*np.dot(RTR,self.P[i,:].T) - self.RPT = np.dot(self.R,self.P.T) - self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) - self.mu = self.w + np.dot(self.P,self.gamma) + self.gamma = self.gamma + (Delta_v - Delta_tau*mu[i])*np.dot(RTR,P[i,:].T) + RPT = np.dot(R,P.T) + Sigma_diag = Diag + np.sum(RPT.T*RPT.T,-1) + mu = self.w + np.dot(P,self.gamma) self.iterations += 1 #Sigma recomptutation with Cholesky decompositon - self.Diag = self.Diag0/(1.+ self.Diag0 * self.tau_tilde) - self.P = (self.Diag / self.Diag0)[:,None] * self.P0 - self.RPT0 = np.dot(self.R0,self.P0.T) - L = jitchol(np.eye(self.M) + np.dot(self.RPT0,(1./self.Diag0 - self.Diag/(self.Diag0**2))[:,None]*self.RPT0.T)) - self.R,info = linalg.flapack.dtrtrs(L,self.R0,lower=1) - self.RPT = np.dot(self.R,self.P.T) - self.Sigma_diag = self.Diag + np.sum(self.RPT.T*self.RPT.T,-1) - self.w = self.Diag * self.v_tilde - self.gamma = np.dot(self.R.T, np.dot(self.RPT,self.v_tilde)) - self.mu = self.w + np.dot(self.P,self.gamma) + Diag = Diag0/(1.+ Diag0 * self.tau_tilde) + P = (Diag / Diag0)[:,None] * P0 + RPT0 = np.dot(R0,P0.T) + L = jitchol(np.eye(self.M) + np.dot(RPT0,(1./Diag0 - Diag/(Diag0**2))[:,None]*RPT0.T)) + R,info = linalg.flapack.dtrtrs(L,R0,lower=1) + RPT = np.dot(R,P.T) + Sigma_diag = Diag + np.sum(RPT.T*RPT.T,-1) + self.w = Diag * self.v_tilde + self.gamma = np.dot(R.T, np.dot(RPT,self.v_tilde)) + mu = self.w + np.dot(P,self.gamma) epsilon_np1 = sum((self.tau_tilde-self.np1[-1])**2)/self.N epsilon_np2 = sum((self.v_tilde-self.np2[-1])**2)/self.N self.np1.append(self.tau_tilde.copy()) diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 39428c70..4f571e14 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -7,7 +7,6 @@ from scipy import stats import scipy as sp import pylab as pb from ..util.plot import gpplot -#from . import EP class likelihood_function: """ diff --git a/GPy/models/GP.py b/GPy/models/GP.py index e64da2c9..0c3ea6b7 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -7,7 +7,7 @@ import pylab as pb from .. import kern from ..core import model from ..util.linalg import pdinv,mdot -from ..util.plot import gpplot, Tango +from ..util.plot import gpplot,x_frame, Tango from ..likelihoods import EP class GP(model): @@ -175,37 +175,7 @@ class GP(model): return mean, _5pc, _95pc - def _x_frame(self,plot_limits=None,which_data='all',which_functions='all',resolution=None): - """ - Internal helper function for making plots, return a set of new input values to plot as well as lower and upper limits - """ - if which_functions=='all': - which_functions = [True]*self.kern.Nparts - if which_data=='all': - which_data = slice(None) - - X = self.X[which_data,:] - Y = self.likelihood.Y[which_data,:] - - if plot_limits is None: - xmin,xmax = X.min(0),X.max(0) - xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) - elif len(plot_limits)==2: - xmin, xmax = plot_limits - else: - raise ValueError, "Bad limits for plotting" - - if self.X.shape[1]==1: - Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - elif self.X.shape[1]==2: - resolution = resolution or 50 - xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] - Xnew = np.vstack((xx.flatten(),yy.flatten())).T - else: - raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" - return Xnew, xmin, xmax - - def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + def plot_GP(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): """ Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian @@ -223,21 +193,29 @@ class GP(model): - In higher dimensions, we've no implemented this yet !TODO! Can plot only part of the data and part of the posterior functions using which_data and which_functions - """ - """ Plot the data's view of the world, with non-normalised values and GP predictions passed through the likelihood """ - Xnew, xmin, xmax = self._x_frame() - m,v = self._raw_predict(Xnew) - if isinstance(self.likelihood,EP): - pb.subplot(211) + if which_functions=='all': + which_functions = [True]*self.kern.Nparts + if which_data=='all': + which_data = slice(None) + + Xnew, xmin, xmax = x_frame(self.X, plot_limits=plot_limits) + + m,v = self._raw_predict(Xnew, slices=which_functions) gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) - pb.plot(self.X,self.likelihood.Y,'kx',mew=1.5) + pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) pb.xlim(xmin,xmax) - if isinstance(self.likelihood,EP): - pb.subplot(212) - phi_m,phi_l,phi_u = self.likelihood.predictive_values(m,v) - gpplot(Xnew,phi_m,phi_l,phi_u) - pb.plot(self.X,self.likelihood.data,'kx',mew=1.5) - pb.xlim(xmin,xmax) + def plot_output(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + if which_functions=='all': + which_functions = [True]*self.kern.Nparts + if which_data=='all': + which_data = slice(None) + Xnew, xmin, xmax = x_frame(self.X, plot_limits=plot_limits) + m, lower, upper = self.predict(Xnew, slices=which_functions) + gpplot(Xnew,m, lower, upper) + pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) + ymin,ymax = self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 + pb.xlim(xmin,xmax) + pb.ylim(ymin,ymax) diff --git a/GPy/util/plot.py b/GPy/util/plot.py index bf372869..60e3e488 100644 --- a/GPy/util/plot.py +++ b/GPy/util/plot.py @@ -70,4 +70,24 @@ def align_subplots(N,M,xlim=None, ylim=None): else: removeUpperTicks() +def x_frame(X,plot_limits=None,resolution=None): + """ + Internal helper function for making plots, returns a set of input values to plot as well as lower and upper limits + """ + if plot_limits is None: + xmin,xmax = X.min(0),X.max(0) + xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) + elif len(plot_limits)==2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + if X.shape[1]==1: + Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] + elif X.shape[1]==2: + resolution = resolution or 50 + xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + Xnew = np.vstack((xx.flatten(),yy.flatten())).T + else: + raise NotImplementedError, "Cannot define a frame with more than two input dimensions" + return Xnew, xmin, xmax From 24b6dfa086ad1d992e7a2171c0886a64b28bba62 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 16:21:26 +0000 Subject: [PATCH 106/197] Classification examples corrected (2/3) --- GPy/examples/classification.py | 78 ++++++++++++++++++++-------------- GPy/examples/ep_fix.py | 53 ----------------------- GPy/testing/unit_tests.py | 19 ++++----- 3 files changed, 54 insertions(+), 96 deletions(-) delete mode 100644 GPy/examples/ep_fix.py diff --git a/GPy/examples/classification.py b/GPy/examples/classification.py index fb14139d..7645964d 100644 --- a/GPy/examples/classification.py +++ b/GPy/examples/classification.py @@ -3,16 +3,15 @@ """ -Simple Gaussian Processes classification +Gaussian Processes classification """ import pylab as pb import numpy as np import GPy default_seed=10000 -###################################### -## 2 dimensional example -def crescent_data(model_type='Full', inducing=10, seed=default_seed): + +def crescent_data(model_type='Full', inducing=10, seed=default_seed): #FIXME """Run a Gaussian process classification on the crescent data. The demonstration calls the basic GP classification model and uses EP to approximate the likelihood. :param model_type: type of model to fit ['Full', 'FITC', 'DTC']. @@ -30,7 +29,7 @@ def crescent_data(model_type='Full', inducing=10, seed=default_seed): # create sparse GP EP model m = GPy.models.sparse_GP_EP(data['X'],likelihood=likelihood,inducing=inducing,ep_proxy=model_type) - m.approximate_likelihood() + m.update_likelihood_approximation() print(m) # optimize @@ -42,53 +41,66 @@ def crescent_data(model_type='Full', inducing=10, seed=default_seed): return m def oil(): - """Run a Gaussian process classification on the oil data. The demonstration calls the basic GP classification model and uses EP to approximate the likelihood.""" + """ + Run a Gaussian process classification on the oil data. The demonstration calls the basic GP classification model and uses EP to approximate the likelihood. + """ data = GPy.util.datasets.oil() - likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) + # Kernel object + kernel = GPy.kern.rbf(12) - # create simple GP model - m = GPy.models.GP_EP(data['X'],likelihood) + # Likelihood object + distribution = GPy.likelihoods.likelihood_functions.probit() + likelihood = GPy.likelihoods.EP(data['Y'][:, 0:1],distribution) - # contrain all parameters to be positive + # Create GP model + m = GPy.models.GP(data['X'],kernel,likelihood=likelihood) + + # Contrain all parameters to be positive m.constrain_positive('') m.tie_param('lengthscale') - m.approximate_likelihood() + m.update_likelihood_approximation() - # optimize + # Optimize m.optimize() - # plot - #m.plot() print(m) return m -def toy_linear_1d_classification(model_type='Full', inducing=4, seed=default_seed): - """Simple 1D classification example. - :param model_type: type of model to fit ['Full', 'FITC', 'DTC']. +def toy_linear_1d_classification(seed=default_seed): + """ + Simple 1D classification example :param seed : seed value for data generation (default is 4). :type seed: int - :param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). :type inducing: int """ + data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) - likelihood = GPy.inference.likelihoods.probit(data['Y'][:, 0:1]) - assert model_type in ('Full','DTC','FITC') - # create simple GP model - if model_type=='Full': - m = GPy.models.GP_EP(data['X'],likelihood) - else: - # create sparse GP EP model - m = GPy.models.sparse_GP_EP(data['X'],likelihood=likelihood,inducing=inducing,ep_proxy=model_type) + # Kernel object + kernel = GPy.kern.rbf(1) - m.constrain_positive('var') - m.constrain_positive('len') - m.tie_param('lengthscale') - m.approximate_likelihood() + # Likelihood object + distribution = GPy.likelihoods.likelihood_functions.probit() + likelihood = GPy.likelihoods.EP(data['Y'][:, 0:1],distribution) - # Optimize and plot - m.em(plot_all=False) # EM algorithm - m.plot() + # Model definition + m = GPy.models.GP(data['X'],kernel,likelihood=likelihood) + # Optimize + """ + EPEM runs a loop that consists of two steps: + 1) EP likelihood approximation: + m.update_likelihood_approximation() + 2) Parameters optimization: + m.optimize() + """ + m.EPEM() + + # Plot + pb.subplot(211) + m.plot_GP() + pb.subplot(212) + m.plot_output() print(m) + return m diff --git a/GPy/examples/ep_fix.py b/GPy/examples/ep_fix.py deleted file mode 100644 index 440d00aa..00000000 --- a/GPy/examples/ep_fix.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - -""" -Simple Gaussian Processes classification 1D -probit likelihood -""" -import pylab as pb -import numpy as np -import GPy -pb.ion() - -pb.close('all') - -# Inputs -N = 30 -X1 = np.random.normal(5,2,N/2) -X2 = np.random.normal(10,2,N/2) -X = np.hstack([X1,X2])[:,None] - -# Output -Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] - -# Kernel object -kernel = GPy.kern.rbf(1) - -# Likelihood object -distribution = GPy.likelihoods.likelihood_functions.probit() -likelihood = GPy.likelihoods.EP(Y,distribution) - -# Model definition -m = GPy.models.GP(X,kernel,likelihood=likelihood) - -# Model constraints -m.ensure_default_constraints() - -# Optimize model -""" -EPEM runs a loop that consists of two steps: -1) EP likelihood approximation: - m.update_likelihood_approximation() -2) Parameters optimization: - m.optimize() -""" -m.EPEM() - -# Plot -pb.subplot(211) -m.plot_GP() -pb.subplot(212) -m.plot_output() - -print(m) diff --git a/GPy/testing/unit_tests.py b/GPy/testing/unit_tests.py index a302b25f..4cc1c4ab 100644 --- a/GPy/testing/unit_tests.py +++ b/GPy/testing/unit_tests.py @@ -154,17 +154,16 @@ class GradientTests(unittest.TestCase): m.constrain_positive('(linear|bias|white)') self.assertTrue(m.checkgrad()) - def test_GP_EP(self): - return # Disabled TODO + def test_GP_EP_probit(self): N = 20 - X = np.hstack([np.random.rand(N/2)+1,np.random.rand(N/2)-1])[:,None] - k = GPy.kern.rbf(1) + GPy.kern.white(1) - Y = np.hstack([np.ones(N/2),-np.ones(N/2)])[:,None] - likelihood = GPy.inference.likelihoods.probit(Y) - m = GPy.models.GP_EP(X,likelihood,k) - m.constrain_positive('(var|len)') - m.approximate_likelihood() - self.assertTrue(m.checkgrad()) + X = np.hstack([np.random.normal(5,2,N/2),np.random.normal(10,2,N/2)])[:,None] + Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] + kernel = GPy.kern.rbf(1) + distribution = GPy.likelihoods.likelihood_functions.probit() + likelihood = GPy.likelihoods.EP(Y,distribution) + m = GPy.models.GP(X,kernel,likelihood=likelihood) + m.ensure_default_constraints() + self.assertTrue(m.EPEM) @unittest.skip("FITC will be broken for a while") def test_generalized_FITC(self): From 5447d6fbfc48c30299bfb41bf3419479d589e37c Mon Sep 17 00:00:00 2001 From: James Hensman Date: Fri, 1 Feb 2013 17:12:45 +0000 Subject: [PATCH 107/197] Assorted work on combining the EP and sparse methods --- GPy/likelihoods/EP.py | 2 + GPy/likelihoods/Gaussian.py | 5 +- GPy/models/GP.py | 4 +- GPy/models/GP_regression.py | 2 +- GPy/models/__init__.py | 9 ++-- GPy/models/sparse_GP.py | 73 ++++++++++++++++-------------- GPy/models/sparse_GP_regression.py | 44 ++++++++++++++++++ 7 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 GPy/models/sparse_GP_regression.py diff --git a/GPy/likelihoods/EP.py b/GPy/likelihoods/EP.py index 1148ff4c..3b76a737 100644 --- a/GPy/likelihoods/EP.py +++ b/GPy/likelihoods/EP.py @@ -19,6 +19,7 @@ class EP(likelihood): self.data = data self.N = self.data.size self.is_heteroscedastic = True + self.Nparams = 0 #Initial values - Likelihood approximation parameters: #p(y|f) = t(f|tau_tilde,v_tilde) @@ -28,6 +29,7 @@ class EP(likelihood): #initial values for the GP variables self.Y = np.zeros((self.N,1)) self.covariance_matrix = np.eye(self.N) + self.precision = np.ones(self.N) self.Z = 0 self.YYT = None diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 8c1c93f5..94cb0560 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -4,6 +4,7 @@ from likelihood import likelihood class Gaussian(likelihood): def __init__(self,data,variance=1.,normalize=False): self.is_heteroscedastic = False + self.Nparams = 1 self.data = data self.N,D = data.shape self.Z = 0. # a correction factor which accounts for the approximation made @@ -18,7 +19,9 @@ class Gaussian(likelihood): self._std = np.ones((1,D)) self.Y = self.data + #TODO: make this work efficiently (only compute YYT if D>>N) self.YYT = np.dot(self.Y,self.Y.T) + self.trYYT = np.trace(self.YYT) self._set_params(np.asarray(variance)) @@ -50,4 +53,4 @@ class Gaussian(likelihood): pass def _gradients(self,partial): - return np.sum(np.diag(partial)) + return np.sum(partial) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index e64da2c9..d30a31e0 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -31,7 +31,7 @@ class GP(model): """ - def __init__(self, X, kernel, likelihood, normalize_X=False, Xslices=None): + def __init__(self, X, likelihood, kernel, normalize_X=False, Xslices=None): # parse arguments self.Xslices = Xslices @@ -121,7 +121,7 @@ class GP(model): For the likelihood parameters, pass in alpha = K^-1 y """ - return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK,X=self.X), self.likelihood._gradients(partial=self.dL_dK))) + return np.hstack((self.kern.dK_dtheta(partial=self.dL_dK,X=self.X), self.likelihood._gradients(partial=np.diag(self.dL_dK)))) def _raw_predict(self,_Xnew,slices=None, full_cov=False): """ diff --git a/GPy/models/GP_regression.py b/GPy/models/GP_regression.py index 916e5284..5f9f9f3e 100644 --- a/GPy/models/GP_regression.py +++ b/GPy/models/GP_regression.py @@ -33,4 +33,4 @@ class GP_regression(GP): likelihood = likelihoods.Gaussian(Y,normalize=normalize_Y) - GP.__init__(self, X, kernel, likelihood, normalize_X=normalize_X, Xslices=Xslices) + GP.__init__(self, X, likelihood, kernel, normalize_X=normalize_X, Xslices=Xslices) diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index 1175eb71..2269610d 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -2,14 +2,13 @@ # Licensed under the BSD 3-clause license (see LICENSE.txt) -#from sparse_GP_regression import sparse_GP_regression -# TODO ^^ remove these? +from GP import GP +from GP_regression import GP_regression +from sparse_GP import sparse_GP +from sparse_GP_regression import sparse_GP_regression from GPLVM import GPLVM from warped_GP import warpedGP # TODO: from generalized_FITC import generalized_FITC #from sparse_GPLVM import sparse_GPLVM #from uncollapsed_sparse_GP import uncollapsed_sparse_GP -from GP import GP -from GP_regression import GP_regression -#from sparse_GP import sparse_GP #from BGPLVM import Bayesian_GPLVM diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 5048a174..7252f085 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -6,7 +6,6 @@ import pylab as pb from ..util.linalg import mdot, jitchol, chol_inv, pdinv from ..util.plot import gpplot from .. import kern -from ..inference.likelihoods import likelihood from GP import GP #Still TODO: @@ -34,16 +33,15 @@ class sparse_GP(GP): :type normalize_(X|Y): bool """ - def __init__(self,X,likelihood,kernel, X_uncertainty=None, Z=None,Zslices=None,M=10,normalize_X=False): - self.scale_factor = 1000.0# a scaling factor to help keep the algorithm stable + def __init__(self, X, likelihood, kernel, Z, X_uncertainty=None, Xslices=None,Zslices=None, normalize_X=False): + self.scale_factor = 1.0# a scaling factor to help keep the algorithm stable + + self.Z = Z + self.Zslices = Zslices + self.Xslices = Xslices + self.M = Z.shape[0] + self.likelihood = likelihood - if Z is None: - self.Z = np.random.permutation(X.copy())[:M] - self.M = M - else: - assert Z.shape[1]==X.shape[1] - self.Z = Z - self.M = Z.shape[0] if X_uncertainty is None: self.has_uncertain_inputs=False else: @@ -51,7 +49,7 @@ class sparse_GP(GP): self.has_uncertain_inputs=True self.X_uncertainty = X_uncertainty - GP.__init__(self, X, Y, kernel=kernel, normalize_X=normalize_X, Xslices=Xslices) + GP.__init__(self, X, likelihood, kernel=kernel, normalize_X=normalize_X, Xslices=Xslices) #normalise X uncertainty also if self.has_uncertain_inputs: @@ -67,7 +65,7 @@ class sparse_GP(GP): # kernel computations, using BGPLVM notation self.Kmm = self.kern.K(self.Z) if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty).sum() + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncerTainty) self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) if self.likelihood.is_heteroscedastic: @@ -76,17 +74,18 @@ class sparse_GP(GP): else: self.psi2_beta_scaled = (self.psi2*(self.likelihood.precision/sf2)).sum(0) else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices).sum() + self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices) self.psi1 = self.kern.K(self.Z,self.X) if self.likelihood.is_heteroscedastic: tmp = self.psi1*(np.sqrt(self.likelihood.precision.reshape(self.N,1))/sf) else: tmp = self.psi1*(np.sqrt(self.likelihood.precision)/sf) self.psi2_beta_scaled = np.dot(tmp,tmp.T) + self.psi2 = self.psi1.T[:,:,None]*self.psi1.T[:,None,:] # TODO: remove me for efficiency and stability self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.V = (self.likelihood.precision/self.scale_factor)*self.Y + self.V = (self.likelihood.precision/self.scale_factor)*self.likelihood.Y self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) self.B = np.eye(self.M)/sf2 + self.A @@ -97,27 +96,38 @@ class sparse_GP(GP): self.C = mdot(self.Lmi.T, self.Bi, self.Lmi) self.E = mdot(self.C, self.psi1VVpsi1/sf2, self.C.T) - # Compute dL_dpsi # FIXME - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) + # Compute dL_dpsi # FIXME: this is untested for the het. case + self.dL_dpsi0 = - 0.5 * self.D * self.likelihood.precision * np.ones(self.N) self.dL_dpsi1 = mdot(self.V, self.psi1V.T,self.C).T - self.dL_dpsi2 = 0.5 * self.beta * self.D * self.Kmmi[None,:,:] # dB - self.dL_dpsi2 += - 0.5 * self.beta/sf2 * self.D * self.C[None,:,:] # dC - self.dL_dpsi2 += - 0.5 * self.beta * self.E[None,:,:] # dD + if self.likelihood.is_heteroscedastic: + self.dL_dpsi2 = 0.5 * self.likelihood.precision[:,None,None] * self.D * self.Kmmi[None,:,:] # dB + self.dL_dpsi2 += - 0.5 * self.likelihood.precision[:,None,None]/sf2 * self.D * self.C[None,:,:] # dC + self.dL_dpsi2 += - 0.5 * self.likelihood.precision[:,None,None]* self.E[None,:,:] # dD + else: + self.dL_dpsi2 = 0.5 * self.likelihood.precision * self.D * self.Kmmi[None,:,:] # dB + self.dL_dpsi2 += - 0.5 * self.likelihood.precision/sf2 * self.D * self.C[None,:,:] # dC + self.dL_dpsi2 += - 0.5 * self.likelihood.precision * self.E[None,:,:] # dD # Compute dL_dKmm self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi)*sf2 # dB self.dL_dKmm += -0.5 * self.D * (- self.C/sf2 - 2.*mdot(self.C, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC self.dL_dKmm += np.dot(np.dot(self.E*sf2, self.psi2_beta_scaled) - np.dot(self.C, self.psi1VVpsi1), self.Kmmi) + 0.5*self.E # dD + #the partial derivative vector for the likelihood + self.partial_for_likelihood = - 0.5 * self.D*self.likelihood.precision + 0.5 * (self.likelihood.Y**2).sum(1)*self.likelihood.precision**2 #dA + self.partial_for_likelihood += 0.5 * self.D * (self.psi0*self.likelihood.precision**2 - (self.psi2*self.Kmmi[None,:,:]*self.likelihood.precision[:,None,None]**2).sum(1).sum(1)/sf2) #dB + #self.partial_for_likelihood += 0.5 * self.D * np.sum(self.Bi*self.A)*self.likelihood.precision #dC + #self.partial_for_likelihood += -np.diag(np.dot((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) , self.psi1VVpsi1 ))*self.likelihood.precision #dD + def _set_params(self, p): self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) - self.beta = p[self.M*self.Q] # FIXME - self.kern._set_params(p[self.Z.size + 1:]) + self.kern._set_params(p[self.Z.size:self.Z.size+self.kern.Nparam]) + self.likelihood._set_params(p[self.Z.size+self.kern.Nparam:]) self._computations() def _get_params(self): - return np.hstack([self.Z.flatten(),GP._get_params(self)) + return np.hstack([self.Z.flatten(),GP._get_params(self)]) def _get_param_names(self): return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + GP._get_param_names(self) @@ -125,24 +135,17 @@ class sparse_GP(GP): def log_likelihood(self): """ Compute the (lower bound on the) log marginal likelihood """ sf2 = self.scale_factor**2 - A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) -0.5*self.beta*self.trYYT # FIXME - B = -0.5*self.D*(self.beta*self.psi0-np.trace(self.A)*sf2)# FIXME + if self.likelihood.is_heteroscedastic: + A = -0.5*self.N*self.D*np.log(2.*np.pi) +0.5*np.sum(np.log(self.likelihood.precision)) -0.5*np.sum(self.V*self.likelihood.Y) + else: + A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.likelihood.precision)) -0.5*self.likelihood.precision*self.likelihood.trYYT + B = -0.5*self.D*(np.sum(self.likelihood.precision*self.psi0) - np.trace(self.A)*sf2) C = -0.5*self.D * (self.B_logdet + self.M*np.log(sf2)) D = +0.5*np.sum(self.psi1VVpsi1 * self.C) return A+B+C+D def _log_likelihood_gradients(self): - return np.hstack([self.dL_dZ().flatten(), GP._log_likelihood_gradients(self)]) - - # FIXME: move this into the lieklihood class - def dL_dbeta(self): - sf2 = self.scale_factor**2 - dA_dbeta = 0.5 * self.N*self.D/self.beta - 0.5 * self.trYYT - dB_dbeta = - 0.5 * self.D * (self.psi0 - np.trace(self.A)/self.beta*sf2) - dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta - dD_dbeta = np.sum((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) * self.psi1VVpsi1 )/self.beta - - return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta) + return np.hstack((self.dL_dZ().flatten(), self.dL_dtheta(), self.likelihood._gradients(partial=self.partial_for_likelihood))) def dL_dtheta(self): """ diff --git a/GPy/models/sparse_GP_regression.py b/GPy/models/sparse_GP_regression.py new file mode 100644 index 00000000..178c8023 --- /dev/null +++ b/GPy/models/sparse_GP_regression.py @@ -0,0 +1,44 @@ +# Copyright (c) 2012, James Hensman +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +import numpy as np +from sparse_GP import sparse_GP +from .. import likelihoods +from .. import kern + +class sparse_GP_regression(sparse_GP): + """ + Gaussian Process model for regression + + This is a thin wrapper around the GP class, with a set of sensible defalts + + :param X: input observations + :param Y: observed values + :param kernel: a GPy kernel, defaults to rbf+white + :param normalize_X: whether to normalize the input data before computing (predictions will be in original scales) + :type normalize_X: False|True + :param normalize_Y: whether to normalize the input data before computing (predictions will be in original scales) + :type normalize_Y: False|True + :param Xslices: how the X,Y data co-vary in the kernel (i.e. which "outputs" they correspond to). See (link:slicing) + :rtype: model object + + .. Note:: Multiple independent outputs are allowed using columns of Y + + """ + + def __init__(self,X,Y,kernel=None,normalize_X=False,normalize_Y=False, Xslices=None,Z=None, M=10): + #kern defaults to rbf + if kernel is None: + kernel = kern.rbf(X.shape[1]) + kern.white(X.shape[1],1e-3) + + #Z defaults to a subset of the data + if Z is None: + Z = np.random.permutation(X.copy())[:M] + else: + assert Z.shape[1]==X.shape[1] + + #likelihood defaults to Gaussian + likelihood = likelihoods.Gaussian(Y,normalize=normalize_Y) + + sparse_GP.__init__(self, X, likelihood, kernel, Z, normalize_X=normalize_X, Xslices=Xslices) From 2b756e96e1902168138a864e8a8048c306cd93c8 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Fri, 1 Feb 2013 17:42:51 +0000 Subject: [PATCH 108/197] made the BGPLVM work in the new world order --- GPy/models/BGPLVM.py | 28 +++++++++++++++++++--------- GPy/models/__init__.py | 4 ++-- GPy/models/sparse_GP.py | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/GPy/models/BGPLVM.py b/GPy/models/BGPLVM.py index db147944..ffa7df54 100644 --- a/GPy/models/BGPLVM.py +++ b/GPy/models/BGPLVM.py @@ -5,10 +5,12 @@ import numpy as np import pylab as pb import sys, pdb from GPLVM import GPLVM -from sparse_GP_regression import sparse_GP_regression +from sparse_GP import sparse_GP from GPy.util.linalg import pdinv +from ..likelihoods import Gaussian +from .. import kern -class Bayesian_GPLVM(sparse_GP_regression, GPLVM): +class Bayesian_GPLVM(sparse_GP, GPLVM): """ Bayesian Gaussian Process Latent Variable Model @@ -20,15 +22,23 @@ class Bayesian_GPLVM(sparse_GP_regression, GPLVM): :type init: 'PCA'|'random' """ - def __init__(self, Y, Q, init='PCA', **kwargs): + def __init__(self, Y, Q, init='PCA', M=10, Z=None, **kwargs): X = self.initialise_latent(init, Q, Y) - S = np.ones_like(X) * 1e-2# - sparse_GP_regression.__init__(self, X, Y, X_uncertainty = S, **kwargs) + + if Z is None: + Z = np.random.permutation(X.copy())[:M] + else: + assert Z.shape[1]==X.shape[1] + + kernel = kern.rbf(Q) + kern.white(Q) + + S = np.ones_like(X) * 1e-2# + sparse_GP.__init__(self, X, Gaussian(Y), X_uncertainty = S, Z=Z,**kwargs) def _get_param_names(self): X_names = sum([['X_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[]) S_names = sum([['S_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[]) - return (X_names + S_names + sparse_GP_regression._get_param_names(self)) + return (X_names + S_names + sparse_GP._get_param_names(self)) def _get_params(self): """ @@ -40,13 +50,13 @@ class Bayesian_GPLVM(sparse_GP_regression, GPLVM): =============================================================== """ - return np.hstack((self.X.flatten(), self.X_uncertainty.flatten(), sparse_GP_regression._get_params(self))) + return np.hstack((self.X.flatten(), self.X_uncertainty.flatten(), sparse_GP._get_params(self))) def _set_params(self,x): N, Q = self.N, self.Q self.X = x[:self.X.size].reshape(N,Q).copy() self.X_uncertainty = x[(N*Q):(2*N*Q)].reshape(N,Q).copy() - sparse_GP_regression._set_params(self, x[(2*N*Q):]) + sparse_GP._set_params(self, x[(2*N*Q):]) def dL_dmuS(self): dL_dmu_psi0, dL_dS_psi0 = self.kern.dpsi1_dmuS(self.dL_dpsi1,self.Z,self.X,self.X_uncertainty) @@ -58,5 +68,5 @@ class Bayesian_GPLVM(sparse_GP_regression, GPLVM): return np.hstack((dL_dmu.flatten(), dL_dS.flatten())) def _log_likelihood_gradients(self): - return np.hstack((self.dL_dmuS().flatten(), sparse_GP_regression._log_likelihood_gradients(self))) + return np.hstack((self.dL_dmuS().flatten(), sparse_GP._log_likelihood_gradients(self))) diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index 2269610d..9cc8fa68 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -9,6 +9,6 @@ from sparse_GP_regression import sparse_GP_regression from GPLVM import GPLVM from warped_GP import warpedGP # TODO: from generalized_FITC import generalized_FITC -#from sparse_GPLVM import sparse_GPLVM +from sparse_GPLVM import sparse_GPLVM #from uncollapsed_sparse_GP import uncollapsed_sparse_GP -#from BGPLVM import Bayesian_GPLVM +from BGPLVM import Bayesian_GPLVM diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 7252f085..f35b3918 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -65,7 +65,7 @@ class sparse_GP(GP): # kernel computations, using BGPLVM notation self.Kmm = self.kern.K(self.Z) if self.has_uncertain_inputs: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncerTainty) + self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty) self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) if self.likelihood.is_heteroscedastic: From 5e2baf191954521fc84280d2ed363e32be9548c2 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 1 Feb 2013 17:58:21 +0000 Subject: [PATCH 109/197] Changes in plotting functions. --- GPy/examples/classification.py | 6 +-- GPy/likelihoods/Gaussian.py | 2 +- GPy/likelihoods/likelihood_functions.py | 4 +- GPy/models/GP.py | 59 ++++++++++++++++++------- GPy/util/plot.py | 31 +++++++++---- 5 files changed, 71 insertions(+), 31 deletions(-) diff --git a/GPy/examples/classification.py b/GPy/examples/classification.py index 7645964d..c25ea124 100644 --- a/GPy/examples/classification.py +++ b/GPy/examples/classification.py @@ -84,7 +84,7 @@ def toy_linear_1d_classification(seed=default_seed): likelihood = GPy.likelihoods.EP(data['Y'][:, 0:1],distribution) # Model definition - m = GPy.models.GP(data['X'],kernel,likelihood=likelihood) + m = GPy.models.GP(data['X'],likelihood=likelihood,kernel=kernel) # Optimize """ @@ -98,9 +98,9 @@ def toy_linear_1d_classification(seed=default_seed): # Plot pb.subplot(211) - m.plot_GP() + m.plot_internal() pb.subplot(212) - m.plot_output() + m.plot() print(m) return m diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 94cb0560..ff358b2d 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -42,7 +42,7 @@ class Gaussian(likelihood): """ mean = mu*self._std + self._mean true_var = (var + self._variance)*self._std**2 - _5pc = mean + mean - 2.*np.sqrt(var) + _5pc = mean + - 2.*np.sqrt(var) _95pc = mean + 2.*np.sqrt(var) return mean, _5pc, _95pc diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index 4f571e14..de97824a 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -52,8 +52,8 @@ class probit(likelihood_function): mu = mu.flatten() var = var.flatten() mean = stats.norm.cdf(mu/np.sqrt(1+var)) - p_05 = np.zeros([mu.size]) - p_95 = np.ones([mu.size]) + p_05 = np.zeros(mu.shape)#np.zeros([mu.size]) + p_95 = np.zeros(mu.shape)#np.ones([mu.size]) return mean, p_05, p_95 class Poisson(likelihood_function): diff --git a/GPy/models/GP.py b/GPy/models/GP.py index db00755c..c640e529 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -7,7 +7,7 @@ import pylab as pb from .. import kern from ..core import model from ..util.linalg import pdinv,mdot -from ..util.plot import gpplot,x_frame, Tango +from ..util.plot import gpplot,x_frame1D,x_frame2D, Tango from ..likelihoods import EP class GP(model): @@ -175,7 +175,7 @@ class GP(model): return mean, _5pc, _95pc - def plot_GP(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + def plot_internal(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): """ Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian @@ -200,22 +200,49 @@ class GP(model): if which_data=='all': which_data = slice(None) - Xnew, xmin, xmax = x_frame(self.X, plot_limits=plot_limits) + if self.X.shape[1] == 1: + Xnew, xmin, xmax = x_frame1D(self.X, plot_limits=plot_limits) + m,v = self._raw_predict(Xnew, slices=which_functions) + gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) + pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) + pb.xlim(xmin,xmax) + elif X.shape[1]==2: + resolution = resolution or 50 + Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits=plot_limits) + m,v = self._raw_predict(Xnew, slices=which_functions) + m = m.reshape(resolution,resolution) + pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + pb.xlim(xmin[0],xmax[0]) + pb.ylim(xmin[1],xmax[1]) + else: + raise NotImplementedError, "Cannot define a frame with more than two input dimensions" - m,v = self._raw_predict(Xnew, slices=which_functions) - gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) - pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) - pb.xlim(xmin,xmax) - - def plot_output(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): if which_functions=='all': which_functions = [True]*self.kern.Nparts if which_data=='all': which_data = slice(None) - Xnew, xmin, xmax = x_frame(self.X, plot_limits=plot_limits) - m, lower, upper = self.predict(Xnew, slices=which_functions) - gpplot(Xnew,m, lower, upper) - pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) - ymin,ymax = self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 - pb.xlim(xmin,xmax) - pb.ylim(ymin,ymax) + + if self.X.shape[1] == 1: + Xnew, xmin, xmax = x_frame1D(self.X, plot_limits=plot_limits) + m, lower, upper = self.predict(Xnew, slices=which_functions) + gpplot(Xnew,m, lower, upper) + pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) + ymin,ymax = self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 + pb.xlim(xmin,xmax) + pb.ylim(ymin,ymax) + elif X.shape[1]==2: + resolution = resolution or 50 + Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits=plot_limits) + m,v = self.predict(Xnew, slices=which_functions) + m = m.reshape(resolution,resolution) + pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + pb.xlim(xmin[0],xmax[0]) + pb.ylim(xmin[1],xmax[1]) + else: + raise NotImplementedError, "Cannot define a frame with more than two input dimensions" + + + diff --git a/GPy/util/plot.py b/GPy/util/plot.py index 60e3e488..7b346330 100644 --- a/GPy/util/plot.py +++ b/GPy/util/plot.py @@ -70,10 +70,11 @@ def align_subplots(N,M,xlim=None, ylim=None): else: removeUpperTicks() -def x_frame(X,plot_limits=None,resolution=None): +def x_frame1D(X,plot_limits=None,resolution=None): """ Internal helper function for making plots, returns a set of input values to plot as well as lower and upper limits """ + assert X.shape[1] ==1, "x_frame1D is defined for one-dimensional inputs" if plot_limits is None: xmin,xmax = X.min(0),X.max(0) xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) @@ -82,12 +83,24 @@ def x_frame(X,plot_limits=None,resolution=None): else: raise ValueError, "Bad limits for plotting" - if X.shape[1]==1: - Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - elif X.shape[1]==2: - resolution = resolution or 50 - xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] - Xnew = np.vstack((xx.flatten(),yy.flatten())).T - else: - raise NotImplementedError, "Cannot define a frame with more than two input dimensions" + Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] return Xnew, xmin, xmax + +def x_frame2D(X,plot_limits=None,resolution=None): + """ + Internal helper function for making plots, returns a set of input values to plot as well as lower and upper limits + """ + assert X.shape[1] ==2, "x_frame2D is defined for two-dimensional inputs" + if plot_limits is None: + xmin,xmax = X.min(0),X.max(0) + xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) + elif len(plot_limits)==2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + + resolution = resolution or 50 + xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + Xnew = np.vstack((xx.flatten(),yy.flatten())).T + return Xnew, xx,yy,xmin, xmax + From 976b27b654f991f396d76e88d117da1e1d55bbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Fusi?= Date: Sun, 3 Feb 2013 15:00:53 +0000 Subject: [PATCH 110/197] working on linear kernel --- GPy/examples/BGPLVM_demo.py | 2 +- GPy/kern/kern.py | 38 ++++++++++++++++++------------------- GPy/kern/linear.py | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/GPy/examples/BGPLVM_demo.py b/GPy/examples/BGPLVM_demo.py index a5912462..18a96a47 100644 --- a/GPy/examples/BGPLVM_demo.py +++ b/GPy/examples/BGPLVM_demo.py @@ -17,7 +17,7 @@ K = k.K(X) Y = np.random.multivariate_normal(np.zeros(N),K,D).T # k = GPy.kern.rbf(Q) + GPy.kern.bias(Q) + GPy.kern.white(Q, 0.00001) -k = GPy.kern.linear(Q, ARD = False) + GPy.kern.white(Q, 0.00001) +k = GPy.kern.linear(Q, ARD = True) + GPy.kern.white(Q) m = GPy.models.Bayesian_GPLVM(Y, Q, kernel = k, M=M) m.constrain_positive('(rbf|bias|noise|white|S)') # m.constrain_fixed('S', 1) diff --git a/GPy/kern/kern.py b/GPy/kern/kern.py index 0433d1f4..6bfd224f 100644 --- a/GPy/kern/kern.py +++ b/GPy/kern/kern.py @@ -6,7 +6,7 @@ import numpy as np from ..core.parameterised import parameterised from kernpart import kernpart import itertools -from product_orthogonal import product_orthogonal +from product_orthogonal import product_orthogonal class kern(parameterised): def __init__(self,D,parts=[], input_slices=None): @@ -325,11 +325,11 @@ class kern(parameterised): # MASSIVE TODO: do something smart for white # "crossterms" - psi1_matrices = [np.zeros((mu.shape[0], Z.shape[0])) for p in self.parts] - [p.psi1(Z[s2],mu[s1],S[s1],psi1_target[s1,s2]) for p,s1,s2,psi1_target in zip(self.parts,slices1,slices2, psi1_matrices)] - for a,b in itertools.combinations(psi1_matrices, 2): - tmp = np.multiply(a,b) - target += tmp[:,None,:] + tmp[:, :,None] + # psi1_matrices = [np.zeros((mu.shape[0], Z.shape[0])) for p in self.parts] + # [p.psi1(Z[s2],mu[s1],S[s1],psi1_target[s1,s2]) for p,s1,s2,psi1_target in zip(self.parts,slices1,slices2, psi1_matrices)] + # for a,b in itertools.combinations(psi1_matrices, 2): + # tmp = np.multiply(a,b) + # target += tmp[:,None,:] + tmp[:, :,None] return target @@ -340,21 +340,21 @@ class kern(parameterised): [p.dpsi2_dtheta(partial[s1,s2,s2],Z[s2,i_s],mu[s1,i_s],S[s1,i_s],target[ps]) for p,i_s,s1,s2,ps in zip(self.parts,self.input_slices,slices1,slices2,self.param_slices)] - # "crossterms" - # 1. get all the psi1 statistics - psi1_matrices = [np.zeros((mu.shape[0], Z.shape[0])) for p in self.parts] - [p.psi1(Z[s2],mu[s1],S[s1],psi1_target[s1,s2]) for p,s1,s2,psi1_target in zip(self.parts,slices1,slices2, psi1_matrices)] - partial1 = np.zeros_like(partial1) + # # "crossterms" + # # 1. get all the psi1 statistics + # psi1_matrices = [np.zeros((mu.shape[0], Z.shape[0])) for p in self.parts] + # [p.psi1(Z[s2],mu[s1],S[s1],psi1_target[s1,s2]) for p,s1,s2,psi1_target in zip(self.parts,slices1,slices2, psi1_matrices)] + # partial1 = np.zeros_like(partial1) - # 2. get all the dpsi1/dtheta gradients - psi1_gradients = [np.zeros(self.Nparam) for p in self.parts] - [p.dpsi1_dtheta(partial1[s2,s1],Z[s2,i_s],mu[s1,i_s],S[s1,i_s],psi1g_target[ps]) for p,ps,s1,s2,i_s,psi1g_target in zip(self.parts, self.param_slices,slices1,slices2,self.input_slices,psi1_gradients)] + # # 2. get all the dpsi1/dtheta gradients + # psi1_gradients = [np.zeros(self.Nparam) for p in self.parts] + # [p.dpsi1_dtheta(partial1[s2,s1],Z[s2,i_s],mu[s1,i_s],S[s1,i_s],psi1g_target[ps]) for p,ps,s1,s2,i_s,psi1g_target in zip(self.parts, self.param_slices,slices1,slices2,self.input_slices,psi1_gradients)] - # 3. multiply them somehow - for a,b in itertools.combinations(range(len(psi1_matrices)), 2): - gne = (psi1_gradients[a][None]*psi1_matrices[b].sum(0)[:,None]).sum(0) - - target += (gne[None] + gne[:, None]).sum(0) + # # 3. multiply them somehow + # for a,b in itertools.combinations(range(len(psi1_matrices)), 2): + # gne = (psi1_gradients[a][None]*psi1_matrices[b].sum(0)[:,None]).sum(0) + # target += (gne[None] + gne[:, None]).sum(0) + return target def dpsi2_dZ(self,partial,Z,mu,S,slices1=None,slices2=None): diff --git a/GPy/kern/linear.py b/GPy/kern/linear.py index 52bc1757..7c8ba398 100644 --- a/GPy/kern/linear.py +++ b/GPy/kern/linear.py @@ -30,7 +30,7 @@ class linear(kernpart): if variances is not None: if isinstance(variances, float): variances = np.array([variances]) - + assert variances.shape == (1,) else: variances = np.ones(1) From 3a61c39cb86bbc6d247235516420ea26751cb040 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Mon, 4 Feb 2013 10:22:01 +0000 Subject: [PATCH 111/197] partial derivatives for the new likelihood framework --- GPy/models/sparse_GP.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index f35b3918..d3592d69 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -114,10 +114,23 @@ class sparse_GP(GP): self.dL_dKmm += np.dot(np.dot(self.E*sf2, self.psi2_beta_scaled) - np.dot(self.C, self.psi1VVpsi1), self.Kmmi) + 0.5*self.E # dD #the partial derivative vector for the likelihood - self.partial_for_likelihood = - 0.5 * self.D*self.likelihood.precision + 0.5 * (self.likelihood.Y**2).sum(1)*self.likelihood.precision**2 #dA - self.partial_for_likelihood += 0.5 * self.D * (self.psi0*self.likelihood.precision**2 - (self.psi2*self.Kmmi[None,:,:]*self.likelihood.precision[:,None,None]**2).sum(1).sum(1)/sf2) #dB - #self.partial_for_likelihood += 0.5 * self.D * np.sum(self.Bi*self.A)*self.likelihood.precision #dC - #self.partial_for_likelihood += -np.diag(np.dot((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) , self.psi1VVpsi1 ))*self.likelihood.precision #dD + if self.likelihood.Nparams ==0: + #save computation here. + self.partial_for_likelihood = None + elif self.likelihood.is_heteroscedastic: + raise NotImplementedError, "heteroscedatic derivates not implemented" + #self.partial_for_likelihood = - 0.5 * self.D*self.likelihood.precision + 0.5 * (self.likelihood.Y**2).sum(1)*self.likelihood.precision**2 #dA + #self.partial_for_likelihood += 0.5 * self.D * (self.psi0*self.likelihood.precision**2 - (self.psi2*self.Kmmi[None,:,:]*self.likelihood.precision[:,None,None]**2).sum(1).sum(1)/sf2) #dB + #self.partial_for_likelihood += 0.5 * self.D * np.sum(self.Bi*self.A)*self.likelihood.precision #dC + #self.partial_for_likelihood += -np.diag(np.dot((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) , self.psi1VVpsi1 ))*self.likelihood.precision #dD + else: + #likelihood is not heterscedatic + beta = self.likelihood.precision + dbeta = 0.5 * self.N*self.D/beta - 0.5 * np.sum(np.square(self.likelihood.Y)) + dbeta += - 0.5 * self.D * (self.psi0.sum() - np.trace(self.A)/beta*sf2) + dbeta += - 0.5 * self.D * np.sum(self.Bi*self.A)/beta + dbeta += np.sum((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) * self.psi1VVpsi1 )/beta + self.partial_for_likelihood = -dbeta*self.likelihood.precision def _set_params(self, p): @@ -195,9 +208,9 @@ class sparse_GP(GP): def plot(self, *args, **kwargs): """ - Plot the fitted model: just call the GP_regression plot function and then add inducing inputs + Plot the fitted model: just call the GP plot function and then add inducing inputs """ - GP_regression.plot(self,*args,**kwargs) + GP.plot(self,*args,**kwargs) if self.Q==1: pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) if self.has_uncertain_inputs: From 7a5466068962fd3c57b694c78ffa1030902ec190 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Mon, 4 Feb 2013 10:29:01 +0000 Subject: [PATCH 112/197] fixed bug in my schoolboy mathematics --- GPy/likelihoods/Gaussian.py | 2 +- GPy/models/sparse_GP.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index ff358b2d..630b5d91 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -32,7 +32,7 @@ class Gaussian(likelihood): return ["noise variance"] def _set_params(self,x): - self._variance = x + self._variance = float(x) self.covariance_matrix = np.eye(self.N)*self._variance self.precision = 1./self._variance diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index d3592d69..6ba74e38 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -130,7 +130,7 @@ class sparse_GP(GP): dbeta += - 0.5 * self.D * (self.psi0.sum() - np.trace(self.A)/beta*sf2) dbeta += - 0.5 * self.D * np.sum(self.Bi*self.A)/beta dbeta += np.sum((self.C - 0.5 * mdot(self.C,self.psi2_beta_scaled,self.C) ) * self.psi1VVpsi1 )/beta - self.partial_for_likelihood = -dbeta*self.likelihood.precision + self.partial_for_likelihood = -dbeta*self.likelihood.precision**2 def _set_params(self, p): From 049e60f16ba87ed8db51524d6eb9f40efa640d01 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Mon, 4 Feb 2013 12:01:27 +0000 Subject: [PATCH 113/197] var[:,None] added in full_cov = false, sparse_GP --- GPy/models/GP.py | 2 +- GPy/models/sparse_GP.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index c640e529..2afa4252 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -133,7 +133,7 @@ class GP(model): KiKx = np.dot(self.Ki,Kx) if full_cov: Kxx = self.kern.K(_Xnew, slices1=slices,slices2=slices) - var = Kxx - np.dot(KiKx.T,Kx) #NOTE is the shape of v right? + var = Kxx - np.dot(KiKx.T,Kx) #NOTE this won't work for plotting else: Kxx = self.kern.Kdiag(_Xnew, slices=slices) var = Kxx - np.sum(np.multiply(KiKx,Kx),0) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index 6ba74e38..a90f73cb 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -196,15 +196,14 @@ class sparse_GP(GP): Kx = self.kern.K(self.Z, Xnew) mu = mdot(Kx.T, self.C/self.scale_factor, self.psi1V) - if full_cov: Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) + var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) #NOTE thiswon't work for plotting else: Kxx = self.kern.Kdiag(Xnew) var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.C/self.scale_factor**2, Kx),0) - return mu,var + return mu,var[:,None] def plot(self, *args, **kwargs): """ From 9b69b049337f325adf6762c155b3b135422ada20 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Mon, 4 Feb 2013 12:18:14 +0000 Subject: [PATCH 114/197] proper propagation of variance through the Gaussian likelihood --- GPy/likelihoods/Gaussian.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GPy/likelihoods/Gaussian.py b/GPy/likelihoods/Gaussian.py index 630b5d91..a34b3e6c 100644 --- a/GPy/likelihoods/Gaussian.py +++ b/GPy/likelihoods/Gaussian.py @@ -42,8 +42,8 @@ class Gaussian(likelihood): """ mean = mu*self._std + self._mean true_var = (var + self._variance)*self._std**2 - _5pc = mean + - 2.*np.sqrt(var) - _95pc = mean + 2.*np.sqrt(var) + _5pc = mean + - 2.*np.sqrt(true_var) + _95pc = mean + 2.*np.sqrt(true_var) return mean, _5pc, _95pc def fit_full(self): From dacdaa1b418672dbecf9d70f5f8ae062862ed5ed Mon Sep 17 00:00:00 2001 From: James Hensman Date: Mon, 4 Feb 2013 12:36:08 +0000 Subject: [PATCH 115/197] simplified the checkgrad logic somewhat --- GPy/core/model.py | 110 +++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index f26bf2ee..5e228b15 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -304,54 +304,62 @@ class model(parameterised): return '\n'.join(s) - def checkgrad(self, verbose=False, include_priors=False, step=1e-6, tolerance = 1e-3, return_ratio=False, *args): + def checkgrad(self, verbose=False, include_priors=False, step=1e-6, tolerance = 1e-3): """ Check the gradient of the model by comparing to a numerical estimate. - If the overall gradient fails, invividual components are tested. + If the verbose flag is passed, invividual components are tested (and printed) + + :param verbose: If True, print a "full" checking of each parameter + :type verbose: bool + :param step: The size of the step around which to linearise the objective + :type step: float (defaul 1e-6) + :param tolerance: the tolerance allowed (see note) + :type tolerance: float (default 1e-3) + + Note:- + The gradient is considered correct if the ratio of the analytical + and numerical gradients is within of unity. """ x = self._get_params_transformed().copy() - #choose a random direction to step in: - dx = step*np.sign(np.random.uniform(-1,1,x.size)) + if not verbose: + #just check the global ratio + dx = step*np.sign(np.random.uniform(-1,1,x.size)) - #evaulate around the point x - self._set_params_transformed(x+dx) - f1,g1 = self.log_likelihood() + self.log_prior(), self._log_likelihood_gradients_transformed() - self._set_params_transformed(x-dx) - f2,g2 = self.log_likelihood() + self.log_prior(), self._log_likelihood_gradients_transformed() - self._set_params_transformed(x) - gradient = self._log_likelihood_gradients_transformed() + #evaulate around the point x + self._set_params_transformed(x+dx) + f1,g1 = self.log_likelihood() + self.log_prior(), self._log_likelihood_gradients_transformed() + self._set_params_transformed(x-dx) + f2,g2 = self.log_likelihood() + self.log_prior(), self._log_likelihood_gradients_transformed() + self._set_params_transformed(x) + gradient = self._log_likelihood_gradients_transformed() - numerical_gradient = (f1-f2)/(2*dx) - global_ratio = (f1-f2)/(2*np.dot(dx,gradient)) - if verbose: - print "Gradient ratio = ", global_ratio, '\n' - sys.stdout.flush() + numerical_gradient = (f1-f2)/(2*dx) + global_ratio = (f1-f2)/(2*np.dot(dx,gradient)) - if (np.abs(1.-global_ratio) Date: Mon, 4 Feb 2013 16:15:54 +0000 Subject: [PATCH 116/197] various merge conflicts from the newGP branch --- GPy/examples/BGPLVM_demo.py | 2 +- GPy/models/BGPLVM.py | 4 ++-- GPy/models/GPLVM.py | 26 +++++++++++++------------- GPy/testing/unit_tests.py | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/GPy/examples/BGPLVM_demo.py b/GPy/examples/BGPLVM_demo.py index a5912462..02092dbf 100644 --- a/GPy/examples/BGPLVM_demo.py +++ b/GPy/examples/BGPLVM_demo.py @@ -17,7 +17,7 @@ K = k.K(X) Y = np.random.multivariate_normal(np.zeros(N),K,D).T # k = GPy.kern.rbf(Q) + GPy.kern.bias(Q) + GPy.kern.white(Q, 0.00001) -k = GPy.kern.linear(Q, ARD = False) + GPy.kern.white(Q, 0.00001) +k = GPy.kern.rbf(Q, ARD = False) + GPy.kern.white(Q, 0.00001) m = GPy.models.Bayesian_GPLVM(Y, Q, kernel = k, M=M) m.constrain_positive('(rbf|bias|noise|white|S)') # m.constrain_fixed('S', 1) diff --git a/GPy/models/BGPLVM.py b/GPy/models/BGPLVM.py index ffa7df54..16115025 100644 --- a/GPy/models/BGPLVM.py +++ b/GPy/models/BGPLVM.py @@ -33,7 +33,7 @@ class Bayesian_GPLVM(sparse_GP, GPLVM): kernel = kern.rbf(Q) + kern.white(Q) S = np.ones_like(X) * 1e-2# - sparse_GP.__init__(self, X, Gaussian(Y), X_uncertainty = S, Z=Z,**kwargs) + sparse_GP.__init__(self, X, Gaussian(Y), X_uncertainty=S, Z=Z,**kwargs) def _get_param_names(self): X_names = sum([['X_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[]) @@ -46,7 +46,7 @@ class Bayesian_GPLVM(sparse_GP, GPLVM): The resulting 1-D array has this structure: =============================================================== - | mu | S | Z | beta | theta | + | mu | S | Z | theta | beta | =============================================================== """ diff --git a/GPy/models/GPLVM.py b/GPy/models/GPLVM.py index a8f6a5b1..73762433 100644 --- a/GPy/models/GPLVM.py +++ b/GPy/models/GPLVM.py @@ -8,9 +8,10 @@ import sys, pdb from .. import kern from ..core import model from ..util.linalg import pdinv, PCA -from GP_regression import GP_regression +from GP import GP +from ..likelihoods import Gaussian -class GPLVM(GP_regression): +class GPLVM(GP): """ Gaussian Process Latent Variable Model @@ -22,10 +23,13 @@ class GPLVM(GP_regression): :type init: 'PCA'|'random' """ - def __init__(self, Y, Q, init='PCA', X = None, **kwargs): + def __init__(self, Y, Q, init='PCA', X = None, kernel=None, **kwargs): if X is None: X = self.initialise_latent(init, Q, Y) - GP_regression.__init__(self, X, Y, **kwargs) + if kernel is None: + kernel = kern.rbf(Q) + kern.bias(Q) + likelihood = Gaussian(Y) + GP.__init__(self, X, likelihood, kernel, **kwargs) def initialise_latent(self, init, Q, Y): if init == 'PCA': @@ -34,23 +38,19 @@ class GPLVM(GP_regression): return np.random.randn(Y.shape[0], Q) def _get_param_names(self): - return (sum([['X_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[]) - + self.kern._get_param_names_transformed()) + return sum([['X_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[]) + GP._get_param_names(self) def _get_params(self): - return np.hstack((self.X.flatten(), self.kern._get_params_transformed())) + return np.hstack((self.X.flatten(), GP._get_params(self))) def _set_params(self,x): self.X = x[:self.X.size].reshape(self.N,self.Q).copy() - GP_regression._set_params(self, x[self.X.size:]) + GP._set_params(self, x[self.X.size:]) def _log_likelihood_gradients(self): - dL_dK = self.dL_dK() + dL_dX = 2.*self.kern.dK_dX(self.dL_dK,self.X) - dL_dtheta = self.kern.dK_dtheta(dL_dK,self.X) - dL_dX = 2*self.kern.dK_dX(dL_dK,self.X) - - return np.hstack((dL_dX.flatten(),dL_dtheta)) + return np.hstack((dL_dX.flatten(),GP._log_likelihood_gradients(self))) def plot(self): assert self.Y.shape[1]==2 diff --git a/GPy/testing/unit_tests.py b/GPy/testing/unit_tests.py index 4cc1c4ab..61fb15bb 100644 --- a/GPy/testing/unit_tests.py +++ b/GPy/testing/unit_tests.py @@ -160,8 +160,8 @@ class GradientTests(unittest.TestCase): Y = np.hstack([np.ones(N/2),np.repeat(-1,N/2)])[:,None] kernel = GPy.kern.rbf(1) distribution = GPy.likelihoods.likelihood_functions.probit() - likelihood = GPy.likelihoods.EP(Y,distribution) - m = GPy.models.GP(X,kernel,likelihood=likelihood) + likelihood = GPy.likelihoods.EP(Y, distribution) + m = GPy.models.GP(X, likelihood, kernel) m.ensure_default_constraints() self.assertTrue(m.EPEM) From 4dce1a428fa0c699bd792a42ec3f2b006de00e79 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Mon, 4 Feb 2013 16:42:44 +0000 Subject: [PATCH 117/197] tidying up after wide reaching changes: removed sparse_GP_old.py --- GPy/models/sparse_GP_old.py | 258 ------------------------------------ 1 file changed, 258 deletions(-) delete mode 100644 GPy/models/sparse_GP_old.py diff --git a/GPy/models/sparse_GP_old.py b/GPy/models/sparse_GP_old.py deleted file mode 100644 index 7b043209..00000000 --- a/GPy/models/sparse_GP_old.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - -import numpy as np -import pylab as pb -from ..util.linalg import mdot, jitchol, chol_inv, pdinv -from ..util.plot import gpplot -from .. import kern -from GP import GP - - -#Still TODO: -# make use of slices properly (kernel can now do this) -# enable heteroscedatic noise (kernel will need to compute psi2 as a (NxMxM) array) - -class sparse_GP(GP): - """ - Variational sparse GP model (Regression) - - :param X: inputs - :type X: np.ndarray (N x Q) - :param Y: observed data - :type Y: np.ndarray of observations (N x D) - :param kernel : the kernel/covariance function. See link kernels - :type kernel: a GPy kernel - :param Z: inducing inputs (optional, see note) - :type Z: np.ndarray (M x Q) | None - :param X_uncertainty: The uncertainty in the measurements of X (Gaussian variance) - :type X_uncertainty: np.ndarray (N x Q) | None - :param Zslices: slices for the inducing inputs (see slicing TODO: link) - :param M : Number of inducing points (optional, default 10. Ignored if Z is not None) - :type M: int - :param beta: noise precision. TODO> ignore beta if doing EP - :type beta: float - :param normalize_(X|Y) : whether to normalize the data before computing (predictions will be in original scales) - :type normalize_(X|Y): bool - """ - - def __init__(self,X,Y=None,kernel=None,X_uncertainty=None,beta=100.,Z=None,Zslices=None,M=10,normalize_X=False,normalize_Y=False,likelihood=None,method_ep='DTC',epsilon_ep=1e-3,power_ep=[1.,1.]): - - if Z is None: - self.Z = np.random.permutation(X.copy())[:M] - self.M = M - else: - assert Z.shape[1]==X.shape[1] - self.Z = Z - self.M = Z.shape[0] - if X_uncertainty is None: - self.has_uncertain_inputs=False - else: - assert X_uncertainty.shape==X.shape - self.has_uncertain_inputs=True - self.X_uncertainty = X_uncertainty - - GP.__init__(self, X=X, Y=Y, kernel=kernel, normalize_X=normalize_X, normalize_Y=normalize_Y,likelihood=likelihood,epsilon_ep=epsilon_ep,power_ep=power_ep) - - #normalise X uncertainty also - if self.has_uncertain_inputs: - self.X_uncertainty /= np.square(self._Xstd) - - if not self.EP: - self.trYYT = np.sum(np.square(self.Y)) - else: - self.method_ep = method_ep - - #normalise X uncertainty also - if self.has_uncertain_inputs: - self.X_uncertainty /= np.square(self._Xstd) - - def _set_params(self, p): - self.Z = p[:self.M*self.Q].reshape(self.M, self.Q) - if not self.EP: - self.beta = p[self.M*self.Q] - self.kern._set_params(p[self.Z.size + 1:]) - else: - self.kern._set_params(p[self.Z.size:]) - if self.Y is None: - self.Y = np.ones([self.N,1]) - self._compute_kernel_matrices() - self._computations() - - def _get_params(self): - if not self.EP: - return np.hstack([self.Z.flatten(),self.beta,self.kern._get_params_transformed()]) - else: - return np.hstack([self.Z.flatten(),self.kern._get_params_transformed()]) - - def _get_param_names(self): - if not self.EP: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + ['noise_precision']+self.kern._get_param_names_transformed() - else: - return sum([['iip_%i_%i'%(i,j) for i in range(self.Z.shape[0])] for j in range(self.Z.shape[1])],[]) + self.kern._get_param_names_transformed() - - - def _compute_kernel_matrices(self): - # kernel computations, using BGPLVM notation - #TODO: slices for psi statistics (easy enough) - - self.Kmm = self.kern.K(self.Z) - if self.has_uncertain_inputs: - if not self.EP: - self.psi0 = self.kern.psi0(self.Z,self.X, self.X_uncertainty)#.sum() NOTE psi0 is now a vector - self.psi1 = self.kern.psi1(self.Z,self.X, self.X_uncertainty).T - self.psi2 = self.kern.psi2(self.Z,self.X, self.X_uncertainty) - #self.psi2_beta_scaled = ? - else: - raise NotImplementedError, "uncertain_inputs not yet supported for EP" - else: - self.psi0 = self.kern.Kdiag(self.X,slices=self.Xslices)#.sum() - self.psi1 = self.kern.K(self.Z,self.X) - self.psi2 = np.dot(self.psi1,self.psi1.T) - self.psi2_beta_scaled = np.dot(self.psi1,self.beta*self.psi1.T) - - def _computations(self): - # TODO find routine to multiply triangular matrices - self.V = self.beta*self.Y - self.psi1V = np.dot(self.psi1, self.V) - self.psi1VVpsi1 = np.dot(self.psi1V, self.psi1V.T) - self.Kmmi, self.Lm, self.Lmi, self.Kmm_logdet = pdinv(self.Kmm) - self.A = mdot(self.Lmi, self.psi2_beta_scaled, self.Lmi.T) - self.B = np.eye(self.M) + self.A - self.Bi, self.LB, self.LBi, self.B_logdet = pdinv(self.B) - self.LLambdai = np.dot(self.LBi, self.Lmi) - self.LBL_inv = mdot(self.Lmi.T, self.Bi, self.Lmi) - self.C = mdot(self.LLambdai, self.psi1V) - self.G = mdot(self.LBL_inv, self.psi1VVpsi1, self.LBL_inv.T) - self.trace_K_beta_scaled = (self.psi0*self.beta).sum() - np.trace(self.A) - if not self.EP: - self.trace_K = self.psi0.sum() - np.trace(self.A)/self.beta - - # Compute dL_dpsi - self.dL_dpsi1 = mdot(self.LLambdai.T,self.C,self.V.T) - if not self.EP: - self.dL_dpsi0 = - 0.5 * self.D * self.beta * np.ones(self.N) - if self.has_uncertain_inputs: - self.dL_dpsi2 = - 0.5 * self.beta * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - else: - self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - else: - self.dL_dpsi0 = - 0.5 * self.D * self.beta.flatten() - if not self.has_uncertain_inputs: - self.dL_dpsi2_ = - 0.5 * (self.D*(self.LBL_inv - self.Kmmi) + self.G) - - # Compute dL_dKmm - self.dL_dKmm = -0.5 * self.D * mdot(self.Lmi.T, self.A, self.Lmi) # dB - self.dL_dKmm += -0.5 * self.D * (- self.LBL_inv - 2.*mdot(self.LBL_inv, self.psi2_beta_scaled, self.Kmmi) + self.Kmmi) # dC - self.dL_dKmm += np.dot(np.dot(self.G,self.psi2_beta_scaled) - np.dot(self.LBL_inv, self.psi1VVpsi1), self.Kmmi) + 0.5*self.G # dE - - def approximate_likelihood(self): - assert not isinstance(self.likelihood, gaussian), "EP is only available for non-gaussian likelihoods" - if self.method_ep == 'DTC': - self.ep_approx = DTC(self.Kmm,self.likelihood,self.psi1,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - elif self.method_ep == 'FITC': - self.ep_approx = FITC(self.Kmm,self.likelihood,self.psi1,self.psi0,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - else: - self.ep_approx = Full(self.X,self.likelihood,self.kernel,inducing=None,epsilon=self.epsilon_ep,power_ep=[self.eta,self.delta]) - self.beta, self.Y, self.Z_ep = self.ep_approx.fit_EP() - self.trbetaYYT = np.sum(np.square(self.Y)*self.beta) - self._computations() - - def log_likelihood(self): - """ - Compute the (lower bound on the) log marginal likelihood - """ - if not self.EP: - A = -0.5*self.N*self.D*(np.log(2.*np.pi) - np.log(self.beta)) - D = -0.5*self.beta*self.trYYT - else: - A = -0.5*self.D*(self.N*np.log(2.*np.pi) - np.sum(np.log(self.beta))) - D = -0.5*self.trbetaYYT - B = -0.5*self.D*self.trace_K_beta_scaled - C = -0.5*self.D * self.B_logdet - E = +0.5*np.sum(self.psi1VVpsi1 * self.LBL_inv) - return A+B+C+D+E - - def dL_dbeta(self): - """ - Compute the gradient of the log likelihood wrt beta. - """ - #TODO: suport heteroscedatic noise - dA_dbeta = 0.5 * self.N*self.D/self.beta - dB_dbeta = - 0.5 * self.D * self.trace_K - dC_dbeta = - 0.5 * self.D * np.sum(self.Bi*self.A)/self.beta - dD_dbeta = - 0.5 * self.trYYT - tmp = mdot(self.LBi.T, self.LLambdai, self.psi1V) - dE_dbeta = (np.sum(np.square(self.C)) - 0.5 * np.sum(self.A * np.dot(tmp, tmp.T)))/self.beta - - return np.squeeze(dA_dbeta + dB_dbeta + dC_dbeta + dD_dbeta + dE_dbeta) - - def dL_dtheta(self): - """ - Compute and return the derivative of the log marginal likelihood wrt the parameters of the kernel - """ - dL_dtheta = self.kern.dK_dtheta(self.dL_dKmm,self.Z) - if self.has_uncertain_inputs: - dL_dtheta += self.kern.dpsi0_dtheta(self.dL_dpsi0, self.Z,self.X,self.X_uncertainty) - dL_dtheta += self.kern.dpsi1_dtheta(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) - dL_dtheta += self.kern.dpsi2_dtheta(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) # for multiple_beta, dL_dpsi2 will be a different shape - else: - #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1) #dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) - dL_dtheta += self.kern.dK_dtheta(dL_dpsi1,self.Z,self.X) - dL_dtheta += self.kern.dKdiag_dtheta(self.dL_dpsi0, self.X) - - return dL_dtheta - - def dL_dZ(self): - """ - The derivative of the bound wrt the inducing inputs Z - """ - dL_dZ = 2.*self.kern.dK_dX(self.dL_dKmm,self.Z,)#factor of two becase of vertical and horizontal 'stripes' in dKmm_dZ - if self.has_uncertain_inputs: - dL_dZ += self.kern.dpsi1_dZ(self.dL_dpsi1.T,self.Z,self.X, self.X_uncertainty) - dL_dZ += self.kern.dpsi2_dZ(self.dL_dpsi2,self.Z,self.X, self.X_uncertainty) - else: - #re-cast computations in psi2 back to psi1: - dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2_,self.beta.T*self.psi1)#dL_dpsi1 = self.dL_dpsi1 + 2.*np.dot(self.dL_dpsi2,self.psi1) - dL_dZ += self.kern.dK_dX(dL_dpsi1,self.Z,self.X) - return dL_dZ - - def _log_likelihood_gradients(self): - if not self.EP: - return np.hstack([self.dL_dZ().flatten(), self.dL_dbeta(), self.dL_dtheta()]) - else: - return np.hstack([self.dL_dZ().flatten(), self.dL_dtheta()]) - - def _raw_predict(self, Xnew, slices, full_cov=False): - """Internal helper function for making predictions, does not account for normalisation""" - Kx = self.kern.K(self.Z, Xnew) - mu = mdot(Kx.T, self.LBL_inv, self.psi1V) - phi = None - if full_cov: - Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.LBL_inv), Kx) - if not self.EP: - var += np.eye(Xnew.shape[0])/self.beta - else: - raise NotImplementedError, "full_cov = True not implemented for EP" - else: - Kxx = self.kern.Kdiag(Xnew) - var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.LBL_inv, Kx),0) - if not self.EP: - var += 1./self.beta - else: - phi = self.likelihood.predictive_mean(mu,var) - return mu,var,phi - - def plot(self, *args, **kwargs): - """ - Plot the fitted model: just call the GP_regression plot function and then add inducing inputs - """ - GP.plot(self,*args,**kwargs) - if self.Q==1: - pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) - if self.has_uncertain_inputs: - pb.errorbar(self.X[:,0], pb.ylim()[0]+np.zeros(self.N), xerr=2*np.sqrt(self.X_uncertainty.flatten())) - if self.Q==2: - pb.plot(self.Z[:,0],self.Z[:,1],'wo') From 5b19d8609ab43fb88f1e690f49542249ffeeb338 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 5 Feb 2013 12:27:12 +0000 Subject: [PATCH 118/197] Many modifications in GP plots to make it work --- GPy/kern/kern.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++- GPy/models/GP.py | 27 +++++++++++++------------- GPy/util/plot.py | 3 +-- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/GPy/kern/kern.py b/GPy/kern/kern.py index 89def0e5..a00a20e5 100644 --- a/GPy/kern/kern.py +++ b/GPy/kern/kern.py @@ -3,10 +3,11 @@ import numpy as np +import pylab as pb from ..core.parameterised import parameterised from kernpart import kernpart import itertools -from product_orthogonal import product_orthogonal +from product_orthogonal import product_orthogonal class kern(parameterised): def __init__(self,D,parts=[], input_slices=None): @@ -372,3 +373,50 @@ class kern(parameterised): #TODO: there are some extra terms to compute here! return target_mu, target_S + + def plot(self, x = None, plot_limits=None,which_functions='all',resolution=None): + if which_functions=='all': + which_functions = [True]*self.Nparts + if self.D == 1: + if x is None: + x = np.zeros((1,1)) + else: + assert x.shape == (1,1), "The shape fo the fixed variable x is not (1,D)" + + if plot_limits == None: + xmin, xmax = (x-5).flatten(), (x+5).flatten() + elif len(plot_limits) == 2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + + Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] + Kx = self.K(Xnew,x) + pb.plot(Xnew,Kx) + pb.xlim(xmin,xmax) + pb.ylim(Kx.min() - (Kx.max()-Kx.min())*0.15,Kx.max() + (Kx.max()-Kx.min())*0.15) + + elif self.D == 2: + if x is None: + x = np.zeros((1,2)) + else: + assert x.shape == (1,2), "The shape fo the fixed variable x is not (1,D)" + + if plot_limits == None: + xmin, xmax = (x-5).flatten(), (x+5).flatten() + elif len(plot_limits) == 2: + xmin, xmax = plot_limits + else: + raise ValueError, "Bad limits for plotting" + + resolution = resolution or 50 + xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + Xnew = np.vstack((xx.flatten(),yy.flatten())).T + Kx = self.K(Xnew,x) + Kx = Kx.reshape(resolution,resolution) + pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + pb.xlim(xmin[0],xmax[0]) + pb.ylim(xmin[1],xmax[1]) + else: + raise NotImplementedError, "Cannot plot a kernel with more than two input dimensions" diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 2afa4252..b482ab89 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -206,13 +206,13 @@ class GP(model): gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) pb.xlim(xmin,xmax) - elif X.shape[1]==2: + elif self.X.shape[1]==2: resolution = resolution or 50 - Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits=plot_limits) + Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits,resolution) m,v = self._raw_predict(Xnew, slices=which_functions) - m = m.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + m = m.reshape(resolution,resolution).T + pb.contour(xx,yy,m,vmin=m.min(),vmax=m.max(),cmap=pb.cm.jet) + pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=m.min(), vmax=m.max()) pb.xlim(xmin[0],xmax[0]) pb.ylim(xmin[1],xmax[1]) else: @@ -232,17 +232,16 @@ class GP(model): ymin,ymax = self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) - elif X.shape[1]==2: + elif self.X.shape[1]==2: resolution = resolution or 50 - Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits=plot_limits) - m,v = self.predict(Xnew, slices=which_functions) - m = m.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + Xnew, xx, yy, xmin, xmax = x_frame2D(self.X, plot_limits,resolution) + x, y = np.linspace(xmin[0],xmax[0],resolution), np.linspace(xmin[1],xmax[1],resolution) + m,lower,upper = self.predict(Xnew, slices=which_functions) + m = m.reshape(resolution,resolution).T + pb.contour(x,y,m,vmin=m.min(),vmax=m.max(),cmap=pb.cm.jet) + Yf = self.likelihood.Y.flatten() + pb.scatter(self.X[:,0], self.X[:,1], 40, Yf, cmap=pb.cm.jet,vmin=m.min(),vmax=m.max(), linewidth=0.) pb.xlim(xmin[0],xmax[0]) pb.ylim(xmin[1],xmax[1]) else: raise NotImplementedError, "Cannot define a frame with more than two input dimensions" - - - diff --git a/GPy/util/plot.py b/GPy/util/plot.py index 7b346330..8e71764d 100644 --- a/GPy/util/plot.py +++ b/GPy/util/plot.py @@ -102,5 +102,4 @@ def x_frame2D(X,plot_limits=None,resolution=None): resolution = resolution or 50 xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] Xnew = np.vstack((xx.flatten(),yy.flatten())).T - return Xnew, xx,yy,xmin, xmax - + return Xnew, xx, yy, xmin, xmax From 642b1449e1b3f265641363bcb72a940948658a5a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 5 Feb 2013 14:23:51 +0000 Subject: [PATCH 119/197] Small fixes to ploting --- GPy/models/GP.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index b482ab89..190f770d 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -153,7 +153,8 @@ class GP(model): :param full_cov: whether to return the folll covariance matrix, or just the diagonal :type full_cov: bool :rtype: posterior mean, a Numpy array, Nnew x self.D - :rtype: posterior variance, a Numpy array, Nnew x Nnew x (self.D) + :rtype: posterior variance, a Numpy array, Nnew x 1 if full_cov=False, Nnew x Nnew otherwise + :rtype: lower and upper boundaries of the 95% confidence intervals, Numpy arrays, Nnew x self.D .. Note:: "slices" specifies how the the points X_new co-vary wich the training points. @@ -167,12 +168,12 @@ class GP(model): """ #normalise X values Xnew = (Xnew.copy() - self._Xmean) / self._Xstd - mu, var = self._raw_predict(Xnew, slices, full_cov=full_cov) + mu, var = self._raw_predict(Xnew, slices, full_cov) #now push through likelihood TODO mean, _5pc, _95pc = self.likelihood.predictive_values(mu, var) - return mean, _5pc, _95pc + return mean, var, _5pc, _95pc def plot_internal(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): @@ -206,9 +207,9 @@ class GP(model): gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) pb.xlim(xmin,xmax) - elif self.X.shape[1]==2: + elif self.X.shape[1] == 2: resolution = resolution or 50 - Xnew, xmin, xmax,xx,yy = x_frame2D(self.X, plot_limits,resolution) + Xnew, xmin, xmax, xx, yy = x_frame2D(self.X, plot_limits,resolution) m,v = self._raw_predict(Xnew, slices=which_functions) m = m.reshape(resolution,resolution).T pb.contour(xx,yy,m,vmin=m.min(),vmax=m.max(),cmap=pb.cm.jet) @@ -226,17 +227,18 @@ class GP(model): if self.X.shape[1] == 1: Xnew, xmin, xmax = x_frame1D(self.X, plot_limits=plot_limits) - m, lower, upper = self.predict(Xnew, slices=which_functions) + m, var, lower, upper = self.predict(Xnew, slices=which_functions) gpplot(Xnew,m, lower, upper) pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) - ymin,ymax = self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 + ymin,ymax = lower.min(),upper.max() #self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) + elif self.X.shape[1]==2: resolution = resolution or 50 Xnew, xx, yy, xmin, xmax = x_frame2D(self.X, plot_limits,resolution) x, y = np.linspace(xmin[0],xmax[0],resolution), np.linspace(xmin[1],xmax[1],resolution) - m,lower,upper = self.predict(Xnew, slices=which_functions) + m, var, lower, upper = self.predict(Xnew, slices=which_functions) m = m.reshape(resolution,resolution).T pb.contour(x,y,m,vmin=m.min(),vmax=m.max(),cmap=pb.cm.jet) Yf = self.likelihood.Y.flatten() From a1568ca1c8b79b25f3edb8c6245c0dd0bcae88dd Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 5 Feb 2013 18:57:30 +0000 Subject: [PATCH 120/197] Few more fix to the plotings and predictions --- GPy/models/GP.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 190f770d..4eef749e 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -137,7 +137,8 @@ class GP(model): else: Kxx = self.kern.Kdiag(_Xnew, slices=slices) var = Kxx - np.sum(np.multiply(KiKx,Kx),0) - return mu, var[:,None] + var = var[:,None] + return mu, var def predict(self,Xnew, slices=None, full_cov=False): @@ -171,12 +172,12 @@ class GP(model): mu, var = self._raw_predict(Xnew, slices, full_cov) #now push through likelihood TODO - mean, _5pc, _95pc = self.likelihood.predictive_values(mu, var) + mean, _025pm, _975pm = self.likelihood.predictive_values(mu, var) - return mean, var, _5pc, _95pc + return mean, var, _025pm, _975pm - def plot_internal(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + def plot_f(self, samples=0, plot_limits=None, which_data='all', which_functions='all', resolution=None, full_cov=False): """ Plot the GP's view of the world, where the data is normalised and the likelihood is Gaussian @@ -203,8 +204,17 @@ class GP(model): if self.X.shape[1] == 1: Xnew, xmin, xmax = x_frame1D(self.X, plot_limits=plot_limits) - m,v = self._raw_predict(Xnew, slices=which_functions) - gpplot(Xnew,m,m-np.sqrt(v),m+np.sqrt(v)) + if samples == 0: + m,v = self._raw_predict(Xnew, slices=which_functions) + gpplot(Xnew,m,m-2*np.sqrt(v),m+2*np.sqrt(v)) + pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) + else: + m,v = self._raw_predict(Xnew, slices=which_functions,full_cov=True) + Ysim = np.random.multivariate_normal(m.flatten(),v,samples) + gpplot(Xnew,m,m-2*np.sqrt(np.diag(v)[:,None]),m+2*np.sqrt(np.diag(v))[:,None]) + for i in range(samples): + pb.plot(Xnew,Ysim[i,:],Tango.coloursHex['darkBlue'],linewidth=0.25) + pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) pb.xlim(xmin,xmax) elif self.X.shape[1] == 2: @@ -220,6 +230,7 @@ class GP(model): raise NotImplementedError, "Cannot define a frame with more than two input dimensions" def plot(self,samples=0,plot_limits=None,which_data='all',which_functions='all',resolution=None,full_cov=False): + # TODO include samples if which_functions=='all': which_functions = [True]*self.kern.Nparts if which_data=='all': @@ -230,10 +241,10 @@ class GP(model): m, var, lower, upper = self.predict(Xnew, slices=which_functions) gpplot(Xnew,m, lower, upper) pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) - ymin,ymax = lower.min(),upper.max() #self.likelihood.data.min()*1.2,self.likelihood.data.max()*1.2 + ymin,ymax = lower.min(),upper.max() pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) - + elif self.X.shape[1]==2: resolution = resolution or 50 Xnew, xx, yy, xmin, xmax = x_frame2D(self.X, plot_limits,resolution) From b0f6495ed4400fb5e98ad3046c7b3fe3f036bf08 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 6 Feb 2013 09:52:54 +0000 Subject: [PATCH 121/197] Fixed bug in the product of kernels with tied parameters --- GPy/core/parameterised.py | 7 +++++-- GPy/kern/kern.py | 2 ++ GPy/models/GP.py | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/GPy/core/parameterised.py b/GPy/core/parameterised.py index 6e5493ad..ab656f52 100644 --- a/GPy/core/parameterised.py +++ b/GPy/core/parameterised.py @@ -102,6 +102,11 @@ class parameterised(object): else: return expr + def Nparam_transformed(self): + ties = 0 + for ar in self.tied_indices: + ties += ar.size - 1 + return self.Nparam - len(self.constrained_fixed_indices) - ties def constrain_positive(self, which): """ @@ -149,8 +154,6 @@ class parameterised(object): - - def constrain_negative(self,which): """ Set negative constraints. diff --git a/GPy/kern/kern.py b/GPy/kern/kern.py index a00a20e5..5e9273ae 100644 --- a/GPy/kern/kern.py +++ b/GPy/kern/kern.py @@ -236,6 +236,8 @@ class kern(parameterised): X2 = X target = np.zeros(self.Nparam) [p.dK_dtheta(partial[s1,s2],X[s1,i_s],X2[s2,i_s],target[ps]) for p,i_s,ps,s1,s2 in zip(self.parts, self.input_slices, self.param_slices, slices1, slices2)] + + #TODO: transform the gradients here! return target def dK_dX(self,partial,X,X2=None,slices1=None,slices2=None): diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 4eef749e..f8bd27bf 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -62,7 +62,9 @@ class GP(model): def _set_params(self,p): self.kern._set_params_transformed(p[:self.kern.Nparam]) - self.likelihood._set_params(p[self.kern.Nparam:]) + #self.likelihood._set_params(p[self.kern.Nparam:]) # test by Nicolas + self.likelihood._set_params(p[self.kern.Nparam_transformed():]) # test by Nicolas + self.K = self.kern.K(self.X,slices1=self.Xslices) self.K += self.likelihood.covariance_matrix From 8fd79f6eee2212aaac02f7afd351df7ab5066625 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 6 Feb 2013 15:20:04 +0000 Subject: [PATCH 122/197] Added new plotting function for kernels --- GPy/kern/kern.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/GPy/kern/kern.py b/GPy/kern/kern.py index 5e9273ae..be382d11 100644 --- a/GPy/kern/kern.py +++ b/GPy/kern/kern.py @@ -383,7 +383,9 @@ class kern(parameterised): if x is None: x = np.zeros((1,1)) else: - assert x.shape == (1,1), "The shape fo the fixed variable x is not (1,D)" + x = np.asarray(x) + assert x.size == 1, "The size of the fixed variable x is not 1" + x = x.reshape((1,1)) if plot_limits == None: xmin, xmax = (x-5).flatten(), (x+5).flatten() @@ -392,17 +394,20 @@ class kern(parameterised): else: raise ValueError, "Bad limits for plotting" - Xnew = np.linspace(xmin,xmax,resolution or 200)[:,None] - Kx = self.K(Xnew,x) + Xnew = np.linspace(xmin,xmax,resolution or 201)[:,None] + Kx = self.K(Xnew,x,slices2=which_functions) pb.plot(Xnew,Kx) pb.xlim(xmin,xmax) - pb.ylim(Kx.min() - (Kx.max()-Kx.min())*0.15,Kx.max() + (Kx.max()-Kx.min())*0.15) + pb.xlabel("x") + pb.ylabel("k(x,%0.1f)" %x) elif self.D == 2: if x is None: x = np.zeros((1,2)) else: - assert x.shape == (1,2), "The shape fo the fixed variable x is not (1,D)" + x = np.asarray(x) + assert x.size == 2, "The size of the fixed variable x is not 2" + x = x.reshape((1,2)) if plot_limits == None: xmin, xmax = (x-5).flatten(), (x+5).flatten() @@ -411,14 +416,18 @@ class kern(parameterised): else: raise ValueError, "Bad limits for plotting" - resolution = resolution or 50 + resolution = resolution or 51 xx,yy = np.mgrid[xmin[0]:xmax[0]:1j*resolution,xmin[1]:xmax[1]:1j*resolution] + xg = np.linspace(xmin[0],xmax[0],resolution) + yg = np.linspace(xmin[1],xmax[1],resolution) Xnew = np.vstack((xx.flatten(),yy.flatten())).T - Kx = self.K(Xnew,x) - Kx = Kx.reshape(resolution,resolution) - pb.contour(xx,yy,zz,vmin=zz.min(),vmax=zz.max(),cmap=pb.cm.jet) - pb.scatter(Xorig[:,0],Xorig[:,1],40,Yorig,linewidth=0,cmap=pb.cm.jet,vmin=zz.min(),vmax=zz.max()) + Kx = self.K(Xnew,x,slices2=which_functions) + Kx = Kx.reshape(resolution,resolution).T + pb.contour(xg,yg,Kx,vmin=Kx.min(),vmax=Kx.max(),cmap=pb.cm.jet) pb.xlim(xmin[0],xmax[0]) pb.ylim(xmin[1],xmax[1]) + pb.xlabel("x1") + pb.ylabel("x2") + pb.title("k(x1,x2 ; %0.1f,%0.1f)" %(x[0,0],x[0,1]) ) else: raise NotImplementedError, "Cannot plot a kernel with more than two input dimensions" From 02dc5c7b482c23788ca70373c9a31d06e7a3c2bb Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Thu, 7 Feb 2013 11:35:29 +0000 Subject: [PATCH 123/197] Example is working --- GPy/examples/poisson.py | 63 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/GPy/examples/poisson.py b/GPy/examples/poisson.py index 934637f1..ce68e921 100644 --- a/GPy/examples/poisson.py +++ b/GPy/examples/poisson.py @@ -3,46 +3,45 @@ """ -Simple Gaussian Processes classification +Gaussian Processes + Expectation Propagation - Poisson Likelihood """ import pylab as pb import numpy as np import GPy -pb.ion() -pb.close('all') default_seed=10000 -model_type='Full' -inducing=4 -seed=default_seed -"""Simple 1D classification example. -:param model_type: type of model to fit ['Full', 'FITC', 'DTC']. -:param seed : seed value for data generation (default is 4). -:type seed: int -:param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). -:type inducing: int -""" +def toy_1d(seed=default_seed): + """ + Simple 1D classification example + :param seed : seed value for data generation (default is 4). + :type seed: int + """ -X = np.arange(0,100,5)[:,None] -F = np.round(np.sin(X/18.) + .1*X) + np.arange(5,25)[:,None] -E = np.random.randint(-5,5,20)[:,None] -Y = F + E -pb.figure() -likelihood = GPy.inference.likelihoods.poisson(Y,scale=1.) + X = np.arange(0,100,5)[:,None] + F = np.round(np.sin(X/18.) + .1*X) + np.arange(5,25)[:,None] + E = np.random.randint(-5,5,20)[:,None] + Y = F + E -m = GPy.models.GP(X,likelihood=likelihood) -#m = GPy.models.GP(X,Y=likelihood.Y) + kernel = GPy.kern.rbf(1) + distribution = GPy.likelihoods.likelihood_functions.Poisson() + likelihood = GPy.likelihoods.EP(Y,distribution) -m.constrain_positive('var') -m.constrain_positive('len') -m.tie_param('lengthscale') -if not isinstance(m.likelihood,GPy.inference.likelihoods.gaussian): - m.approximate_likelihood() -print m.checkgrad() -# Optimize and plot -m.optimize() -#m.em(plot_all=False) # EM algorithm -m.plot(samples=4) + m = GPy.models.GP(X,likelihood,kernel) + m.ensure_default_constraints() -print(m) + # Approximate likelihood + m.update_likelihood_approximation() + + # Optimize and plot + m.optimize() + #m.EPEM FIXME + print m + + # Plot + pb.subplot(211) + m.plot_f() #GP plot + pb.subplot(212) + m.plot() #Output plot + + return m From cf3e5220697230b780a2a1cdcda9b81aaa002823 Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Thu, 7 Feb 2013 11:36:22 +0000 Subject: [PATCH 124/197] Change in plot() y-limits --- GPy/models/GP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index f8bd27bf..403f1597 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -243,7 +243,7 @@ class GP(model): m, var, lower, upper = self.predict(Xnew, slices=which_functions) gpplot(Xnew,m, lower, upper) pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) - ymin,ymax = lower.min(),upper.max() + ymin,ymax = self.likelihood.data.min() -.1*(upper.max()-lower.min()), self.likelihood.data.max()+.1*(upper.max()-lower.min()) pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) From 4563a5f8a68345170e33e7b08b762351106a314c Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Thu, 7 Feb 2013 11:36:45 +0000 Subject: [PATCH 125/197] Probit likelihood modified for plotting. --- GPy/examples/classification.py | 23 ++++++++++++++------ GPy/likelihoods/likelihood_functions.py | 29 +++++++++++++------------ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/GPy/examples/classification.py b/GPy/examples/classification.py index c25ea124..592299d8 100644 --- a/GPy/examples/classification.py +++ b/GPy/examples/classification.py @@ -20,11 +20,19 @@ def crescent_data(model_type='Full', inducing=10, seed=default_seed): #FIXME :param inducing : number of inducing variables (only used for 'FITC' or 'DTC'). :type inducing: int """ + data = GPy.util.datasets.crescent_data(seed=seed) - likelihood = GPy.inference.likelihoods.probit(data['Y']) + + # Kernel object + kernel = GPy.kern.rbf(data['X'].shape[1]) + + # Likelihood object + distribution = GPy.likelihoods.likelihood_functions.probit() + likelihood = GPy.likelihoods.EP(data['Y'],distribution) + if model_type=='Full': - m = GPy.models.GP_EP(data['X'],likelihood) + m = GPy.models.GP(data['X'],likelihood,kernel) else: # create sparse GP EP model m = GPy.models.sparse_GP_EP(data['X'],likelihood=likelihood,inducing=inducing,ep_proxy=model_type) @@ -33,7 +41,7 @@ def crescent_data(model_type='Full', inducing=10, seed=default_seed): #FIXME print(m) # optimize - m.em() + m.optimize() print(m) # plot @@ -53,7 +61,7 @@ def oil(): likelihood = GPy.likelihoods.EP(data['Y'][:, 0:1],distribution) # Create GP model - m = GPy.models.GP(data['X'],kernel,likelihood=likelihood) + m = GPy.models.GP(data['X'],likelihood=likelihood,kernel=kernel) # Contrain all parameters to be positive m.constrain_positive('') @@ -71,17 +79,18 @@ def toy_linear_1d_classification(seed=default_seed): Simple 1D classification example :param seed : seed value for data generation (default is 4). :type seed: int - :type inducing: int """ data = GPy.util.datasets.toy_linear_1d_classification(seed=seed) + Y = data['Y'][:, 0:1] + Y[Y == -1] = 0 # Kernel object kernel = GPy.kern.rbf(1) # Likelihood object distribution = GPy.likelihoods.likelihood_functions.probit() - likelihood = GPy.likelihoods.EP(data['Y'][:, 0:1],distribution) + likelihood = GPy.likelihoods.EP(Y,distribution) # Model definition m = GPy.models.GP(data['X'],likelihood=likelihood,kernel=kernel) @@ -98,7 +107,7 @@ def toy_linear_1d_classification(seed=default_seed): # Plot pb.subplot(211) - m.plot_internal() + m.plot_f() pb.subplot(212) m.plot() print(m) diff --git a/GPy/likelihoods/likelihood_functions.py b/GPy/likelihoods/likelihood_functions.py index de97824a..23881899 100644 --- a/GPy/likelihoods/likelihood_functions.py +++ b/GPy/likelihoods/likelihood_functions.py @@ -38,6 +38,7 @@ class probit(likelihood_function): :param v_i: mean/variance of the cavity distribution (float) """ # TODO: some version of assert np.sum(np.abs(Y)-1) == 0, "Output values must be either -1 or 1" + if data_i == 0: data_i = -1 #NOTE Binary classification works better classes {-1,1}, 1D-plotting works better with classes {0,1}. z = data_i*v_i/np.sqrt(tau_i**2 + tau_i) Z_hat = stats.norm.cdf(z) phi = stats.norm.pdf(z) @@ -52,9 +53,9 @@ class probit(likelihood_function): mu = mu.flatten() var = var.flatten() mean = stats.norm.cdf(mu/np.sqrt(1+var)) - p_05 = np.zeros(mu.shape)#np.zeros([mu.size]) - p_95 = np.zeros(mu.shape)#np.ones([mu.size]) - return mean, p_05, p_95 + p_025 = np.zeros(mu.shape) + p_975 = np.ones(mu.shape) + return mean, p_025, p_975 class Poisson(likelihood_function): """ @@ -65,7 +66,7 @@ class Poisson(likelihood_function): L(x) = \exp(\lambda) * \lambda**Y_i / Y_i! $$ """ - def moments_match(self,i,tau_i,v_i): + def moments_match(self,data_i,tau_i,v_i): """ Moments match of the marginal approximation in EP algorithm @@ -81,14 +82,14 @@ class Poisson(likelihood_function): """ pdf_norm_f = stats.norm.pdf(f,loc=mu,scale=sigma) rate = np.exp( (f*self.scale)+self.location) - poisson = stats.poisson.pmf(float(self.Y[i]),rate) + poisson = stats.poisson.pmf(float(data_i),rate) return pdf_norm_f*poisson def log_pnm(f): """ Log of poisson_norm """ - return -(-.5*(f-mu)**2/sigma**2 - np.exp( (f*self.scale)+self.location) + ( (f*self.scale)+self.location)*self.Y[i]) + return -(-.5*(f-mu)**2/sigma**2 - np.exp( (f*self.scale)+self.location) + ( (f*self.scale)+self.location)*data_i) """ Golden Search and Simpson's Rule @@ -99,17 +100,17 @@ class Poisson(likelihood_function): #TODO golden search & simpson's rule can be defined in the general likelihood class, rather than in each specific case. #Golden search - golden_A = -1 if self.Y[i] == 0 else np.array([np.log(self.Y[i]),mu]).min() #Lower limit - golden_B = np.array([np.log(self.Y[i]),mu]).max() #Upper limit + golden_A = -1 if data_i == 0 else np.array([np.log(data_i),mu]).min() #Lower limit + golden_B = np.array([np.log(data_i),mu]).max() #Upper limit golden_A = (golden_A - self.location)/self.scale golden_B = (golden_B - self.location)/self.scale opt = sp.optimize.golden(log_pnm,brack=(golden_A,golden_B)) #Better to work with log_pnm than with poisson_norm # Simpson's approximation - width = 3./np.log(max(self.Y[i],2)) + width = 3./np.log(max(data_i,2)) A = opt - width #Lower limit B = opt + width #Upper limit - K = 10*int(np.log(max(self.Y[i],150))) #Number of points in the grid, we DON'T want K to be the same number for every case + K = 10*int(np.log(max(data_i,150))) #Number of points in the grid, we DON'T want K to be the same number for every case h = (B-A)/K # length of the intervals grid_x = np.hstack([np.linspace(opt-width,opt,K/2+1)[1:-1], np.linspace(opt,opt+width,K/2+1)]) # grid of points (X axis) x = np.hstack([A,B,grid_x[range(1,K,2)],grid_x[range(2,K-1,2)]]) # grid_x rearranged, just to make Simpson's algorithm easier @@ -127,7 +128,7 @@ class Poisson(likelihood_function): Compute mean, and conficence interval (percentiles 5 and 95) of the prediction """ mean = np.exp(mu*self.scale + self.location) - tmp = stats.poisson.ppf(np.array([.05,.95]),mu) - p_05 = tmp[:,0] - p_95 = tmp[:,1] - return mean,p_05,p_95 + tmp = stats.poisson.ppf(np.array([.025,.975]),mean) + p_025 = tmp[:,0] + p_975 = tmp[:,1] + return mean,p_025,p_975 From 2abaafd882cd11ee8de76ffadc6458062276cf5e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 7 Feb 2013 13:04:29 +0000 Subject: [PATCH 126/197] Modifications made to tutorial due to changes in GPy --- GPy/models/GP.py | 3 +- doc/Figures/tuto_GP_regression_m1.png | Bin 30678 -> 54971 bytes doc/Figures/tuto_GP_regression_m2.png | Bin 46404 -> 32176 bytes doc/Figures/tuto_GP_regression_m3.png | Bin 80246 -> 78550 bytes doc/tuto_GP_regression.rst | 40 ++++++++++++-------------- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index f8bd27bf..5e400c52 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -243,7 +243,8 @@ class GP(model): m, var, lower, upper = self.predict(Xnew, slices=which_functions) gpplot(Xnew,m, lower, upper) pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) - ymin,ymax = lower.min(),upper.max() + ymin,ymax = min(np.append(self.likelihood.data,lower)), max(np.append(self.likelihood.data,upper)) + ymin, ymax = ymin - 0.1*(ymax - ymin), ymax + 0.1*(ymax - ymin) pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) diff --git a/doc/Figures/tuto_GP_regression_m1.png b/doc/Figures/tuto_GP_regression_m1.png index c78d8a043aaca53d9aa419515375f8c576453bae..e4174825c50805bcc1634961d2d4bbc9847b88d2 100644 GIT binary patch literal 54971 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 XS?s>ZT>xGp%fR62>gTe~DWM4fDpmQq literal 30678 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 diff --git a/doc/Figures/tuto_GP_regression_m2.png b/doc/Figures/tuto_GP_regression_m2.png index b976a69c615ef1353ecb48f681c1d11db491c3a0..bf28f4e0b135647ecda1d454cd97ecc86065305d 100644 GIT binary patch literal 32176 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 j(0c^K2>bu%40ZTi{9~_LK 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%0DZP>}n4qL971q#yG|~k{PM59=`+)b3{K*+}q+|;H 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 V<99iUF~I?clAJoIM#ePwe*wJ$K)L_` diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 7d1a43df..17284707 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -22,13 +22,11 @@ For this toy example, we assume we have the following inputs and outputs:: Note that the observations Y include some noise. -The first step is to define the covariance kernel we want to use for the model. We choose here a kernel based on Gaussian kernel (i.e. rbf or square exponential) plus some white noise:: +The first step is to define the covariance kernel we want to use for the model. We choose here a kernel based on Gaussian kernel (i.e. rbf or square exponential):: - Gaussian = GPy.kern.rbf(D=1) - noise = GPy.kern.white(D=1) - kernel = Gaussian + noise + kernel = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) -The parameter ``D`` stands for the dimension of the input space. Note that many other kernels are implemented such as: +The parameter ``D`` stands for the dimension of the input space. The parameters ``variance`` and ``lengthscale`` are optional. Note that many other kernels are implemented such as: * linear (``GPy.kern.linear``) * exponential kernel (``GPy.kern.exponential``) @@ -41,19 +39,18 @@ The inputs required for building the model are the observations and the kernel:: m = GPy.models.GP_regression(X,Y,kernel) -The functions ``print`` and ``plot`` give an insight of the model we have just build. The code:: +By default, some observation noise is added to the modle. The functions ``print`` and ``plot`` give an insight of the model we have just build. The code:: print m m.plot() gives the following output: :: - - Marginal log-likelihood: -2.281e+01 + Marginal log-likelihood: -4.479e+00 Name | Value | Constraints | Ties | Prior ----------------------------------------------------------------- rbf_variance | 1.0000 | | | rbf_lengthscale | 1.0000 | | | - white_variance | 1.0000 | | | + noise variance | 1.0000 | | | .. figure:: Figures/tuto_GP_regression_m1.png :align: center @@ -75,7 +72,7 @@ but it is also possible to set a range on to constrain one parameter to be fixed m.unconstrain('') # Required to remove the previous constrains m.constrain_positive('rbf_variance') m.constrain_bounded('lengthscale',1.,10. ) - m.constrain_fixed('white',0.0025) + m.constrain_fixed('noise',0.0025) Once the constrains have been imposed, the model can be optimized:: @@ -87,12 +84,12 @@ If we want to perform some restarts to try to improve the result of the optimiza Once again, we can use ``print(m)`` and ``m.plot()`` to look at the resulting model resulting model:: - Marginal log-likelihood: 2.001e+01 + Marginal log-likelihood: 3.603e+01 Name | Value | Constraints | Ties | Prior ----------------------------------------------------------------- - rbf_variance | 0.8033 | (+ve) | | - rbf_lengthscale | 1.8033 | (1.0, 10.0) | | - white_variance | 0.0025 | Fixed | | + rbf_variance | 0.8151 | (+ve) | | + rbf_lengthscale | 1.8037 | (1.0, 10.0) | | + noise variance | 0.0025 | Fixed | | .. figure:: Figures/tuto_GP_regression_m2.png :align: center @@ -133,13 +130,14 @@ Here is a 2 dimensional example:: The flag ``ARD=True`` in the definition of the Matern kernel specifies that we want one lengthscale parameter per dimension (ie the GP is not isotropic). The output of the last 2 lines is:: - Marginal log-likelihood: 2.893e+01 - Name | Value | Constraints | Ties | Prior - ------------------------------------------------------------------------- - Mat52_ARD_variance | 0.4094 | (+ve) | | - Mat52_ARD_lengthscale_0 | 2.1060 | (+ve) | | - Mat52_ARD_lengthscale_1 | 2.0546 | (+ve) | | - white_variance | 0.0012 | (+ve) | | + Marginal log-likelihood: 6.682e+01 + Name | Value | Constraints | Ties | Prior + --------------------------------------------------------------------- + Mat52_variance | 0.3860 | (+ve) | | + Mat52_lengthscale_0 | 2.0578 | (+ve) | | + Mat52_lengthscale_1 | 1.8542 | (+ve) | | + white_variance | 0.0023 | (+ve) | | + noise variance | 0.0000 | (+ve) | | .. figure:: Figures/tuto_GP_regression_m3.png :align: center From 1d6885f6d99d344cb30d86442243d05215baeb7d Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 7 Feb 2013 16:09:58 +0000 Subject: [PATCH 127/197] small changes in tutorial --- doc/tuto_GP_regression.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 17284707..3527e86f 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -45,6 +45,7 @@ By default, some observation noise is added to the modle. The functions ``print` m.plot() gives the following output: :: + Marginal log-likelihood: -4.479e+00 Name | Value | Constraints | Ties | Prior ----------------------------------------------------------------- @@ -78,7 +79,7 @@ Once the constrains have been imposed, the model can be optimized:: m.optimize() -If we want to perform some restarts to try to improve the result of the optimization, we can use the optimize_restart function:: +If we want to perform some restarts to try to improve the result of the optimization, we can use the ``optimize_restart`` function:: m.optimize_restarts(Nrestarts = 10) @@ -128,7 +129,7 @@ Here is a 2 dimensional example:: m.plot() print(m) -The flag ``ARD=True`` in the definition of the Matern kernel specifies that we want one lengthscale parameter per dimension (ie the GP is not isotropic). The output of the last 2 lines is:: +The flag ``ARD=True`` in the definition of the Matern kernel specifies that we want one lengthscale parameter per dimension (ie the GP is not isotropic). The output of the last two lines is:: Marginal log-likelihood: 6.682e+01 Name | Value | Constraints | Ties | Prior From a32e2108634390ef699a677e9573f5b0769177e9 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:27:34 +0000 Subject: [PATCH 128/197] Taking another bash at executable docs... in vain --- doc/conf.py | 154 ++-- doc/sphinxext/__init__.py | 2 - doc/sphinxext/apigen.py | 427 --------- doc/sphinxext/docscrape.py | 497 ----------- doc/sphinxext/docscrape_sphinx.py | 136 --- doc/sphinxext/inheritance_diagram.py | 407 --------- doc/sphinxext/ipython_console_highlighting.py | 115 --- doc/sphinxext/ipython_directive.py | 830 ------------------ doc/sphinxext/mathmpl.py | 120 --- doc/sphinxext/numpydoc.py | 116 --- doc/sphinxext/only_directives.py | 64 -- doc/sphinxext/plot_directive.py | 819 ----------------- doc/tuto_GP_regression.rst | 6 + 13 files changed, 55 insertions(+), 3638 deletions(-) delete mode 100644 doc/sphinxext/__init__.py delete mode 100644 doc/sphinxext/apigen.py delete mode 100644 doc/sphinxext/docscrape.py delete mode 100644 doc/sphinxext/docscrape_sphinx.py delete mode 100644 doc/sphinxext/inheritance_diagram.py delete mode 100644 doc/sphinxext/ipython_console_highlighting.py delete mode 100644 doc/sphinxext/ipython_directive.py delete mode 100644 doc/sphinxext/mathmpl.py delete mode 100644 doc/sphinxext/numpydoc.py delete mode 100644 doc/sphinxext/only_directives.py delete mode 100644 doc/sphinxext/plot_directive.py diff --git a/doc/conf.py b/doc/conf.py index 693a3197..47bb52fb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,96 +11,38 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os -#Mocking uninstalled modules: https://read-the-docs.readthedocs.org/en/latest/faq.html - -#class Mock(object): - #__all__ = [] - #def __init__(self, *args, **kwargs): - #for key, value in kwargs.iteritems(): - #setattr(self, key, value) - - #def __call__(self, *args, **kwargs): - #return Mock() - - #__add__ = __mul__ = __getitem__ = __setitem__ = \ -#__delitem__ = __sub__ = __floordiv__ = __mod__ = __divmod__ = \ -#__pow__ = __lshift__ = __rshift__ = __and__ = __xor__ = __or__ = \ -#__rmul__ = __rsub__ = __rfloordiv__ = __rmod__ = __rdivmod__ = \ -#__rpow__ = __rlshift__ = __rrshift__ = __rand__ = __rxor__ = __ror__ = \ -#__imul__ = __isub__ = __ifloordiv__ = __imod__ = __idivmod__ = \ -#__ipow__ = __ilshift__ = __irshift__ = __iand__ = __ixor__ = __ior__ = \ -#__neg__ = __pos__ = __abs__ = __invert__ = __call__ - - #def __getattr__(self, name): - #if name in ('__file__', '__path__'): - #return '/dev/null' - #if name == 'sqrt': - #return math.sqrt - #elif name[0] != '_' and name[0] == name[0].upper(): - #return type(name, (), {}) - #else: - #return Mock(**vars(self)) - - #def __lt__(self, *args, **kwargs): - #return True - - #__nonzero__ = __le__ = __eq__ = __ne__ = __gt__ = __ge__ = __contains__ = \ -#__lt__ - - - #def __repr__(self): - ## Use _mock_repr to fake the __repr__ call - #res = getattr(self, "_mock_repr") - #return res if isinstance(res, str) else "Mock" - - #def __hash__(self): - #return 1 - - #__len__ = __int__ = __long__ = __index__ = __hash__ - - #def __oct__(self): - #return '01' - - #def __hex__(self): - #return '0x1' - - #def __float__(self): - #return 0.1 - - #def __complex__(self): - #return 1j - - -#MOCK_MODULES = [ - #'pylab', 'scipy', 'matplotlib', 'matplotlib.pyplot', 'pyfits', - #'scipy.constants.constants', 'matplotlib.cm', - #'matplotlib.image', 'matplotlib.colors', 'sunpy.cm', - #'pandas', 'pandas.io', 'pandas.io.parsers', - #'suds', 'matplotlib.ticker', 'matplotlib.colorbar', - #'matplotlib.dates', 'scipy.optimize', 'scipy.ndimage', - #'matplotlib.figure', 'scipy.ndimage.interpolation', 'bs4'] -#for mod_name in MOCK_MODULES: - #sys.modules[mod_name] = Mock() - - -#sys.modules['numpy'] = Mock(pi=math.pi, G=6.67364e-11, - #ndarray=type('ndarray', (), {}), - #dtype=lambda _: Mock(_mock_repr='np.dtype(\'float32\')')) -#sys.modules['scipy.constants'] = Mock(pi=math.pi, G=6.67364e-11) +print "python exec:", sys.executable +print "sys.path:", sys.path +try: + import numpy + print "numpy: %s, %s" % (numpy.__version__, numpy.__file__) +except ImportError: + print "no numpy" +try: + import matplotlib + print "matplotlib: %s, %s" % (matplotlib.__version__, matplotlib.__file__) +except ImportError: + print "no matplotlib" +try: + import IPython + print "ipython: %s, %s" % (IPython.__version__, IPython.__file__) +except ImportError: + print "no ipython" +sys.path.insert(0, os.getcwd() + "/..") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('..')) -print "Adding path" # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.append(os.path.abspath('./sphinxext')) +sys.path.append(os.path.abspath('sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -117,23 +59,24 @@ print "Adding path" # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. print "Importing extensions" -extensions = [#'ipython_directive', - 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'sphinx.ext.pngmath' +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', + 'sphinx.ext.pngmath', + 'ipython_directive', + 'ipython_console_highlighting.py' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', - #'ipython_directive' - ] - #'sphinx.ext.doctest', - #'ipython_console_highlighting', - #'inheritance_diagram', - #'numpydoc'] + ] + print "finished importing" ############################################################################## ## ## Mock out imports with C dependencies because ReadTheDocs can't build them. +############################################################################# + class Mock(object): def __init__(self, *args, **kwargs): pass @@ -156,7 +99,8 @@ class Mock(object): #import mock print "Mocking" -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser']#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] +MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser'] +#'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() @@ -164,11 +108,11 @@ for mod_name in MOCK_MODULES: on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: - sys.path.append("../GPy") - os.system("pwd") - os.system("sphinx-apidoc -f -o . ../GPy") - #os.system("cd ..") - #os.system("cd ./docs") + sys.path.append("../GPy") + os.system("pwd") + os.system("sphinx-apidoc -f -o . ../GPy") + #os.system("cd ..") + #os.system("cd ./docs") print "Compiled files" @@ -318,21 +262,21 @@ htmlhelp_basename = 'GPydoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'GPy.tex', u'GPy Documentation', - u'Author', 'manual'), + ('index', 'GPy.tex', u'GPy Documentation', + u'Author', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -375,9 +319,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'GPy', u'GPy Documentation', - u'Author', 'GPy', 'One line description of project.', - 'Miscellaneous'), + ('index', 'GPy', u'GPy Documentation', + u'Author', 'GPy', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/doc/sphinxext/__init__.py b/doc/sphinxext/__init__.py deleted file mode 100644 index 2caf15b1..00000000 --- a/doc/sphinxext/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from __future__ import print_function - diff --git a/doc/sphinxext/apigen.py b/doc/sphinxext/apigen.py deleted file mode 100644 index 12374096..00000000 --- a/doc/sphinxext/apigen.py +++ /dev/null @@ -1,427 +0,0 @@ -"""Attempt to generate templates for module reference with Sphinx - -XXX - we exclude extension modules - -To include extension modules, first identify them as valid in the -``_uri2path`` method, then handle them in the ``_parse_module`` script. - -We get functions and classes by parsing the text of .py files. -Alternatively we could import the modules for discovery, and we'd have -to do that for extension modules. This would involve changing the -``_parse_module`` method to work via import and introspection, and -might involve changing ``discover_modules`` (which determines which -files are modules, and therefore which module URIs will be passed to -``_parse_module``). - -NOTE: this is a modified version of a script originally shipped with the -PyMVPA project, which we've adapted for NIPY use. PyMVPA is an MIT-licensed -project.""" - -# Stdlib imports -import os -import re - -# Functions and classes -class ApiDocWriter(object): - ''' Class for automatic detection and parsing of API docs - to Sphinx-parsable reST format''' - - # only separating first two levels - rst_section_levels = ['*', '=', '-', '~', '^'] - - def __init__(self, - package_name, - rst_extension='.rst', - package_skip_patterns=None, - module_skip_patterns=None, - ): - ''' Initialize package for parsing - - Parameters - ---------- - package_name : string - Name of the top-level package. *package_name* must be the - name of an importable package - rst_extension : string, optional - Extension for reST files, default '.rst' - package_skip_patterns : None or sequence of {strings, regexps} - Sequence of strings giving URIs of packages to be excluded - Operates on the package path, starting at (including) the - first dot in the package path, after *package_name* - so, - if *package_name* is ``sphinx``, then ``sphinx.util`` will - result in ``.util`` being passed for earching by these - regexps. If is None, gives default. Default is: - ['\.tests$'] - module_skip_patterns : None or sequence - Sequence of strings giving URIs of modules to be excluded - Operates on the module name including preceding URI path, - back to the first dot after *package_name*. For example - ``sphinx.util.console`` results in the string to search of - ``.util.console`` - If is None, gives default. Default is: - ['\.setup$', '\._'] - ''' - if package_skip_patterns is None: - package_skip_patterns = ['\\.tests$'] - if module_skip_patterns is None: - module_skip_patterns = ['\\.setup$', '\\._'] - self.package_name = package_name - self.rst_extension = rst_extension - self.package_skip_patterns = package_skip_patterns - self.module_skip_patterns = module_skip_patterns - - def get_package_name(self): - return self._package_name - - def set_package_name(self, package_name): - ''' Set package_name - - >>> docwriter = ApiDocWriter('sphinx') - >>> import sphinx - >>> docwriter.root_path == sphinx.__path__[0] - True - >>> docwriter.package_name = 'docutils' - >>> import docutils - >>> docwriter.root_path == docutils.__path__[0] - True - ''' - # It's also possible to imagine caching the module parsing here - self._package_name = package_name - self.root_module = __import__(package_name) - self.root_path = self.root_module.__path__[0] - self.written_modules = None - - package_name = property(get_package_name, set_package_name, None, - 'get/set package_name') - - def _get_object_name(self, line): - ''' Get second token in line - >>> docwriter = ApiDocWriter('sphinx') - >>> docwriter._get_object_name(" def func(): ") - 'func' - >>> docwriter._get_object_name(" class Klass(object): ") - 'Klass' - >>> docwriter._get_object_name(" class Klass: ") - 'Klass' - ''' - name = line.split()[1].split('(')[0].strip() - # in case we have classes which are not derived from object - # ie. old style classes - return name.rstrip(':') - - def _uri2path(self, uri): - ''' Convert uri to absolute filepath - - Parameters - ---------- - uri : string - URI of python module to return path for - - Returns - ------- - path : None or string - Returns None if there is no valid path for this URI - Otherwise returns absolute file system path for URI - - Examples - -------- - >>> docwriter = ApiDocWriter('sphinx') - >>> import sphinx - >>> modpath = sphinx.__path__[0] - >>> res = docwriter._uri2path('sphinx.builder') - >>> res == os.path.join(modpath, 'builder.py') - True - >>> res = docwriter._uri2path('sphinx') - >>> res == os.path.join(modpath, '__init__.py') - True - >>> docwriter._uri2path('sphinx.does_not_exist') - - ''' - if uri == self.package_name: - return os.path.join(self.root_path, '__init__.py') - path = uri.replace('.', os.path.sep) - path = path.replace(self.package_name + os.path.sep, '') - path = os.path.join(self.root_path, path) - # XXX maybe check for extensions as well? - if os.path.exists(path + '.py'): # file - path += '.py' - elif os.path.exists(os.path.join(path, '__init__.py')): - path = os.path.join(path, '__init__.py') - else: - return None - return path - - def _path2uri(self, dirpath): - ''' Convert directory path to uri ''' - relpath = dirpath.replace(self.root_path, self.package_name) - if relpath.startswith(os.path.sep): - relpath = relpath[1:] - return relpath.replace(os.path.sep, '.') - - def _parse_module(self, uri): - ''' Parse module defined in *uri* ''' - filename = self._uri2path(uri) - if filename is None: - # nothing that we could handle here. - return ([],[]) - f = open(filename, 'rt') - functions, classes = self._parse_lines(f) - f.close() - return functions, classes - - def _parse_lines(self, linesource): - ''' Parse lines of text for functions and classes ''' - functions = [] - classes = [] - for line in linesource: - if line.startswith('def ') and line.count('('): - # exclude private stuff - name = self._get_object_name(line) - if not name.startswith('_'): - functions.append(name) - elif line.startswith('class '): - # exclude private stuff - name = self._get_object_name(line) - if not name.startswith('_'): - classes.append(name) - else: - pass - functions.sort() - classes.sort() - return functions, classes - - def generate_api_doc(self, uri): - '''Make autodoc documentation template string for a module - - Parameters - ---------- - uri : string - python location of module - e.g 'sphinx.builder' - - Returns - ------- - S : string - Contents of API doc - ''' - # get the names of all classes and functions - functions, classes = self._parse_module(uri) - if not len(functions) and not len(classes): - print 'WARNING: Empty -',uri # dbg - return '' - - # Make a shorter version of the uri that omits the package name for - # titles - uri_short = re.sub(r'^%s\.' % self.package_name,'',uri) - - ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' - - chap_title = uri_short - ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) - + '\n\n') - - # Set the chapter title to read 'module' for all modules except for the - # main packages - if '.' in uri: - title = 'Module: :mod:`' + uri_short + '`' - else: - title = ':mod:`' + uri_short + '`' - ad += title + '\n' + self.rst_section_levels[2] * len(title) - - if len(classes): - ad += '\nInheritance diagram for ``%s``:\n\n' % uri - ad += '.. inheritance-diagram:: %s \n' % uri - ad += ' :parts: 3\n' - - ad += '\n.. automodule:: ' + uri + '\n' - ad += '\n.. currentmodule:: ' + uri + '\n' - multi_class = len(classes) > 1 - multi_fx = len(functions) > 1 - if multi_class: - ad += '\n' + 'Classes' + '\n' + \ - self.rst_section_levels[2] * 7 + '\n' - elif len(classes) and multi_fx: - ad += '\n' + 'Class' + '\n' + \ - self.rst_section_levels[2] * 5 + '\n' - for c in classes: - ad += '\n:class:`' + c + '`\n' \ - + self.rst_section_levels[multi_class + 2 ] * \ - (len(c)+9) + '\n\n' - ad += '\n.. autoclass:: ' + c + '\n' - # must NOT exclude from index to keep cross-refs working - ad += ' :members:\n' \ - ' :undoc-members:\n' \ - ' :show-inheritance:\n' \ - ' :inherited-members:\n' \ - '\n' \ - ' .. automethod:: __init__\n' - if multi_fx: - ad += '\n' + 'Functions' + '\n' + \ - self.rst_section_levels[2] * 9 + '\n\n' - elif len(functions) and multi_class: - ad += '\n' + 'Function' + '\n' + \ - self.rst_section_levels[2] * 8 + '\n\n' - for f in functions: - # must NOT exclude from index to keep cross-refs working - ad += '\n.. autofunction:: ' + uri + '.' + f + '\n\n' - return ad - - def _survives_exclude(self, matchstr, match_type): - ''' Returns True if *matchstr* does not match patterns - - ``self.package_name`` removed from front of string if present - - Examples - -------- - >>> dw = ApiDocWriter('sphinx') - >>> dw._survives_exclude('sphinx.okpkg', 'package') - True - >>> dw.package_skip_patterns.append('^\\.badpkg$') - >>> dw._survives_exclude('sphinx.badpkg', 'package') - False - >>> dw._survives_exclude('sphinx.badpkg', 'module') - True - >>> dw._survives_exclude('sphinx.badmod', 'module') - True - >>> dw.module_skip_patterns.append('^\\.badmod$') - >>> dw._survives_exclude('sphinx.badmod', 'module') - False - ''' - if match_type == 'module': - patterns = self.module_skip_patterns - elif match_type == 'package': - patterns = self.package_skip_patterns - else: - raise ValueError('Cannot interpret match type "%s"' - % match_type) - # Match to URI without package name - L = len(self.package_name) - if matchstr[:L] == self.package_name: - matchstr = matchstr[L:] - for pat in patterns: - try: - pat.search - except AttributeError: - pat = re.compile(pat) - if pat.search(matchstr): - return False - return True - - def discover_modules(self): - ''' Return module sequence discovered from ``self.package_name`` - - - Parameters - ---------- - None - - Returns - ------- - mods : sequence - Sequence of module names within ``self.package_name`` - - Examples - -------- - >>> dw = ApiDocWriter('sphinx') - >>> mods = dw.discover_modules() - >>> 'sphinx.util' in mods - True - >>> dw.package_skip_patterns.append('\.util$') - >>> 'sphinx.util' in dw.discover_modules() - False - >>> - ''' - modules = [self.package_name] - # raw directory parsing - for dirpath, dirnames, filenames in os.walk(self.root_path): - # Check directory names for packages - root_uri = self._path2uri(os.path.join(self.root_path, - dirpath)) - for dirname in dirnames[:]: # copy list - we modify inplace - package_uri = '.'.join((root_uri, dirname)) - if (self._uri2path(package_uri) and - self._survives_exclude(package_uri, 'package')): - modules.append(package_uri) - else: - dirnames.remove(dirname) - # Check filenames for modules - for filename in filenames: - module_name = filename[:-3] - module_uri = '.'.join((root_uri, module_name)) - if (self._uri2path(module_uri) and - self._survives_exclude(module_uri, 'module')): - modules.append(module_uri) - return sorted(modules) - - def write_modules_api(self, modules,outdir): - # write the list - written_modules = [] - for m in modules: - api_str = self.generate_api_doc(m) - if not api_str: - continue - # write out to file - outfile = os.path.join(outdir, - m + self.rst_extension) - fileobj = open(outfile, 'wt') - fileobj.write(api_str) - fileobj.close() - written_modules.append(m) - self.written_modules = written_modules - - def write_api_docs(self, outdir): - """Generate API reST files. - - Parameters - ---------- - outdir : string - Directory name in which to store files - We create automatic filenames for each module - - Returns - ------- - None - - Notes - ----- - Sets self.written_modules to list of written modules - """ - if not os.path.exists(outdir): - os.mkdir(outdir) - # compose list of modules - modules = self.discover_modules() - self.write_modules_api(modules,outdir) - - def write_index(self, outdir, froot='gen', relative_to=None): - """Make a reST API index file from written files - - Parameters - ---------- - path : string - Filename to write index to - outdir : string - Directory to which to write generated index file - froot : string, optional - root (filename without extension) of filename to write to - Defaults to 'gen'. We add ``self.rst_extension``. - relative_to : string - path to which written filenames are relative. This - component of the written file path will be removed from - outdir, in the generated index. Default is None, meaning, - leave path as it is. - """ - if self.written_modules is None: - raise ValueError('No modules written') - # Get full filename path - path = os.path.join(outdir, froot+self.rst_extension) - # Path written into index is relative to rootpath - if relative_to is not None: - relpath = outdir.replace(relative_to + os.path.sep, '') - else: - relpath = outdir - idx = open(path,'wt') - w = idx.write - w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') - w('.. toctree::\n\n') - for f in self.written_modules: - w(' %s\n' % os.path.join(relpath,f)) - idx.close() diff --git a/doc/sphinxext/docscrape.py b/doc/sphinxext/docscrape.py deleted file mode 100644 index f374b3dd..00000000 --- a/doc/sphinxext/docscrape.py +++ /dev/null @@ -1,497 +0,0 @@ -"""Extract reference documentation from the NumPy source tree. - -""" - -import inspect -import textwrap -import re -import pydoc -from StringIO import StringIO -from warnings import warn -4 -class Reader(object): - """A line-based string reader. - - """ - def __init__(self, data): - """ - Parameters - ---------- - data : str - String with lines separated by '\n'. - - """ - if isinstance(data,list): - self._str = data - else: - self._str = data.split('\n') # store string as list of lines - - self.reset() - - def __getitem__(self, n): - return self._str[n] - - def reset(self): - self._l = 0 # current line nr - - def read(self): - if not self.eof(): - out = self[self._l] - self._l += 1 - return out - else: - return '' - - def seek_next_non_empty_line(self): - for l in self[self._l:]: - if l.strip(): - break - else: - self._l += 1 - - def eof(self): - return self._l >= len(self._str) - - def read_to_condition(self, condition_func): - start = self._l - for line in self[start:]: - if condition_func(line): - return self[start:self._l] - self._l += 1 - if self.eof(): - return self[start:self._l+1] - return [] - - def read_to_next_empty_line(self): - self.seek_next_non_empty_line() - def is_empty(line): - return not line.strip() - return self.read_to_condition(is_empty) - - def read_to_next_unindented_line(self): - def is_unindented(line): - return (line.strip() and (len(line.lstrip()) == len(line))) - return self.read_to_condition(is_unindented) - - def peek(self,n=0): - if self._l + n < len(self._str): - return self[self._l + n] - else: - return '' - - def is_empty(self): - return not ''.join(self._str).strip() - - -class NumpyDocString(object): - def __init__(self,docstring): - docstring = textwrap.dedent(docstring).split('\n') - - self._doc = Reader(docstring) - self._parsed_data = { - 'Signature': '', - 'Summary': [''], - 'Extended Summary': [], - 'Parameters': [], - 'Returns': [], - 'Raises': [], - 'Warns': [], - 'Other Parameters': [], - 'Attributes': [], - 'Methods': [], - 'See Also': [], - 'Notes': [], - 'Warnings': [], - 'References': '', - 'Examples': '', - 'index': {} - } - - self._parse() - - def __getitem__(self,key): - return self._parsed_data[key] - - def __setitem__(self,key,val): - if not self._parsed_data.has_key(key): - warn("Unknown section %s" % key) - else: - self._parsed_data[key] = val - - def _is_at_section(self): - self._doc.seek_next_non_empty_line() - - if self._doc.eof(): - return False - - l1 = self._doc.peek().strip() # e.g. Parameters - - if l1.startswith('.. index::'): - return True - - l2 = self._doc.peek(1).strip() # ---------- or ========== - return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) - - def _strip(self,doc): - i = 0 - j = 0 - for i,line in enumerate(doc): - if line.strip(): break - - for j,line in enumerate(doc[::-1]): - if line.strip(): break - - return doc[i:len(doc)-j] - - def _read_to_next_section(self): - section = self._doc.read_to_next_empty_line() - - while not self._is_at_section() and not self._doc.eof(): - if not self._doc.peek(-1).strip(): # previous line was empty - section += [''] - - section += self._doc.read_to_next_empty_line() - - return section - - def _read_sections(self): - while not self._doc.eof(): - data = self._read_to_next_section() - name = data[0].strip() - - if name.startswith('..'): # index section - yield name, data[1:] - elif len(data) < 2: - yield StopIteration - else: - yield name, self._strip(data[2:]) - - def _parse_param_list(self,content): - r = Reader(content) - params = [] - while not r.eof(): - header = r.read().strip() - if ' : ' in header: - arg_name, arg_type = header.split(' : ')[:2] - else: - arg_name, arg_type = header, '' - - desc = r.read_to_next_unindented_line() - desc = dedent_lines(desc) - - params.append((arg_name,arg_type,desc)) - - return params - - - _name_rgx = re.compile(r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" - r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) - def _parse_see_also(self, content): - """ - func_name : Descriptive text - continued text - another_func_name : Descriptive text - func_name1, func_name2, :meth:`func_name`, func_name3 - - """ - items = [] - - def parse_item_name(text): - """Match ':role:`name`' or 'name'""" - m = self._name_rgx.match(text) - if m: - g = m.groups() - if g[1] is None: - return g[3], None - else: - return g[2], g[1] - raise ValueError("%s is not a item name" % text) - - def push_item(name, rest): - if not name: - return - name, role = parse_item_name(name) - items.append((name, list(rest), role)) - del rest[:] - - current_func = None - rest = [] - - for line in content: - if not line.strip(): continue - - m = self._name_rgx.match(line) - if m and line[m.end():].strip().startswith(':'): - push_item(current_func, rest) - current_func, line = line[:m.end()], line[m.end():] - rest = [line.split(':', 1)[1].strip()] - if not rest[0]: - rest = [] - elif not line.startswith(' '): - push_item(current_func, rest) - current_func = None - if ',' in line: - for func in line.split(','): - push_item(func, []) - elif line.strip(): - current_func = line - elif current_func is not None: - rest.append(line.strip()) - push_item(current_func, rest) - return items - - def _parse_index(self, section, content): - """ - .. index: default - :refguide: something, else, and more - - """ - def strip_each_in(lst): - return [s.strip() for s in lst] - - out = {} - section = section.split('::') - if len(section) > 1: - out['default'] = strip_each_in(section[1].split(','))[0] - for line in content: - line = line.split(':') - if len(line) > 2: - out[line[1]] = strip_each_in(line[2].split(',')) - return out - - def _parse_summary(self): - """Grab signature (if given) and summary""" - if self._is_at_section(): - return - - summary = self._doc.read_to_next_empty_line() - summary_str = " ".join([s.strip() for s in summary]).strip() - if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): - self['Signature'] = summary_str - if not self._is_at_section(): - self['Summary'] = self._doc.read_to_next_empty_line() - else: - self['Summary'] = summary - - if not self._is_at_section(): - self['Extended Summary'] = self._read_to_next_section() - - def _parse(self): - self._doc.reset() - self._parse_summary() - - for (section,content) in self._read_sections(): - if not section.startswith('..'): - section = ' '.join([s.capitalize() for s in section.split(' ')]) - if section in ('Parameters', 'Attributes', 'Methods', - 'Returns', 'Raises', 'Warns'): - self[section] = self._parse_param_list(content) - elif section.startswith('.. index::'): - self['index'] = self._parse_index(section, content) - elif section == 'See Also': - self['See Also'] = self._parse_see_also(content) - else: - self[section] = content - - # string conversion routines - - def _str_header(self, name, symbol='-'): - return [name, len(name)*symbol] - - def _str_indent(self, doc, indent=4): - out = [] - for line in doc: - out += [' '*indent + line] - return out - - def _str_signature(self): - if self['Signature']: - return [self['Signature'].replace('*','\*')] + [''] - else: - return [''] - - def _str_summary(self): - if self['Summary']: - return self['Summary'] + [''] - else: - return [] - - def _str_extended_summary(self): - if self['Extended Summary']: - return self['Extended Summary'] + [''] - else: - return [] - - def _str_param_list(self, name): - out = [] - if self[name]: - out += self._str_header(name) - for param,param_type,desc in self[name]: - out += ['%s : %s' % (param, param_type)] - out += self._str_indent(desc) - out += [''] - return out - - def _str_section(self, name): - out = [] - if self[name]: - out += self._str_header(name) - out += self[name] - out += [''] - return out - - def _str_see_also(self, func_role): - if not self['See Also']: return [] - out = [] - out += self._str_header("See Also") - last_had_desc = True - for func, desc, role in self['See Also']: - if role: - link = ':%s:`%s`' % (role, func) - elif func_role: - link = ':%s:`%s`' % (func_role, func) - else: - link = "`%s`_" % func - if desc or last_had_desc: - out += [''] - out += [link] - else: - out[-1] += ", %s" % link - if desc: - out += self._str_indent([' '.join(desc)]) - last_had_desc = True - else: - last_had_desc = False - out += [''] - return out - - def _str_index(self): - idx = self['index'] - out = [] - out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): - if section == 'default': - continue - out += [' :%s: %s' % (section, ', '.join(references))] - return out - - def __str__(self, func_role=''): - out = [] - out += self._str_signature() - out += self._str_summary() - out += self._str_extended_summary() - for param_list in ('Parameters','Returns','Raises'): - out += self._str_param_list(param_list) - out += self._str_section('Warnings') - out += self._str_see_also(func_role) - for s in ('Notes','References','Examples'): - out += self._str_section(s) - out += self._str_index() - return '\n'.join(out) - - -def indent(str,indent=4): - indent_str = ' '*indent - if str is None: - return indent_str - lines = str.split('\n') - return '\n'.join(indent_str + l for l in lines) - -def dedent_lines(lines): - """Deindent a list of lines maximally""" - return textwrap.dedent("\n".join(lines)).split("\n") - -def header(text, style='-'): - return text + '\n' + style*len(text) + '\n' - - -class FunctionDoc(NumpyDocString): - def __init__(self, func, role='func', doc=None): - self._f = func - self._role = role # e.g. "func" or "meth" - if doc is None: - doc = inspect.getdoc(func) or '' - try: - NumpyDocString.__init__(self, doc) - except ValueError, e: - print '*'*78 - print "ERROR: '%s' while parsing `%s`" % (e, self._f) - print '*'*78 - #print "Docstring follows:" - #print doclines - #print '='*78 - - if not self['Signature']: - func, func_name = self.get_func() - try: - # try to read signature - argspec = inspect.getargspec(func) - argspec = inspect.formatargspec(*argspec) - argspec = argspec.replace('*','\*') - signature = '%s%s' % (func_name, argspec) - except TypeError, e: - signature = '%s()' % func_name - self['Signature'] = signature - - def get_func(self): - func_name = getattr(self._f, '__name__', self.__class__.__name__) - if inspect.isclass(self._f): - func = getattr(self._f, '__call__', self._f.__init__) - else: - func = self._f - return func, func_name - - def __str__(self): - out = '' - - func, func_name = self.get_func() - signature = self['Signature'].replace('*', '\*') - - roles = {'func': 'function', - 'meth': 'method'} - - if self._role: - if not roles.has_key(self._role): - print "Warning: invalid role %s" % self._role - out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), - func_name) - - out += super(FunctionDoc, self).__str__(func_role=self._role) - return out - - -class ClassDoc(NumpyDocString): - def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None): - if not inspect.isclass(cls): - raise ValueError("Initialise using a class. Got %r" % cls) - self._cls = cls - - if modulename and not modulename.endswith('.'): - modulename += '.' - self._mod = modulename - self._name = cls.__name__ - self._func_doc = func_doc - - if doc is None: - doc = pydoc.getdoc(cls) - - NumpyDocString.__init__(self, doc) - - @property - def methods(self): - return [name for name,func in inspect.getmembers(self._cls) - if not name.startswith('_') and callable(func)] - - def __str__(self): - out = '' - out += super(ClassDoc, self).__str__() - out += "\n\n" - - #for m in self.methods: - # print "Parsing `%s`" % m - # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n' - # out += '.. index::\n single: %s; %s\n\n' % (self._name, m) - - return out - - diff --git a/doc/sphinxext/docscrape_sphinx.py b/doc/sphinxext/docscrape_sphinx.py deleted file mode 100644 index 77ed271b..00000000 --- a/doc/sphinxext/docscrape_sphinx.py +++ /dev/null @@ -1,136 +0,0 @@ -import re, inspect, textwrap, pydoc -from docscrape import NumpyDocString, FunctionDoc, ClassDoc - -class SphinxDocString(NumpyDocString): - # string conversion routines - def _str_header(self, name, symbol='`'): - return ['.. rubric:: ' + name, ''] - - def _str_field_list(self, name): - return [':' + name + ':'] - - def _str_indent(self, doc, indent=4): - out = [] - for line in doc: - out += [' '*indent + line] - return out - - def _str_signature(self): - return [''] - if self['Signature']: - return ['``%s``' % self['Signature']] + [''] - else: - return [''] - - def _str_summary(self): - return self['Summary'] + [''] - - def _str_extended_summary(self): - return self['Extended Summary'] + [''] - - def _str_param_list(self, name): - out = [] - if self[name]: - out += self._str_field_list(name) - out += [''] - for param,param_type,desc in self[name]: - out += self._str_indent(['**%s** : %s' % (param.strip(), - param_type)]) - out += [''] - out += self._str_indent(desc,8) - out += [''] - return out - - def _str_section(self, name): - out = [] - if self[name]: - out += self._str_header(name) - out += [''] - content = textwrap.dedent("\n".join(self[name])).split("\n") - out += content - out += [''] - return out - - def _str_see_also(self, func_role): - out = [] - if self['See Also']: - see_also = super(SphinxDocString, self)._str_see_also(func_role) - out = ['.. seealso::', ''] - out += self._str_indent(see_also[2:]) - return out - - def _str_warnings(self): - out = [] - if self['Warnings']: - out = ['.. warning::', ''] - out += self._str_indent(self['Warnings']) - return out - - def _str_index(self): - idx = self['index'] - out = [] - if len(idx) == 0: - return out - - out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): - if section == 'default': - continue - elif section == 'refguide': - out += [' single: %s' % (', '.join(references))] - else: - out += [' %s: %s' % (section, ','.join(references))] - return out - - def _str_references(self): - out = [] - if self['References']: - out += self._str_header('References') - if isinstance(self['References'], str): - self['References'] = [self['References']] - out.extend(self['References']) - out += [''] - return out - - def __str__(self, indent=0, func_role="obj"): - out = [] - out += self._str_signature() - out += self._str_index() + [''] - out += self._str_summary() - out += self._str_extended_summary() - for param_list in ('Parameters', 'Attributes', 'Methods', - 'Returns','Raises'): - out += self._str_param_list(param_list) - out += self._str_warnings() - out += self._str_see_also(func_role) - out += self._str_section('Notes') - out += self._str_references() - out += self._str_section('Examples') - out = self._str_indent(out,indent) - return '\n'.join(out) - -class SphinxFunctionDoc(SphinxDocString, FunctionDoc): - pass - -class SphinxClassDoc(SphinxDocString, ClassDoc): - pass - -def get_doc_object(obj, what=None, doc=None): - if what is None: - if inspect.isclass(obj): - what = 'class' - elif inspect.ismodule(obj): - what = 'module' - elif callable(obj): - what = 'function' - else: - what = 'object' - if what == 'class': - return SphinxClassDoc(obj, '', func_doc=SphinxFunctionDoc, doc=doc) - elif what in ('function', 'method'): - return SphinxFunctionDoc(obj, '', doc=doc) - else: - if doc is None: - doc = pydoc.getdoc(obj) - return SphinxDocString(doc) - diff --git a/doc/sphinxext/inheritance_diagram.py b/doc/sphinxext/inheritance_diagram.py deleted file mode 100644 index 407fc13f..00000000 --- a/doc/sphinxext/inheritance_diagram.py +++ /dev/null @@ -1,407 +0,0 @@ -""" -Defines a docutils directive for inserting inheritance diagrams. - -Provide the directive with one or more classes or modules (separated -by whitespace). For modules, all of the classes in that module will -be used. - -Example:: - - Given the following classes: - - class A: pass - class B(A): pass - class C(A): pass - class D(B, C): pass - class E(B): pass - - .. inheritance-diagram: D E - - Produces a graph like the following: - - A - / \ - B C - / \ / - E D - -The graph is inserted as a PNG+image map into HTML and a PDF in -LaTeX. -""" - -import inspect -import os -import re -import subprocess -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - -from docutils.nodes import Body, Element -from docutils.parsers.rst import directives -from sphinx.roles import xfileref_role - -def my_import(name): - """Module importer - taken from the python documentation. - - This function allows importing names with dots in them.""" - - mod = __import__(name) - components = name.split('.') - for comp in components[1:]: - mod = getattr(mod, comp) - return mod - -class DotException(Exception): - pass - -class InheritanceGraph(object): - """ - Given a list of classes, determines the set of classes that - they inherit from all the way to the root "object", and then - is able to generate a graphviz dot graph from them. - """ - def __init__(self, class_names, show_builtins=False): - """ - *class_names* is a list of child classes to show bases from. - - If *show_builtins* is True, then Python builtins will be shown - in the graph. - """ - self.class_names = class_names - self.classes = self._import_classes(class_names) - self.all_classes = self._all_classes(self.classes) - if len(self.all_classes) == 0: - raise ValueError("No classes found for inheritance diagram") - self.show_builtins = show_builtins - - py_sig_re = re.compile(r'''^([\w.]*\.)? # class names - (\w+) \s* $ # optionally arguments - ''', re.VERBOSE) - - def _import_class_or_module(self, name): - """ - Import a class using its fully-qualified *name*. - """ - try: - path, base = self.py_sig_re.match(name).groups() - except: - raise ValueError( - "Invalid class or module '%s' specified for inheritance diagram" % name) - fullname = (path or '') + base - path = (path and path.rstrip('.')) - if not path: - path = base - try: - module = __import__(path, None, None, []) - # We must do an import of the fully qualified name. Otherwise if a - # subpackage 'a.b' is requested where 'import a' does NOT provide - # 'a.b' automatically, then 'a.b' will not be found below. This - # second call will force the equivalent of 'import a.b' to happen - # after the top-level import above. - my_import(fullname) - - except ImportError: - raise ValueError( - "Could not import class or module '%s' specified for inheritance diagram" % name) - - try: - todoc = module - for comp in fullname.split('.')[1:]: - todoc = getattr(todoc, comp) - except AttributeError: - raise ValueError( - "Could not find class or module '%s' specified for inheritance diagram" % name) - - # If a class, just return it - if inspect.isclass(todoc): - return [todoc] - elif inspect.ismodule(todoc): - classes = [] - for cls in todoc.__dict__.values(): - if inspect.isclass(cls) and cls.__module__ == todoc.__name__: - classes.append(cls) - return classes - raise ValueError( - "'%s' does not resolve to a class or module" % name) - - def _import_classes(self, class_names): - """ - Import a list of classes. - """ - classes = [] - for name in class_names: - classes.extend(self._import_class_or_module(name)) - return classes - - def _all_classes(self, classes): - """ - Return a list of all classes that are ancestors of *classes*. - """ - all_classes = {} - - def recurse(cls): - all_classes[cls] = None - for c in cls.__bases__: - if c not in all_classes: - recurse(c) - - for cls in classes: - recurse(cls) - - return all_classes.keys() - - def class_name(self, cls, parts=0): - """ - Given a class object, return a fully-qualified name. This - works for things I've tested in matplotlib so far, but may not - be completely general. - """ - module = cls.__module__ - if module == '__builtin__': - fullname = cls.__name__ - else: - fullname = "%s.%s" % (module, cls.__name__) - if parts == 0: - return fullname - name_parts = fullname.split('.') - return '.'.join(name_parts[-parts:]) - - def get_all_class_names(self): - """ - Get all of the class names involved in the graph. - """ - return [self.class_name(x) for x in self.all_classes] - - # These are the default options for graphviz - default_graph_options = { - "rankdir": "LR", - "size": '"8.0, 12.0"' - } - default_node_options = { - "shape": "box", - "fontsize": 10, - "height": 0.25, - "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans", - "style": '"setlinewidth(0.5)"' - } - default_edge_options = { - "arrowsize": 0.5, - "style": '"setlinewidth(0.5)"' - } - - def _format_node_options(self, options): - return ','.join(["%s=%s" % x for x in options.items()]) - def _format_graph_options(self, options): - return ''.join(["%s=%s;\n" % x for x in options.items()]) - - def generate_dot(self, fd, name, parts=0, urls={}, - graph_options={}, node_options={}, - edge_options={}): - """ - Generate a graphviz dot graph from the classes that - were passed in to __init__. - - *fd* is a Python file-like object to write to. - - *name* is the name of the graph - - *urls* is a dictionary mapping class names to http urls - - *graph_options*, *node_options*, *edge_options* are - dictionaries containing key/value pairs to pass on as graphviz - properties. - """ - g_options = self.default_graph_options.copy() - g_options.update(graph_options) - n_options = self.default_node_options.copy() - n_options.update(node_options) - e_options = self.default_edge_options.copy() - e_options.update(edge_options) - - fd.write('digraph %s {\n' % name) - fd.write(self._format_graph_options(g_options)) - - for cls in self.all_classes: - if not self.show_builtins and cls in __builtins__.values(): - continue - - name = self.class_name(cls, parts) - - # Write the node - this_node_options = n_options.copy() - url = urls.get(self.class_name(cls)) - if url is not None: - this_node_options['URL'] = '"%s"' % url - fd.write(' "%s" [%s];\n' % - (name, self._format_node_options(this_node_options))) - - # Write the edges - for base in cls.__bases__: - if not self.show_builtins and base in __builtins__.values(): - continue - - base_name = self.class_name(base, parts) - fd.write(' "%s" -> "%s" [%s];\n' % - (base_name, name, - self._format_node_options(e_options))) - fd.write('}\n') - - def run_dot(self, args, name, parts=0, urls={}, - graph_options={}, node_options={}, edge_options={}): - """ - Run graphviz 'dot' over this graph, returning whatever 'dot' - writes to stdout. - - *args* will be passed along as commandline arguments. - - *name* is the name of the graph - - *urls* is a dictionary mapping class names to http urls - - Raises DotException for any of the many os and - installation-related errors that may occur. - """ - try: - dot = subprocess.Popen(['dot'] + list(args), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - close_fds=True) - except OSError: - raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?") - except ValueError: - raise DotException("'dot' called with invalid arguments") - except: - raise DotException("Unexpected error calling 'dot'") - - self.generate_dot(dot.stdin, name, parts, urls, graph_options, - node_options, edge_options) - dot.stdin.close() - result = dot.stdout.read() - returncode = dot.wait() - if returncode != 0: - raise DotException("'dot' returned the errorcode %d" % returncode) - return result - -class inheritance_diagram(Body, Element): - """ - A docutils node to use as a placeholder for the inheritance - diagram. - """ - pass - -def inheritance_diagram_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, - state_machine): - """ - Run when the inheritance_diagram directive is first encountered. - """ - node = inheritance_diagram() - - class_names = arguments - - # Create a graph starting with the list of classes - graph = InheritanceGraph(class_names) - - # Create xref nodes for each target of the graph's image map and - # add them to the doc tree so that Sphinx can resolve the - # references to real URLs later. These nodes will eventually be - # removed from the doctree after we're done with them. - for name in graph.get_all_class_names(): - refnodes, x = xfileref_role( - 'class', ':class:`%s`' % name, name, 0, state) - node.extend(refnodes) - # Store the graph object so we can use it to generate the - # dot file later - node['graph'] = graph - # Store the original content for use as a hash - node['parts'] = options.get('parts', 0) - node['content'] = " ".join(class_names) - return [node] - -def get_graph_hash(node): - return md5(node['content'] + str(node['parts'])).hexdigest()[-10:] - -def html_output_graph(self, node): - """ - Output the graph for HTML. This will insert a PNG with clickable - image map. - """ - graph = node['graph'] - parts = node['parts'] - - graph_hash = get_graph_hash(node) - name = "inheritance%s" % graph_hash - path = '_images' - dest_path = os.path.join(setup.app.builder.outdir, path) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - png_path = os.path.join(dest_path, name + ".png") - path = setup.app.builder.imgpath - - # Create a mapping from fully-qualified class names to URLs. - urls = {} - for child in node: - if child.get('refuri') is not None: - urls[child['reftitle']] = child.get('refuri') - elif child.get('refid') is not None: - urls[child['reftitle']] = '#' + child.get('refid') - - # These arguments to dot will save a PNG file to disk and write - # an HTML image map to stdout. - image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'], - name, parts, urls) - return ('%s' % - (path, name, name, image_map)) - -def latex_output_graph(self, node): - """ - Output the graph for LaTeX. This will insert a PDF. - """ - graph = node['graph'] - parts = node['parts'] - - graph_hash = get_graph_hash(node) - name = "inheritance%s" % graph_hash - dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images')) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf")) - - graph.run_dot(['-Tpdf', '-o%s' % pdf_path], - name, parts, graph_options={'size': '"6.0,6.0"'}) - return '\n\\includegraphics{%s}\n\n' % pdf_path - -def visit_inheritance_diagram(inner_func): - """ - This is just a wrapper around html/latex_output_graph to make it - easier to handle errors and insert warnings. - """ - def visitor(self, node): - try: - content = inner_func(self, node) - except DotException, e: - # Insert the exception as a warning in the document - warning = self.document.reporter.warning(str(e), line=node.line) - warning.parent = node - node.children = [warning] - else: - source = self.document.attributes['source'] - self.body.append(content) - node.children = [] - return visitor - -def do_nothing(self, node): - pass - -def setup(app): - setup.app = app - setup.confdir = app.confdir - - app.add_node( - inheritance_diagram, - latex=(visit_inheritance_diagram(latex_output_graph), do_nothing), - html=(visit_inheritance_diagram(html_output_graph), do_nothing)) - app.add_directive( - 'inheritance-diagram', inheritance_diagram_directive, - False, (1, 100, 0), parts = directives.nonnegative_int) diff --git a/doc/sphinxext/ipython_console_highlighting.py b/doc/sphinxext/ipython_console_highlighting.py deleted file mode 100644 index c9bf1c15..00000000 --- a/doc/sphinxext/ipython_console_highlighting.py +++ /dev/null @@ -1,115 +0,0 @@ -"""reST directive for syntax-highlighting ipython interactive sessions. - -XXX - See what improvements can be made based on the new (as of Sept 2009) -'pycon' lexer for the python console. At the very least it will give better -highlighted tracebacks. -""" -from __future__ import print_function - -#----------------------------------------------------------------------------- -# Needed modules - -# Standard library -import re - -# Third party -from pygments.lexer import Lexer, do_insertions -from pygments.lexers.agile import (PythonConsoleLexer, PythonLexer, - PythonTracebackLexer) -from pygments.token import Comment, Generic - -from sphinx import highlighting - -#----------------------------------------------------------------------------- -# Global constants -line_re = re.compile('.*?\n') - -#----------------------------------------------------------------------------- -# Code begins - classes and functions - -class IPythonConsoleLexer(Lexer): - """ - For IPython console output or doctests, such as: - - .. sourcecode:: ipython - - In [1]: a = 'foo' - - In [2]: a - Out[2]: 'foo' - - In [3]: print a - foo - - In [4]: 1 / 0 - - Notes: - - - Tracebacks are not currently supported. - - - It assumes the default IPython prompts, not customized ones. - """ - - name = 'IPython console session' - aliases = ['ipython'] - mimetypes = ['text/x-ipython-console'] - input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") - output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") - continue_prompt = re.compile(" \.\.\.+:") - tb_start = re.compile("\-+") - - def get_tokens_unprocessed(self, text): - pylexer = PythonLexer(**self.options) - tblexer = PythonTracebackLexer(**self.options) - - curcode = '' - insertions = [] - for match in line_re.finditer(text): - line = match.group() - input_prompt = self.input_prompt.match(line) - continue_prompt = self.continue_prompt.match(line.rstrip()) - output_prompt = self.output_prompt.match(line) - if line.startswith("#"): - insertions.append((len(curcode), - [(0, Comment, line)])) - elif input_prompt is not None: - insertions.append((len(curcode), - [(0, Generic.Prompt, input_prompt.group())])) - curcode += line[input_prompt.end():] - elif continue_prompt is not None: - insertions.append((len(curcode), - [(0, Generic.Prompt, continue_prompt.group())])) - curcode += line[continue_prompt.end():] - elif output_prompt is not None: - # Use the 'error' token for output. We should probably make - # our own token, but error is typicaly in a bright color like - # red, so it works fine for our output prompts. - insertions.append((len(curcode), - [(0, Generic.Error, output_prompt.group())])) - curcode += line[output_prompt.end():] - else: - if curcode: - for item in do_insertions(insertions, - pylexer.get_tokens_unprocessed(curcode)): - yield item - curcode = '' - insertions = [] - yield match.start(), Generic.Output, line - if curcode: - for item in do_insertions(insertions, - pylexer.get_tokens_unprocessed(curcode)): - yield item - - -def setup(app): - """Setup as a sphinx extension.""" - - # This is only a lexer, so adding it below to pygments appears sufficient. - # But if somebody knows that the right API usage should be to do that via - # sphinx, by all means fix it here. At least having this setup.py - # suppresses the sphinx warning we'd get without it. - pass - -#----------------------------------------------------------------------------- -# Register the extension as a valid pygments lexer -highlighting.lexers['ipython'] = IPythonConsoleLexer() diff --git a/doc/sphinxext/ipython_directive.py b/doc/sphinxext/ipython_directive.py deleted file mode 100644 index 79cd2aed..00000000 --- a/doc/sphinxext/ipython_directive.py +++ /dev/null @@ -1,830 +0,0 @@ -# -*- coding: utf-8 -*- -"""Sphinx directive to support embedded IPython code. - -This directive allows pasting of entire interactive IPython sessions, prompts -and all, and their code will actually get re-executed at doc build time, with -all prompts renumbered sequentially. It also allows you to input code as a pure -python input by giving the argument python to the directive. The output looks -like an interactive ipython section. - -To enable this directive, simply list it in your Sphinx ``conf.py`` file -(making sure the directory where you placed it is visible to sphinx, as is -needed for all Sphinx directives). - -By default this directive assumes that your prompts are unchanged IPython ones, -but this can be customized. The configurable options that can be placed in -conf.py are - -ipython_savefig_dir: - The directory in which to save the figures. This is relative to the - Sphinx source directory. The default is `html_static_path`. -ipython_rgxin: - The compiled regular expression to denote the start of IPython input - lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You - shouldn't need to change this. -ipython_rgxout: - The compiled regular expression to denote the start of IPython output - lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You - shouldn't need to change this. -ipython_promptin: - The string to represent the IPython input prompt in the generated ReST. - The default is 'In [%d]:'. This expects that the line numbers are used - in the prompt. -ipython_promptout: - - The string to represent the IPython prompt in the generated ReST. The - default is 'Out [%d]:'. This expects that the line numbers are used - in the prompt. - -ToDo ----- - -- Turn the ad-hoc test() function into a real test suite. -- Break up ipython-specific functionality from matplotlib stuff into better - separated code. - -Authors -------- - -- John D Hunter: orignal author. -- Fernando Perez: refactoring, documentation, cleanups, port to 0.11. -- VáclavŠmilauer : Prompt generalizations. -- Skipper Seabold, refactoring, cleanups, pure python addition -""" - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -# Stdlib -import cStringIO -import os -import re -import sys -import tempfile -import ast - -# To keep compatibility with various python versions -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - -# Third-party -import matplotlib -import sphinx -from docutils.parsers.rst import directives -from docutils import nodes -from sphinx.util.compat import Directive - -matplotlib.use('Agg') - -# Our own -from IPython import Config, InteractiveShell -from IPython.core.profiledir import ProfileDir -from IPython.utils import io - -#----------------------------------------------------------------------------- -# Globals -#----------------------------------------------------------------------------- -# for tokenizing blocks -COMMENT, INPUT, OUTPUT = range(3) - -#----------------------------------------------------------------------------- -# Functions and class declarations -#----------------------------------------------------------------------------- -def block_parser(part, rgxin, rgxout, fmtin, fmtout): - """ - part is a string of ipython text, comprised of at most one - input, one ouput, comments, and blank lines. The block parser - parses the text into a list of:: - - blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...] - - where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and - data is, depending on the type of token:: - - COMMENT : the comment string - - INPUT: the (DECORATOR, INPUT_LINE, REST) where - DECORATOR: the input decorator (or None) - INPUT_LINE: the input as string (possibly multi-line) - REST : any stdout generated by the input line (not OUTPUT) - - - OUTPUT: the output string, possibly multi-line - """ - - block = [] - lines = part.split('\n') - N = len(lines) - i = 0 - decorator = None - while 1: - - if i==N: - # nothing left to parse -- the last line - break - - line = lines[i] - i += 1 - line_stripped = line.strip() - if line_stripped.startswith('#'): - block.append((COMMENT, line)) - continue - - if line_stripped.startswith('@'): - # we're assuming at most one decorator -- may need to - # rethink - decorator = line_stripped - continue - - # does this look like an input line? - matchin = rgxin.match(line) - if matchin: - lineno, inputline = int(matchin.group(1)), matchin.group(2) - - # the ....: continuation string - continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) - Nc = len(continuation) - # input lines can continue on for more than one line, if - # we have a '\' line continuation char or a function call - # echo line 'print'. The input line can only be - # terminated by the end of the block or an output line, so - # we parse out the rest of the input line if it is - # multiline as well as any echo text - - rest = [] - while i 1: - if input_lines[-1] != "": - input_lines.append('') # make sure there's a blank line - # so splitter buffer gets reset - - continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) - Nc = len(continuation) - - if is_savefig: - image_file, image_directive = self.process_image(decorator) - - ret = [] - is_semicolon = False - - for i, line in enumerate(input_lines): - if line.endswith(';'): - is_semicolon = True - - if i==0: - # process the first input line - if is_verbatim: - self.process_input_line('') - self.IP.execution_count += 1 # increment it anyway - else: - # only submit the line in non-verbatim mode - self.process_input_line(line, store_history=True) - formatted_line = '%s %s'%(input_prompt, line) - else: - # process a continuation line - if not is_verbatim: - self.process_input_line(line, store_history=True) - - formatted_line = '%s %s'%(continuation, line) - - if not is_suppress: - ret.append(formatted_line) - - if not is_suppress and len(rest.strip()) and is_verbatim: - # the "rest" is the standard output of the - # input, which needs to be added in - # verbatim mode - ret.append(rest) - - self.cout.seek(0) - output = self.cout.read() - if not is_suppress and not is_semicolon: - ret.append(output) - elif is_semicolon: # get spacing right - ret.append('') - - self.cout.truncate(0) - return (ret, input_lines, output, is_doctest, image_file, - image_directive) - #print 'OUTPUT', output # dbg - - def process_output(self, data, output_prompt, - input_lines, output, is_doctest, image_file): - """Process data block for OUTPUT token.""" - if is_doctest: - submitted = data.strip() - found = output - if found is not None: - found = found.strip() - - # XXX - fperez: in 0.11, 'output' never comes with the prompt - # in it, just the actual output text. So I think all this code - # can be nuked... - - # the above comment does not appear to be accurate... (minrk) - - ind = found.find(output_prompt) - if ind<0: - e='output prompt="%s" does not match out line=%s' % \ - (output_prompt, found) - raise RuntimeError(e) - found = found[len(output_prompt):].strip() - - if found!=submitted: - e = ('doctest failure for input_lines="%s" with ' - 'found_output="%s" and submitted output="%s"' % - (input_lines, found, submitted) ) - raise RuntimeError(e) - #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted) - - def process_comment(self, data): - """Process data fPblock for COMMENT token.""" - if not self.is_suppress: - return [data] - - def save_image(self, image_file): - """ - Saves the image file to disk. - """ - self.ensure_pyplot() - command = 'plt.gcf().savefig("%s")'%image_file - #print 'SAVEFIG', command # dbg - self.process_input_line('bookmark ipy_thisdir', store_history=False) - self.process_input_line('cd -b ipy_savedir', store_history=False) - self.process_input_line(command, store_history=False) - self.process_input_line('cd -b ipy_thisdir', store_history=False) - self.process_input_line('bookmark -d ipy_thisdir', store_history=False) - self.clear_cout() - - - def process_block(self, block): - """ - process block from the block_parser and return a list of processed lines - """ - ret = [] - output = None - input_lines = None - lineno = self.IP.execution_count - - input_prompt = self.promptin%lineno - output_prompt = self.promptout%lineno - image_file = None - image_directive = None - - for token, data in block: - if token==COMMENT: - out_data = self.process_comment(data) - elif token==INPUT: - (out_data, input_lines, output, is_doctest, image_file, - image_directive) = \ - self.process_input(data, input_prompt, lineno) - elif token==OUTPUT: - out_data = \ - self.process_output(data, output_prompt, - input_lines, output, is_doctest, - image_file) - if out_data: - ret.extend(out_data) - - # save the image files - if image_file is not None: - self.save_image(image_file) - - return ret, image_directive - - def ensure_pyplot(self): - if self._pyplot_imported: - return - self.process_input_line('import matplotlib.pyplot as plt', - store_history=False) - - def process_pure_python(self, content): - """ - content is a list of strings. it is unedited directive conent - - This runs it line by line in the InteractiveShell, prepends - prompts as needed capturing stderr and stdout, then returns - the content as a list as if it were ipython code - """ - output = [] - savefig = False # keep up with this to clear figure - multiline = False # to handle line continuation - multiline_start = None - fmtin = self.promptin - - ct = 0 - - for lineno, line in enumerate(content): - - line_stripped = line.strip() - if not len(line): - output.append(line) - continue - - # handle decorators - if line_stripped.startswith('@'): - output.extend([line]) - if 'savefig' in line: - savefig = True # and need to clear figure - continue - - # handle comments - if line_stripped.startswith('#'): - output.extend([line]) - continue - - # deal with lines checking for multiline - continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2)) - if not multiline: - modified = u"%s %s" % (fmtin % ct, line_stripped) - output.append(modified) - ct += 1 - try: - ast.parse(line_stripped) - output.append(u'') - except Exception: # on a multiline - multiline = True - multiline_start = lineno - else: # still on a multiline - modified = u'%s %s' % (continuation, line) - output.append(modified) - try: - mod = ast.parse( - '\n'.join(content[multiline_start:lineno+1])) - if isinstance(mod.body[0], ast.FunctionDef): - # check to see if we have the whole function - for element in mod.body[0].body: - if isinstance(element, ast.Return): - multiline = False - else: - output.append(u'') - multiline = False - except Exception: - pass - - if savefig: # clear figure if plotted - self.ensure_pyplot() - self.process_input_line('plt.clf()', store_history=False) - self.clear_cout() - savefig = False - - return output - -class IpythonDirective(Directive): - - has_content = True - required_arguments = 0 - optional_arguments = 4 # python, suppress, verbatim, doctest - final_argumuent_whitespace = True - option_spec = { 'python': directives.unchanged, - 'suppress' : directives.flag, - 'verbatim' : directives.flag, - 'doctest' : directives.flag, - } - - shell = EmbeddedSphinxShell() - - def get_config_options(self): - # contains sphinx configuration variables - config = self.state.document.settings.env.config - - # get config variables to set figure output directory - confdir = self.state.document.settings.env.app.confdir - savefig_dir = config.ipython_savefig_dir - source_dir = os.path.dirname(self.state.document.current_source) - if savefig_dir is None: - savefig_dir = config.html_static_path - if isinstance(savefig_dir, list): - savefig_dir = savefig_dir[0] # safe to assume only one path? - savefig_dir = os.path.join(confdir, savefig_dir) - - # get regex and prompt stuff - rgxin = config.ipython_rgxin - rgxout = config.ipython_rgxout - promptin = config.ipython_promptin - promptout = config.ipython_promptout - - return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout - - def setup(self): - # reset the execution count if we haven't processed this doc - #NOTE: this may be borked if there are multiple seen_doc tmp files - #check time stamp? - seen_docs = [i for i in os.listdir(tempfile.tempdir) - if i.startswith('seen_doc')] - if seen_docs: - fname = os.path.join(tempfile.tempdir, seen_docs[0]) - docs = open(fname).read().split('\n') - if not self.state.document.current_source in docs: - self.shell.IP.history_manager.reset() - self.shell.IP.execution_count = 1 - else: # haven't processed any docs yet - docs = [] - - - # get config values - (savefig_dir, source_dir, rgxin, - rgxout, promptin, promptout) = self.get_config_options() - - # and attach to shell so we don't have to pass them around - self.shell.rgxin = rgxin - self.shell.rgxout = rgxout - self.shell.promptin = promptin - self.shell.promptout = promptout - self.shell.savefig_dir = savefig_dir - self.shell.source_dir = source_dir - - # setup bookmark for saving figures directory - - self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir, - store_history=False) - self.shell.clear_cout() - - # write the filename to a tempfile because it's been "seen" now - if not self.state.document.current_source in docs: - fd, fname = tempfile.mkstemp(prefix="seen_doc", text=True) - fout = open(fname, 'a') - fout.write(self.state.document.current_source+'\n') - fout.close() - - return rgxin, rgxout, promptin, promptout - - - def teardown(self): - # delete last bookmark - self.shell.process_input_line('bookmark -d ipy_savedir', - store_history=False) - self.shell.clear_cout() - - def run(self): - debug = False - - #TODO, any reason block_parser can't be a method of embeddable shell - # then we wouldn't have to carry these around - rgxin, rgxout, promptin, promptout = self.setup() - - options = self.options - self.shell.is_suppress = 'suppress' in options - self.shell.is_doctest = 'doctest' in options - self.shell.is_verbatim = 'verbatim' in options - - - # handle pure python code - if 'python' in self.arguments: - content = self.content - self.content = self.shell.process_pure_python(content) - - parts = '\n'.join(self.content).split('\n\n') - - lines = ['.. code-block:: ipython',''] - figures = [] - - for part in parts: - - block = block_parser(part, rgxin, rgxout, promptin, promptout) - - if len(block): - rows, figure = self.shell.process_block(block) - for row in rows: - lines.extend([' %s'%line for line in row.split('\n')]) - - if figure is not None: - figures.append(figure) - - #text = '\n'.join(lines) - #figs = '\n'.join(figures) - - for figure in figures: - lines.append('') - lines.extend(figure.split('\n')) - lines.append('') - - #print lines - if len(lines)>2: - if debug: - print '\n'.join(lines) - else: #NOTE: this raises some errors, what's it for? - #print 'INSERTING %d lines'%len(lines) - self.state_machine.insert_input( - lines, self.state_machine.input_lines.source(0)) - - text = '\n'.join(lines) - txtnode = nodes.literal_block(text, text) - txtnode['language'] = 'ipython' - #imgnode = nodes.image(figs) - - # cleanup - self.teardown() - - return []#, imgnode] - -# Enable as a proper Sphinx directive -def setup(app): - setup.app = app - - app.add_directive('ipython', IpythonDirective) - app.add_config_value('ipython_savefig_dir', None, True) - app.add_config_value('ipython_rgxin', - re.compile('In \[(\d+)\]:\s?(.*)\s*'), True) - app.add_config_value('ipython_rgxout', - re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True) - app.add_config_value('ipython_promptin', 'In [%d]:', True) - app.add_config_value('ipython_promptout', 'Out[%d]:', True) - - -# Simple smoke test, needs to be converted to a proper automatic test. -def test(): - - examples = [ - r""" -In [9]: pwd -Out[9]: '/home/jdhunter/py4science/book' - -In [10]: cd bookdata/ -/home/jdhunter/py4science/book/bookdata - -In [2]: from pylab import * - -In [2]: ion() - -In [3]: im = imread('stinkbug.png') - -@savefig mystinkbug.png width=4in -In [4]: imshow(im) -Out[4]: - -""", - r""" - -In [1]: x = 'hello world' - -# string methods can be -# used to alter the string -@doctest -In [2]: x.upper() -Out[2]: 'HELLO WORLD' - -@verbatim -In [3]: x.st -x.startswith x.strip -""", - r""" - -In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\ - .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv' - -In [131]: print url.split('&') -['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv'] - -In [60]: import urllib - -""", - r"""\ - -In [133]: import numpy.random - -@suppress -In [134]: numpy.random.seed(2358) - -@doctest -In [135]: numpy.random.rand(10,2) -Out[135]: -array([[ 0.64524308, 0.59943846], - [ 0.47102322, 0.8715456 ], - [ 0.29370834, 0.74776844], - [ 0.99539577, 0.1313423 ], - [ 0.16250302, 0.21103583], - [ 0.81626524, 0.1312433 ], - [ 0.67338089, 0.72302393], - [ 0.7566368 , 0.07033696], - [ 0.22591016, 0.77731835], - [ 0.0072729 , 0.34273127]]) - -""", - - r""" -In [106]: print x -jdh - -In [109]: for i in range(10): - .....: print i - .....: - .....: -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -""", - - r""" - -In [144]: from pylab import * - -In [145]: ion() - -# use a semicolon to suppress the output -@savefig test_hist.png width=4in -In [151]: hist(np.random.randn(10000), 100); - - -@savefig test_plot.png width=4in -In [151]: plot(np.random.randn(10000), 'o'); - """, - - r""" -# use a semicolon to suppress the output -In [151]: plt.clf() - -@savefig plot_simple.png width=4in -In [151]: plot([1,2,3]) - -@savefig hist_simple.png width=4in -In [151]: hist(np.random.randn(10000), 100); - -""", - r""" -# update the current fig -In [151]: ylabel('number') - -In [152]: title('normal distribution') - - -@savefig hist_with_text.png -In [153]: grid(True) - - """, - ] - # skip local-file depending first example: - examples = examples[1:] - - #ipython_directive.DEBUG = True # dbg - #options = dict(suppress=True) # dbg - options = dict() - for example in examples: - content = example.split('\n') - ipython_directive('debug', arguments=None, options=options, - content=content, lineno=0, - content_offset=None, block_text=None, - state=None, state_machine=None, - ) - -# Run test suite as a script -if __name__=='__main__': - if not os.path.isdir('_static'): - os.mkdir('_static') - test() - print 'All OK? Check figures in _static/' diff --git a/doc/sphinxext/mathmpl.py b/doc/sphinxext/mathmpl.py deleted file mode 100644 index 0c126a66..00000000 --- a/doc/sphinxext/mathmpl.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import print_function -import os -import sys -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - -from docutils import nodes -from docutils.parsers.rst import directives -import warnings - -from matplotlib import rcParams -from matplotlib.mathtext import MathTextParser -rcParams['mathtext.fontset'] = 'cm' -mathtext_parser = MathTextParser("Bitmap") - -# Define LaTeX math node: -class latex_math(nodes.General, nodes.Element): - pass - -def fontset_choice(arg): - return directives.choice(arg, ['cm', 'stix', 'stixsans']) - -options_spec = {'fontset': fontset_choice} - -def math_role(role, rawtext, text, lineno, inliner, - options={}, content=[]): - i = rawtext.find('`') - latex = rawtext[i+1:-1] - node = latex_math(rawtext) - node['latex'] = latex - node['fontset'] = options.get('fontset', 'cm') - return [node], [] -math_role.options = options_spec - -def math_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - latex = ''.join(content) - node = latex_math(block_text) - node['latex'] = latex - node['fontset'] = options.get('fontset', 'cm') - return [node] - -# This uses mathtext to render the expression -def latex2png(latex, filename, fontset='cm'): - latex = "$%s$" % latex - orig_fontset = rcParams['mathtext.fontset'] - rcParams['mathtext.fontset'] = fontset - if os.path.exists(filename): - depth = mathtext_parser.get_depth(latex, dpi=100) - else: - try: - depth = mathtext_parser.to_png(filename, latex, dpi=100) - except: - warnings.warn("Could not render math expression %s" % latex, - Warning) - depth = 0 - rcParams['mathtext.fontset'] = orig_fontset - sys.stdout.write("#") - sys.stdout.flush() - return depth - -# LaTeX to HTML translation stuff: -def latex2html(node, source): - inline = isinstance(node.parent, nodes.TextElement) - latex = node['latex'] - name = 'math-%s' % md5(latex).hexdigest()[-10:] - - destdir = os.path.join(setup.app.builder.outdir, '_images', 'mathmpl') - if not os.path.exists(destdir): - os.makedirs(destdir) - dest = os.path.join(destdir, '%s.png' % name) - path = os.path.join(setup.app.builder.imgpath, 'mathmpl') - - depth = latex2png(latex, dest, node['fontset']) - - if inline: - cls = '' - else: - cls = 'class="center" ' - if inline and depth != 0: - style = 'style="position: relative; bottom: -%dpx"' % (depth + 1) - else: - style = '' - - return '' % (path, name, cls, style) - -def setup(app): - setup.app = app - - app.add_node(latex_math) - app.add_role('math', math_role) - - # Add visit/depart methods to HTML-Translator: - def visit_latex_math_html(self, node): - source = self.document.attributes['source'] - self.body.append(latex2html(node, source)) - def depart_latex_math_html(self, node): - pass - - # Add visit/depart methods to LaTeX-Translator: - def visit_latex_math_latex(self, node): - inline = isinstance(node.parent, nodes.TextElement) - if inline: - self.body.append('$%s$' % node['latex']) - else: - self.body.extend(['\\begin{equation}', - node['latex'], - '\\end{equation}']) - def depart_latex_math_latex(self, node): - pass - - app.add_node(latex_math, html=(visit_latex_math_html, - depart_latex_math_html)) - app.add_node(latex_math, latex=(visit_latex_math_latex, - depart_latex_math_latex)) - app.add_role('math', math_role) - app.add_directive('math', math_directive, - True, (0, 0, 0), **options_spec) diff --git a/doc/sphinxext/numpydoc.py b/doc/sphinxext/numpydoc.py deleted file mode 100644 index ff6c44c5..00000000 --- a/doc/sphinxext/numpydoc.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -======== -numpydoc -======== - -Sphinx extension that handles docstrings in the Numpy standard format. [1] - -It will: - -- Convert Parameters etc. sections to field lists. -- Convert See Also section to a See also entry. -- Renumber references. -- Extract the signature from the docstring, if it can't be determined otherwise. - -.. [1] http://projects.scipy.org/scipy/numpy/wiki/CodingStyleGuidelines#docstring-standard - -""" - -import os, re, pydoc -from docscrape_sphinx import get_doc_object, SphinxDocString -import inspect - -def mangle_docstrings(app, what, name, obj, options, lines, - reference_offset=[0]): - if what == 'module': - # Strip top title - title_re = re.compile(r'^\s*[#*=]{4,}\n[a-z0-9 -]+\n[#*=]{4,}\s*', - re.I|re.S) - lines[:] = title_re.sub('', "\n".join(lines)).split("\n") - else: - doc = get_doc_object(obj, what, "\n".join(lines)) - lines[:] = str(doc).split("\n") - - if app.config.numpydoc_edit_link and hasattr(obj, '__name__') and \ - obj.__name__: - if hasattr(obj, '__module__'): - v = dict(full_name="%s.%s" % (obj.__module__, obj.__name__)) - else: - v = dict(full_name=obj.__name__) - lines += ['', '.. htmlonly::', ''] - lines += [' %s' % x for x in - (app.config.numpydoc_edit_link % v).split("\n")] - - # replace reference numbers so that there are no duplicates - references = [] - for l in lines: - l = l.strip() - if l.startswith('.. ['): - try: - references.append(int(l[len('.. ['):l.index(']')])) - except ValueError: - print "WARNING: invalid reference in %s docstring" % name - - # Start renaming from the biggest number, otherwise we may - # overwrite references. - references.sort() - if references: - for i, line in enumerate(lines): - for r in references: - new_r = reference_offset[0] + r - lines[i] = lines[i].replace('[%d]_' % r, - '[%d]_' % new_r) - lines[i] = lines[i].replace('.. [%d]' % r, - '.. [%d]' % new_r) - - reference_offset[0] += len(references) - -def mangle_signature(app, what, name, obj, options, sig, retann): - # Do not try to inspect classes that don't define `__init__` - if (inspect.isclass(obj) and - 'initializes x; see ' in pydoc.getdoc(obj.__init__)): - return '', '' - - if not (callable(obj) or hasattr(obj, '__argspec_is_invalid_')): return - if not hasattr(obj, '__doc__'): return - - doc = SphinxDocString(pydoc.getdoc(obj)) - if doc['Signature']: - sig = re.sub("^[^(]*", "", doc['Signature']) - return sig, '' - -def initialize(app): - try: - app.connect('autodoc-process-signature', mangle_signature) - except: - monkeypatch_sphinx_ext_autodoc() - -def setup(app, get_doc_object_=get_doc_object): - global get_doc_object - get_doc_object = get_doc_object_ - - app.connect('autodoc-process-docstring', mangle_docstrings) - app.connect('builder-inited', initialize) - app.add_config_value('numpydoc_edit_link', None, True) - -#------------------------------------------------------------------------------ -# Monkeypatch sphinx.ext.autodoc to accept argspecless autodocs (Sphinx < 0.5) -#------------------------------------------------------------------------------ - -def monkeypatch_sphinx_ext_autodoc(): - global _original_format_signature - import sphinx.ext.autodoc - - if sphinx.ext.autodoc.format_signature is our_format_signature: - return - - print "[numpydoc] Monkeypatching sphinx.ext.autodoc ..." - _original_format_signature = sphinx.ext.autodoc.format_signature - sphinx.ext.autodoc.format_signature = our_format_signature - -def our_format_signature(what, obj): - r = mangle_signature(None, what, None, obj, None, None, None) - if r is not None: - return r[0] - else: - return _original_format_signature(what, obj) diff --git a/doc/sphinxext/only_directives.py b/doc/sphinxext/only_directives.py deleted file mode 100644 index 9d8d0bb0..00000000 --- a/doc/sphinxext/only_directives.py +++ /dev/null @@ -1,64 +0,0 @@ -# -# A pair of directives for inserting content that will only appear in -# either html or latex. -# - -from __future__ import print_function -from docutils.nodes import Body, Element -from docutils.parsers.rst import directives - -class only_base(Body, Element): - def dont_traverse(self, *args, **kwargs): - return [] - -class html_only(only_base): - pass - -class latex_only(only_base): - pass - -def run(content, node_class, state, content_offset): - text = '\n'.join(content) - node = node_class(text) - state.nested_parse(content, content_offset, node) - return [node] - -def html_only_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - return run(content, html_only, state, content_offset) - -def latex_only_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - return run(content, latex_only, state, content_offset) - -def builder_inited(app): - if app.builder.name == 'html': - latex_only.traverse = only_base.dont_traverse - else: - html_only.traverse = only_base.dont_traverse - -def setup(app): - app.add_directive('htmlonly', html_only_directive, True, (0, 0, 0)) - app.add_directive('latexonly', latex_only_directive, True, (0, 0, 0)) - app.add_node(html_only) - app.add_node(latex_only) - - # This will *really* never see the light of day As it turns out, - # this results in "broken" image nodes since they never get - # processed, so best not to do this. - # app.connect('builder-inited', builder_inited) - - # Add visit/depart methods to HTML-Translator: - def visit_perform(self, node): - pass - def depart_perform(self, node): - pass - def visit_ignore(self, node): - node.children = [] - def depart_ignore(self, node): - node.children = [] - - app.add_node(html_only, html=(visit_perform, depart_perform)) - app.add_node(html_only, latex=(visit_ignore, depart_ignore)) - app.add_node(latex_only, latex=(visit_perform, depart_perform)) - app.add_node(latex_only, html=(visit_ignore, depart_ignore)) diff --git a/doc/sphinxext/plot_directive.py b/doc/sphinxext/plot_directive.py deleted file mode 100644 index ac96d5fa..00000000 --- a/doc/sphinxext/plot_directive.py +++ /dev/null @@ -1,819 +0,0 @@ -""" -A directive for including a matplotlib plot in a Sphinx document. - -By default, in HTML output, `plot` will include a .png file with a -link to a high-res .png and .pdf. In LaTeX output, it will include a -.pdf. - -The source code for the plot may be included in one of three ways: - - 1. **A path to a source file** as the argument to the directive:: - - .. plot:: path/to/plot.py - - When a path to a source file is given, the content of the - directive may optionally contain a caption for the plot:: - - .. plot:: path/to/plot.py - - This is the caption for the plot - - Additionally, one my specify the name of a function to call (with - no arguments) immediately after importing the module:: - - .. plot:: path/to/plot.py plot_function1 - - 2. Included as **inline content** to the directive:: - - .. plot:: - - import matplotlib.pyplot as plt - import matplotlib.image as mpimg - import numpy as np - img = mpimg.imread('_static/stinkbug.png') - imgplot = plt.imshow(img) - - 3. Using **doctest** syntax:: - - .. plot:: - A plotting example: - >>> import matplotlib.pyplot as plt - >>> plt.plot([1,2,3], [4,5,6]) - -Options -------- - -The ``plot`` directive supports the following options: - - format : {'python', 'doctest'} - Specify the format of the input - - include-source : bool - Whether to display the source code. The default can be changed - using the `plot_include_source` variable in conf.py - - encoding : str - If this source file is in a non-UTF8 or non-ASCII encoding, - the encoding must be specified using the `:encoding:` option. - The encoding will not be inferred using the ``-*- coding -*-`` - metacomment. - - context : bool - If provided, the code will be run in the context of all - previous plot directives for which the `:context:` option was - specified. This only applies to inline code plot directives, - not those run from files. - - nofigs : bool - If specified, the code block will be run, but no figures will - be inserted. This is usually useful with the ``:context:`` - option. - -Additionally, this directive supports all of the options of the -`image` directive, except for `target` (since plot will add its own -target). These include `alt`, `height`, `width`, `scale`, `align` and -`class`. - -Configuration options ---------------------- - -The plot directive has the following configuration options: - - plot_include_source - Default value for the include-source option - - plot_pre_code - Code that should be executed before each plot. - - plot_basedir - Base directory, to which ``plot::`` file names are relative - to. (If None or empty, file names are relative to the - directoly where the file containing the directive is.) - - plot_formats - File formats to generate. List of tuples or strings:: - - [(suffix, dpi), suffix, ...] - - that determine the file format and the DPI. For entries whose - DPI was omitted, sensible defaults are chosen. - - plot_html_show_formats - Whether to show links to the files in HTML. - - plot_rcparams - A dictionary containing any non-standard rcParams that should - be applied before each plot. - - plot_apply_rcparams - By default, rcParams are applied when `context` option is not used in - a plot directive. This configuration option overrides this behaviour - and applies rcParams before each plot. - - plot_working_directory - By default, the working directory will be changed to the directory of - the example, so the code can get at its data files, if any. Also its - path will be added to `sys.path` so it can import any helper modules - sitting beside it. This configuration option can be used to specify - a central directory (also added to `sys.path`) where data files and - helper modules for all code are located. - - plot_template - Provide a customized template for preparing resturctured text. - - -""" -from __future__ import print_function - -import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap, \ - traceback, exceptions - -from docutils.parsers.rst import directives -from docutils import nodes -from docutils.parsers.rst.directives.images import Image -align = Image.align -import sphinx - -sphinx_version = sphinx.__version__.split(".") -# The split is necessary for sphinx beta versions where the string is -# '6b1' -sphinx_version = tuple([int(re.split('[a-z]', x)[0]) - for x in sphinx_version[:2]]) - -try: - # Sphinx depends on either Jinja or Jinja2 - import jinja2 - def format_template(template, **kw): - return jinja2.Template(template).render(**kw) -except ImportError: - import jinja - def format_template(template, **kw): - return jinja.from_string(template, **kw) - -import matplotlib -import matplotlib.cbook as cbook -matplotlib.use('Agg') -import matplotlib.pyplot as plt -from matplotlib import _pylab_helpers - -__version__ = 2 - -#------------------------------------------------------------------------------ -# Relative pathnames -#------------------------------------------------------------------------------ - -# os.path.relpath is new in Python 2.6 -try: - from os.path import relpath -except ImportError: - # Copied from Python 2.7 - if 'posix' in sys.builtin_module_names: - def relpath(path, start=os.path.curdir): - """Return a relative version of a path""" - from os.path import sep, curdir, join, abspath, commonprefix, \ - pardir - - if not path: - raise ValueError("no path specified") - - start_list = abspath(start).split(sep) - path_list = abspath(path).split(sep) - - # Work out how much of the filepath is shared by start and path. - i = len(commonprefix([start_list, path_list])) - - rel_list = [pardir] * (len(start_list)-i) + path_list[i:] - if not rel_list: - return curdir - return join(*rel_list) - elif 'nt' in sys.builtin_module_names: - def relpath(path, start=os.path.curdir): - """Return a relative version of a path""" - from os.path import sep, curdir, join, abspath, commonprefix, \ - pardir, splitunc - - if not path: - raise ValueError("no path specified") - start_list = abspath(start).split(sep) - path_list = abspath(path).split(sep) - if start_list[0].lower() != path_list[0].lower(): - unc_path, rest = splitunc(path) - unc_start, rest = splitunc(start) - if bool(unc_path) ^ bool(unc_start): - raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" - % (path, start)) - else: - raise ValueError("path is on drive %s, start on drive %s" - % (path_list[0], start_list[0])) - # Work out how much of the filepath is shared by start and path. - for i in range(min(len(start_list), len(path_list))): - if start_list[i].lower() != path_list[i].lower(): - break - else: - i += 1 - - rel_list = [pardir] * (len(start_list)-i) + path_list[i:] - if not rel_list: - return curdir - return join(*rel_list) - else: - raise RuntimeError("Unsupported platform (no relpath available!)") - -#------------------------------------------------------------------------------ -# Registration hook -#------------------------------------------------------------------------------ - -def plot_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - return run(arguments, content, options, state_machine, state, lineno) -plot_directive.__doc__ = __doc__ - -def _option_boolean(arg): - if not arg or not arg.strip(): - # no argument given, assume used as a flag - return True - elif arg.strip().lower() in ('no', '0', 'false'): - return False - elif arg.strip().lower() in ('yes', '1', 'true'): - return True - else: - raise ValueError('"%s" unknown boolean' % arg) - -def _option_format(arg): - return directives.choice(arg, ('python', 'doctest')) - -def _option_align(arg): - return directives.choice(arg, ("top", "middle", "bottom", "left", "center", - "right")) - -def mark_plot_labels(app, document): - """ - To make plots referenceable, we need to move the reference from - the "htmlonly" (or "latexonly") node to the actual figure node - itself. - """ - for name, explicit in document.nametypes.iteritems(): - if not explicit: - continue - labelid = document.nameids[name] - if labelid is None: - continue - node = document.ids[labelid] - if node.tagname in ('html_only', 'latex_only'): - for n in node: - if n.tagname == 'figure': - sectname = name - for c in n: - if c.tagname == 'caption': - sectname = c.astext() - break - - node['ids'].remove(labelid) - node['names'].remove(name) - n['ids'].append(labelid) - n['names'].append(name) - document.settings.env.labels[name] = \ - document.settings.env.docname, labelid, sectname - break - -def setup(app): - setup.app = app - setup.config = app.config - setup.confdir = app.confdir - - options = {'alt': directives.unchanged, - 'height': directives.length_or_unitless, - 'width': directives.length_or_percentage_or_unitless, - 'scale': directives.nonnegative_int, - 'align': _option_align, - 'class': directives.class_option, - 'include-source': _option_boolean, - 'format': _option_format, - 'context': directives.flag, - 'nofigs': directives.flag, - 'encoding': directives.encoding - } - - app.add_directive('plot', plot_directive, True, (0, 2, False), **options) - app.add_config_value('plot_pre_code', None, True) - app.add_config_value('plot_include_source', False, True) - app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) - app.add_config_value('plot_basedir', None, True) - app.add_config_value('plot_html_show_formats', True, True) - app.add_config_value('plot_rcparams', {}, True) - app.add_config_value('plot_apply_rcparams', False, True) - app.add_config_value('plot_working_directory', None, True) - app.add_config_value('plot_template', None, True) - - app.connect('doctree-read', mark_plot_labels) - -#------------------------------------------------------------------------------ -# Doctest handling -#------------------------------------------------------------------------------ - -def contains_doctest(text): - try: - # check if it's valid Python as-is - compile(text, '', 'exec') - return False - except SyntaxError: - pass - r = re.compile(r'^\s*>>>', re.M) - m = r.search(text) - return bool(m) - -def unescape_doctest(text): - """ - Extract code from a piece of text, which contains either Python code - or doctests. - - """ - if not contains_doctest(text): - return text - - code = "" - for line in text.split("\n"): - m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) - if m: - code += m.group(2) + "\n" - elif line.strip(): - code += "# " + line.strip() + "\n" - else: - code += "\n" - return code - -def split_code_at_show(text): - """ - Split code at plt.show() - - """ - - parts = [] - is_doctest = contains_doctest(text) - - part = [] - for line in text.split("\n"): - if (not is_doctest and line.strip() == 'plt.show()') or \ - (is_doctest and line.strip() == '>>> plt.show()'): - part.append(line) - parts.append("\n".join(part)) - part = [] - else: - part.append(line) - if "\n".join(part).strip(): - parts.append("\n".join(part)) - return parts - -#------------------------------------------------------------------------------ -# Template -#------------------------------------------------------------------------------ - - -TEMPLATE = """ -{{ source_code }} - -{{ only_html }} - - {% if source_link or (html_show_formats and not multi_image) %} - ( - {%- if source_link -%} - `Source code <{{ source_link }}>`__ - {%- endif -%} - {%- if html_show_formats and not multi_image -%} - {%- for img in images -%} - {%- for fmt in img.formats -%} - {%- if source_link or not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - {%- endfor -%} - {%- endif -%} - ) - {% endif %} - - {% for img in images %} - .. figure:: {{ build_dir }}/{{ img.basename }}.png - {%- for option in options %} - {{ option }} - {% endfor %} - - {% if html_show_formats and multi_image -%} - ( - {%- for fmt in img.formats -%} - {%- if not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - ) - {%- endif -%} - - {{ caption }} - {% endfor %} - -{{ only_latex }} - - {% for img in images %} - .. image:: {{ build_dir }}/{{ img.basename }}.pdf - {% endfor %} - -{{ only_texinfo }} - - {% for img in images %} - .. image:: {{ build_dir }}/{{ img.basename }}.png - {%- for option in options %} - {{ option }} - {% endfor %} - - {% endfor %} - -""" - -exception_template = """ -.. htmlonly:: - - [`source code <%(linkdir)s/%(basename)s.py>`__] - -Exception occurred rendering plot. - -""" - -# the context of the plot for all directives specified with the -# :context: option -plot_context = dict() - -class ImageFile(object): - def __init__(self, basename, dirname): - self.basename = basename - self.dirname = dirname - self.formats = [] - - def filename(self, format): - return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) - - def filenames(self): - return [self.filename(fmt) for fmt in self.formats] - -def out_of_date(original, derived): - """ - Returns True if derivative is out-of-date wrt original, - both of which are full file paths. - """ - return (not os.path.exists(derived) or - (os.path.exists(original) and - os.stat(derived).st_mtime < os.stat(original).st_mtime)) - -class PlotError(RuntimeError): - pass - -def run_code(code, code_path, ns=None, function_name=None): - """ - Import a Python module from a path, and run the function given by - name, if function_name is not None. - """ - - # Change the working directory to the directory of the example, so - # it can get at its data files, if any. Add its path to sys.path - # so it can import any helper modules sitting beside it. - - pwd = os.getcwd() - old_sys_path = list(sys.path) - if setup.config.plot_working_directory is not None: - try: - os.chdir(setup.config.plot_working_directory) - except OSError as err: - raise OSError(str(err) + '\n`plot_working_directory` option in' - 'Sphinx configuration file must be a valid ' - 'directory path') - except TypeError as err: - raise TypeError(str(err) + '\n`plot_working_directory` option in ' - 'Sphinx configuration file must be a string or ' - 'None') - sys.path.insert(0, setup.config.plot_working_directory) - elif code_path is not None: - dirname = os.path.abspath(os.path.dirname(code_path)) - os.chdir(dirname) - sys.path.insert(0, dirname) - - # Redirect stdout - stdout = sys.stdout - sys.stdout = cStringIO.StringIO() - - # Reset sys.argv - old_sys_argv = sys.argv - sys.argv = [code_path] - - try: - try: - code = unescape_doctest(code) - if ns is None: - ns = {} - if not ns: - if setup.config.plot_pre_code is None: - exec "import numpy as np\nfrom matplotlib import pyplot as plt\n" in ns - else: - exec setup.config.plot_pre_code in ns - if "__main__" in code: - exec "__name__ = '__main__'" in ns - exec code in ns - if function_name is not None: - exec function_name + "()" in ns - except (Exception, SystemExit), err: - raise PlotError(traceback.format_exc()) - finally: - os.chdir(pwd) - sys.argv = old_sys_argv - sys.path[:] = old_sys_path - sys.stdout = stdout - return ns - -def clear_state(plot_rcparams): - plt.close('all') - matplotlib.rc_file_defaults() - matplotlib.rcParams.update(plot_rcparams) - -def render_figures(code, code_path, output_dir, output_base, context, - function_name, config): - """ - Run a pyplot script and save the low and high res PNGs and a PDF - in outdir. - - Save the images under *output_dir* with file names derived from - *output_base* - """ - # -- Parse format list - default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} - formats = [] - plot_formats = config.plot_formats - if isinstance(plot_formats, (str, unicode)): - plot_formats = eval(plot_formats) - for fmt in plot_formats: - if isinstance(fmt, str): - formats.append((fmt, default_dpi.get(fmt, 80))) - elif type(fmt) in (tuple, list) and len(fmt)==2: - formats.append((str(fmt[0]), int(fmt[1]))) - else: - raise PlotError('invalid image format "%r" in plot_formats' % fmt) - - # -- Try to determine if all images already exist - - code_pieces = split_code_at_show(code) - - # Look for single-figure output files first - # Look for single-figure output files first - all_exists = True - img = ImageFile(output_base, output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - if all_exists: - return [(code, [img])] - - # Then look for multi-figure output files - results = [] - all_exists = True - for i, code_piece in enumerate(code_pieces): - images = [] - for j in xrange(1000): - if len(code_pieces) > 1: - img = ImageFile('%s_%02d_%02d' % (output_base, i, j), output_dir) - else: - img = ImageFile('%s_%02d' % (output_base, j), output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - # assume that if we have one, we have them all - if not all_exists: - all_exists = (j > 0) - break - images.append(img) - if not all_exists: - break - results.append((code_piece, images)) - - if all_exists: - return results - - # We didn't find the files, so build them - - results = [] - if context: - ns = plot_context - else: - ns = {} - - for i, code_piece in enumerate(code_pieces): - if not context or config.plot_apply_rcparams: - clear_state(config.plot_rcparams) - run_code(code_piece, code_path, ns, function_name) - - images = [] - fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() - for j, figman in enumerate(fig_managers): - if len(fig_managers) == 1 and len(code_pieces) == 1: - img = ImageFile(output_base, output_dir) - elif len(code_pieces) == 1: - img = ImageFile("%s_%02d" % (output_base, j), output_dir) - else: - img = ImageFile("%s_%02d_%02d" % (output_base, i, j), - output_dir) - images.append(img) - for format, dpi in formats: - try: - figman.canvas.figure.savefig(img.filename(format), dpi=dpi) - except Exception,err: - raise PlotError(traceback.format_exc()) - img.formats.append(format) - - results.append((code_piece, images)) - - if not context or config.plot_apply_rcparams: - clear_state(config.plot_rcparams) - - return results - -def run(arguments, content, options, state_machine, state, lineno): - # The user may provide a filename *or* Python code content, but not both - if arguments and content: - raise RuntimeError("plot:: directive can't have both args and content") - - document = state_machine.document - config = document.settings.env.config - nofigs = options.has_key('nofigs') - - options.setdefault('include-source', config.plot_include_source) - context = options.has_key('context') - - rst_file = document.attributes['source'] - rst_dir = os.path.dirname(rst_file) - - if len(arguments): - if not config.plot_basedir: - source_file_name = os.path.join(setup.app.builder.srcdir, - directives.uri(arguments[0])) - else: - source_file_name = os.path.join(setup.confdir, config.plot_basedir, - directives.uri(arguments[0])) - - # If there is content, it will be passed as a caption. - caption = '\n'.join(content) - - # If the optional function name is provided, use it - if len(arguments) == 2: - function_name = arguments[1] - else: - function_name = None - - with open(source_file_name, 'r') as fd: - code = fd.read() - output_base = os.path.basename(source_file_name) - else: - source_file_name = rst_file - code = textwrap.dedent("\n".join(map(str, content))) - counter = document.attributes.get('_plot_counter', 0) + 1 - document.attributes['_plot_counter'] = counter - base, ext = os.path.splitext(os.path.basename(source_file_name)) - output_base = '%s-%d.py' % (base, counter) - function_name = None - caption = '' - - base, source_ext = os.path.splitext(output_base) - if source_ext in ('.py', '.rst', '.txt'): - output_base = base - else: - source_ext = '' - - # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames - output_base = output_base.replace('.', '-') - - # is it in doctest format? - is_doctest = contains_doctest(code) - if options.has_key('format'): - if options['format'] == 'python': - is_doctest = False - else: - is_doctest = True - - # determine output directory name fragment - source_rel_name = relpath(source_file_name, setup.confdir) - source_rel_dir = os.path.dirname(source_rel_name) - while source_rel_dir.startswith(os.path.sep): - source_rel_dir = source_rel_dir[1:] - - # build_dir: where to place output files (temporarily) - build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), - 'plot_directive', - source_rel_dir) - # get rid of .. in paths, also changes pathsep - # see note in Python docs for warning about symbolic links on Windows. - # need to compare source and dest paths at end - build_dir = os.path.normpath(build_dir) - - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # output_dir: final location in the builder's directory - dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, - source_rel_dir)) - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) # no problem here for me, but just use built-ins - - # how to link to files from the RST file - dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), - source_rel_dir).replace(os.path.sep, '/') - build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') - source_link = dest_dir_link + '/' + output_base + source_ext - - # make figures - try: - results = render_figures(code, source_file_name, build_dir, output_base, - context, function_name, config) - errors = [] - except PlotError, err: - reporter = state.memo.reporter - sm = reporter.system_message( - 2, "Exception occurred in plotting %s\n from %s:\n%s" % (output_base, - source_file_name, err), - line=lineno) - results = [(code, [])] - errors = [sm] - - # Properly indent the caption - caption = '\n'.join(' ' + line.strip() - for line in caption.split('\n')) - - # generate output restructuredtext - total_lines = [] - for j, (code_piece, images) in enumerate(results): - if options['include-source']: - if is_doctest: - lines = [''] - lines += [row.rstrip() for row in code_piece.split('\n')] - else: - lines = ['.. code-block:: python', ''] - lines += [' %s' % row.rstrip() - for row in code_piece.split('\n')] - source_code = "\n".join(lines) - else: - source_code = "" - - if nofigs: - images = [] - - opts = [':%s: %s' % (key, val) for key, val in options.items() - if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - - only_html = ".. only:: html" - only_latex = ".. only:: latex" - only_texinfo = ".. only:: texinfo" - - if j == 0: - src_link = source_link - else: - src_link = None - - result = format_template( - config.plot_template or TEMPLATE, - dest_dir=dest_dir_link, - build_dir=build_dir_link, - source_link=src_link, - multi_image=len(images) > 1, - only_html=only_html, - only_latex=only_latex, - only_texinfo=only_texinfo, - options=opts, - images=images, - source_code=source_code, - html_show_formats=config.plot_html_show_formats, - caption=caption) - - total_lines.extend(result.split("\n")) - total_lines.extend("\n") - - if total_lines: - state_machine.insert_input(total_lines, source=source_file_name) - - # copy image files to builder's output directory, if necessary - if not os.path.exists(dest_dir): - cbook.mkdirs(dest_dir) - - for code_piece, images in results: - for img in images: - for fn in img.filenames(): - destimg = os.path.join(dest_dir, os.path.basename(fn)) - if fn != destimg: - shutil.copyfile(fn, destimg) - - # copy script (if necessary) - target_name = os.path.join(dest_dir, output_base + source_ext) - with open(target_name, 'w') as f: - if source_file_name == rst_file: - code_escaped = unescape_doctest(code) - else: - code_escaped = code - f.write(code_escaped) - - return errors diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 3527e86f..417bb1bd 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -1,4 +1,10 @@ +.. ipython:: python + + print "Hello world" + X = [[1, 10], [1, 20], [1, -2]] + + ************************************* Gaussian process regression tutorial ************************************* From 92ead7924bebf44fe8b899f23ee7223713d6bd51 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:27:57 +0000 Subject: [PATCH 129/197] Adding ipython extensions --- doc/sphinxext/ipython_console_highlighting.py | 115 +++ doc/sphinxext/ipython_directive.py | 835 ++++++++++++++++++ 2 files changed, 950 insertions(+) create mode 100644 doc/sphinxext/ipython_console_highlighting.py create mode 100644 doc/sphinxext/ipython_directive.py diff --git a/doc/sphinxext/ipython_console_highlighting.py b/doc/sphinxext/ipython_console_highlighting.py new file mode 100644 index 00000000..f5cced41 --- /dev/null +++ b/doc/sphinxext/ipython_console_highlighting.py @@ -0,0 +1,115 @@ +"""reST directive for syntax-highlighting ipython interactive sessions. + +XXX - See what improvements can be made based on the new (as of Sept 2009) +'pycon' lexer for the python console. At the very least it will give better +highlighted tracebacks. +""" + +#----------------------------------------------------------------------------- +# Needed modules + +# Standard library +import re + +# Third party +from pygments.lexer import Lexer, do_insertions +from pygments.lexers.agile import (PythonConsoleLexer, PythonLexer, + PythonTracebackLexer) +from pygments.token import Comment, Generic + +from sphinx import highlighting + +#----------------------------------------------------------------------------- +# Global constants +line_re = re.compile('.*?\n') + +#----------------------------------------------------------------------------- +# Code begins - classes and functions + +class IPythonConsoleLexer(Lexer): + """ + For IPython console output or doctests, such as: + + .. sourcecode:: ipython + + In [1]: a = 'foo' + + In [2]: a + Out[2]: 'foo' + + In [3]: print a + foo + + In [4]: 1 / 0 + + Notes: + + - Tracebacks are not currently supported. + + - It assumes the default IPython prompts, not customized ones. + """ + + name = 'IPython console session' + aliases = ['ipython'] + mimetypes = ['text/x-ipython-console'] + input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") + output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") + continue_prompt = re.compile(" \.\.\.+:") + tb_start = re.compile("\-+") + + def get_tokens_unprocessed(self, text): + pylexer = PythonLexer(**self.options) + tblexer = PythonTracebackLexer(**self.options) + + curcode = '' + insertions = [] + for match in line_re.finditer(text): + line = match.group() + input_prompt = self.input_prompt.match(line) + continue_prompt = self.continue_prompt.match(line.rstrip()) + output_prompt = self.output_prompt.match(line) + if line.startswith("#"): + insertions.append((len(curcode), + [(0, Comment, line)])) + elif input_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, input_prompt.group())])) + curcode += line[input_prompt.end():] + elif continue_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, continue_prompt.group())])) + curcode += line[continue_prompt.end():] + elif output_prompt is not None: + # Use the 'error' token for output. We should probably make + # our own token, but error is typicaly in a bright color like + # red, so it works fine for our output prompts. + insertions.append((len(curcode), + [(0, Generic.Error, output_prompt.group())])) + curcode += line[output_prompt.end():] + else: + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + curcode = '' + insertions = [] + yield match.start(), Generic.Output, line + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + + +def setup(app): + """Setup as a sphinx extension.""" + + # This is only a lexer, so adding it below to pygments appears sufficient. + # But if somebody knows that the right API usage should be to do that via + # sphinx, by all means fix it here. At least having this setup.py + # suppresses the sphinx warning we'd get without it. + pass + +#----------------------------------------------------------------------------- +# Register the extension as a valid pygments lexer +highlighting.lexers['ipython'] = IPythonConsoleLexer() + diff --git a/doc/sphinxext/ipython_directive.py b/doc/sphinxext/ipython_directive.py new file mode 100644 index 00000000..2c2696c1 --- /dev/null +++ b/doc/sphinxext/ipython_directive.py @@ -0,0 +1,835 @@ +# -*- coding: utf-8 -*- +"""Sphinx directive to support embedded IPython code. + +This directive allows pasting of entire interactive IPython sessions, prompts +and all, and their code will actually get re-executed at doc build time, with +all prompts renumbered sequentially. It also allows you to input code as a pure +python input by giving the argument python to the directive. The output looks +like an interactive ipython section. + +To enable this directive, simply list it in your Sphinx ``conf.py`` file +(making sure the directory where you placed it is visible to sphinx, as is +needed for all Sphinx directives). + +By default this directive assumes that your prompts are unchanged IPython ones, +but this can be customized. The configurable options that can be placed in +conf.py are + +ipython_savefig_dir: + The directory in which to save the figures. This is relative to the + Sphinx source directory. The default is `html_static_path`. +ipython_rgxin: + The compiled regular expression to denote the start of IPython input + lines. The default is re.compile('In \[(\d+)\]:\s?(.*)\s*'). You + shouldn't need to change this. +ipython_rgxout: + The compiled regular expression to denote the start of IPython output + lines. The default is re.compile('Out\[(\d+)\]:\s?(.*)\s*'). You + shouldn't need to change this. +ipython_promptin: + The string to represent the IPython input prompt in the generated ReST. + The default is 'In [%d]:'. This expects that the line numbers are used + in the prompt. +ipython_promptout: + + The string to represent the IPython prompt in the generated ReST. The + default is 'Out [%d]:'. This expects that the line numbers are used + in the prompt. + +ToDo +---- + +- Turn the ad-hoc test() function into a real test suite. +- Break up ipython-specific functionality from matplotlib stuff into better + separated code. + +Authors +------- + +- John D Hunter: orignal author. +- Fernando Perez: refactoring, documentation, cleanups, port to 0.11. +- VáclavŠmilauer : Prompt generalizations. +- Skipper Seabold, refactoring, cleanups, pure python addition +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Stdlib +import cStringIO +import os +import re +import sys +import tempfile +import ast + +# To keep compatibility with various python versions +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +# Third-party +try: + import matplotlib + matplotlib.use('Agg') +except ImportError: + print "Couldn't find matplotlib" + +import sphinx +from docutils.parsers.rst import directives +from docutils import nodes +from sphinx.util.compat import Directive + +# Our own +from IPython import Config, InteractiveShell +from IPython.core.profiledir import ProfileDir +from IPython.utils import io + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +# for tokenizing blocks +COMMENT, INPUT, OUTPUT = range(3) + +#----------------------------------------------------------------------------- +# Functions and class declarations +#----------------------------------------------------------------------------- +def block_parser(part, rgxin, rgxout, fmtin, fmtout): + """ + part is a string of ipython text, comprised of at most one + input, one ouput, comments, and blank lines. The block parser + parses the text into a list of:: + + blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...] + + where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and + data is, depending on the type of token:: + + COMMENT : the comment string + + INPUT: the (DECORATOR, INPUT_LINE, REST) where + DECORATOR: the input decorator (or None) + INPUT_LINE: the input as string (possibly multi-line) + REST : any stdout generated by the input line (not OUTPUT) + + + OUTPUT: the output string, possibly multi-line + """ + + block = [] + lines = part.split('\n') + N = len(lines) + i = 0 + decorator = None + while 1: + + if i==N: + # nothing left to parse -- the last line + break + + line = lines[i] + i += 1 + line_stripped = line.strip() + if line_stripped.startswith('#'): + block.append((COMMENT, line)) + continue + + if line_stripped.startswith('@'): + # we're assuming at most one decorator -- may need to + # rethink + decorator = line_stripped + continue + + # does this look like an input line? + matchin = rgxin.match(line) + if matchin: + lineno, inputline = int(matchin.group(1)), matchin.group(2) + + # the ....: continuation string + continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) + Nc = len(continuation) + # input lines can continue on for more than one line, if + # we have a '\' line continuation char or a function call + # echo line 'print'. The input line can only be + # terminated by the end of the block or an output line, so + # we parse out the rest of the input line if it is + # multiline as well as any echo text + + rest = [] + while i 1: + if input_lines[-1] != "": + input_lines.append('') # make sure there's a blank line + # so splitter buffer gets reset + + continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) + Nc = len(continuation) + + if is_savefig: + image_file, image_directive = self.process_image(decorator) + + ret = [] + is_semicolon = False + + for i, line in enumerate(input_lines): + if line.endswith(';'): + is_semicolon = True + + if i==0: + # process the first input line + if is_verbatim: + self.process_input_line('') + self.IP.execution_count += 1 # increment it anyway + else: + # only submit the line in non-verbatim mode + self.process_input_line(line, store_history=True) + formatted_line = '%s %s'%(input_prompt, line) + else: + # process a continuation line + if not is_verbatim: + self.process_input_line(line, store_history=True) + + formatted_line = '%s %s'%(continuation, line) + + if not is_suppress: + ret.append(formatted_line) + + if not is_suppress and len(rest.strip()) and is_verbatim: + # the "rest" is the standard output of the + # input, which needs to be added in + # verbatim mode + ret.append(rest) + + self.cout.seek(0) + output = self.cout.read() + if not is_suppress and not is_semicolon: + ret.append(output) + elif is_semicolon: # get spacing right + ret.append('') + + self.cout.truncate(0) + return (ret, input_lines, output, is_doctest, image_file, + image_directive) + #print 'OUTPUT', output # dbg + + def process_output(self, data, output_prompt, + input_lines, output, is_doctest, image_file): + """Process data block for OUTPUT token.""" + if is_doctest: + submitted = data.strip() + found = output + if found is not None: + found = found.strip() + + # XXX - fperez: in 0.11, 'output' never comes with the prompt + # in it, just the actual output text. So I think all this code + # can be nuked... + + # the above comment does not appear to be accurate... (minrk) + + ind = found.find(output_prompt) + if ind<0: + e='output prompt="%s" does not match out line=%s' % \ + (output_prompt, found) + raise RuntimeError(e) + found = found[len(output_prompt):].strip() + + if found!=submitted: + e = ('doctest failure for input_lines="%s" with ' + 'found_output="%s" and submitted output="%s"' % + (input_lines, found, submitted) ) + raise RuntimeError(e) + #print 'doctest PASSED for input_lines="%s" with found_output="%s" and submitted output="%s"'%(input_lines, found, submitted) + + def process_comment(self, data): + """Process data fPblock for COMMENT token.""" + if not self.is_suppress: + return [data] + + def save_image(self, image_file): + """ + Saves the image file to disk. + """ + self.ensure_pyplot() + command = 'plt.gcf().savefig("%s")'%image_file + #print 'SAVEFIG', command # dbg + self.process_input_line('bookmark ipy_thisdir', store_history=False) + self.process_input_line('cd -b ipy_savedir', store_history=False) + self.process_input_line(command, store_history=False) + self.process_input_line('cd -b ipy_thisdir', store_history=False) + self.process_input_line('bookmark -d ipy_thisdir', store_history=False) + self.clear_cout() + + + def process_block(self, block): + """ + process block from the block_parser and return a list of processed lines + """ + ret = [] + output = None + input_lines = None + lineno = self.IP.execution_count + + input_prompt = self.promptin%lineno + output_prompt = self.promptout%lineno + image_file = None + image_directive = None + + for token, data in block: + if token==COMMENT: + out_data = self.process_comment(data) + elif token==INPUT: + (out_data, input_lines, output, is_doctest, image_file, + image_directive) = \ + self.process_input(data, input_prompt, lineno) + elif token==OUTPUT: + out_data = \ + self.process_output(data, output_prompt, + input_lines, output, is_doctest, + image_file) + if out_data: + ret.extend(out_data) + + # save the image files + if image_file is not None: + self.save_image(image_file) + + return ret, image_directive + + def ensure_pyplot(self): + if self._pyplot_imported: + return + self.process_input_line('import matplotlib.pyplot as plt', + store_history=False) + + def process_pure_python(self, content): + """ + content is a list of strings. it is unedited directive conent + + This runs it line by line in the InteractiveShell, prepends + prompts as needed capturing stderr and stdout, then returns + the content as a list as if it were ipython code + """ + output = [] + savefig = False # keep up with this to clear figure + multiline = False # to handle line continuation + multiline_start = None + fmtin = self.promptin + + ct = 0 + + for lineno, line in enumerate(content): + + line_stripped = line.strip() + if not len(line): + output.append(line) + continue + + # handle decorators + if line_stripped.startswith('@'): + output.extend([line]) + if 'savefig' in line: + savefig = True # and need to clear figure + continue + + # handle comments + if line_stripped.startswith('#'): + output.extend([line]) + continue + + # deal with lines checking for multiline + continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2)) + if not multiline: + modified = u"%s %s" % (fmtin % ct, line_stripped) + output.append(modified) + ct += 1 + try: + ast.parse(line_stripped) + output.append(u'') + except Exception: # on a multiline + multiline = True + multiline_start = lineno + else: # still on a multiline + modified = u'%s %s' % (continuation, line) + output.append(modified) + try: + mod = ast.parse( + '\n'.join(content[multiline_start:lineno+1])) + if isinstance(mod.body[0], ast.FunctionDef): + # check to see if we have the whole function + for element in mod.body[0].body: + if isinstance(element, ast.Return): + multiline = False + else: + output.append(u'') + multiline = False + except Exception: + pass + + if savefig: # clear figure if plotted + self.ensure_pyplot() + self.process_input_line('plt.clf()', store_history=False) + self.clear_cout() + savefig = False + + return output + +class IpythonDirective(Directive): + + has_content = True + required_arguments = 0 + optional_arguments = 4 # python, suppress, verbatim, doctest + final_argumuent_whitespace = True + option_spec = { 'python': directives.unchanged, + 'suppress' : directives.flag, + 'verbatim' : directives.flag, + 'doctest' : directives.flag, + } + + shell = EmbeddedSphinxShell() + + def get_config_options(self): + # contains sphinx configuration variables + config = self.state.document.settings.env.config + + # get config variables to set figure output directory + confdir = self.state.document.settings.env.app.confdir + savefig_dir = config.ipython_savefig_dir + source_dir = os.path.dirname(self.state.document.current_source) + if savefig_dir is None: + savefig_dir = config.html_static_path + if isinstance(savefig_dir, list): + savefig_dir = savefig_dir[0] # safe to assume only one path? + savefig_dir = os.path.join(confdir, savefig_dir) + + # get regex and prompt stuff + rgxin = config.ipython_rgxin + rgxout = config.ipython_rgxout + promptin = config.ipython_promptin + promptout = config.ipython_promptout + + return savefig_dir, source_dir, rgxin, rgxout, promptin, promptout + + def setup(self): + # reset the execution count if we haven't processed this doc + #NOTE: this may be borked if there are multiple seen_doc tmp files + #check time stamp? + seen_docs = [i for i in os.listdir(tempfile.tempdir) + if i.startswith('seen_doc')] + if seen_docs: + fname = os.path.join(tempfile.tempdir, seen_docs[0]) + docs = open(fname).read().split('\n') + if not self.state.document.current_source in docs: + self.shell.IP.history_manager.reset() + self.shell.IP.execution_count = 1 + else: # haven't processed any docs yet + docs = [] + + + # get config values + (savefig_dir, source_dir, rgxin, + rgxout, promptin, promptout) = self.get_config_options() + + # and attach to shell so we don't have to pass them around + self.shell.rgxin = rgxin + self.shell.rgxout = rgxout + self.shell.promptin = promptin + self.shell.promptout = promptout + self.shell.savefig_dir = savefig_dir + self.shell.source_dir = source_dir + + # setup bookmark for saving figures directory + + self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir, + store_history=False) + self.shell.clear_cout() + + # write the filename to a tempfile because it's been "seen" now + if not self.state.document.current_source in docs: + fd, fname = tempfile.mkstemp(prefix="seen_doc", text=True) + fout = open(fname, 'a') + fout.write(self.state.document.current_source+'\n') + fout.close() + + return rgxin, rgxout, promptin, promptout + + + def teardown(self): + # delete last bookmark + self.shell.process_input_line('bookmark -d ipy_savedir', + store_history=False) + self.shell.clear_cout() + + def run(self): + debug = False + + #TODO, any reason block_parser can't be a method of embeddable shell + # then we wouldn't have to carry these around + rgxin, rgxout, promptin, promptout = self.setup() + + options = self.options + self.shell.is_suppress = 'suppress' in options + self.shell.is_doctest = 'doctest' in options + self.shell.is_verbatim = 'verbatim' in options + + + # handle pure python code + if 'python' in self.arguments: + content = self.content + self.content = self.shell.process_pure_python(content) + + parts = '\n'.join(self.content).split('\n\n') + + lines = ['.. code-block:: ipython',''] + figures = [] + + for part in parts: + + block = block_parser(part, rgxin, rgxout, promptin, promptout) + + if len(block): + rows, figure = self.shell.process_block(block) + for row in rows: + lines.extend([' %s'%line for line in row.split('\n')]) + + if figure is not None: + figures.append(figure) + + #text = '\n'.join(lines) + #figs = '\n'.join(figures) + + for figure in figures: + lines.append('') + lines.extend(figure.split('\n')) + lines.append('') + + #print lines + if len(lines)>2: + if debug: + print '\n'.join(lines) + else: #NOTE: this raises some errors, what's it for? + #print 'INSERTING %d lines'%len(lines) + self.state_machine.insert_input( + lines, self.state_machine.input_lines.source(0)) + + text = '\n'.join(lines) + txtnode = nodes.literal_block(text, text) + txtnode['language'] = 'ipython' + #imgnode = nodes.image(figs) + + # cleanup + self.teardown() + + return []#, imgnode] + +# Enable as a proper Sphinx directive +def setup(app): + setup.app = app + + app.add_directive('ipython', IpythonDirective) + app.add_config_value('ipython_savefig_dir', None, True) + app.add_config_value('ipython_rgxin', + re.compile('In \[(\d+)\]:\s?(.*)\s*'), True) + app.add_config_value('ipython_rgxout', + re.compile('Out\[(\d+)\]:\s?(.*)\s*'), True) + app.add_config_value('ipython_promptin', 'In [%d]:', True) + app.add_config_value('ipython_promptout', 'Out[%d]:', True) + + +# Simple smoke test, needs to be converted to a proper automatic test. +def test(): + + examples = [ + r""" +In [9]: pwd +Out[9]: '/home/jdhunter/py4science/book' + +In [10]: cd bookdata/ +/home/jdhunter/py4science/book/bookdata + +In [2]: from pylab import * + +In [2]: ion() + +In [3]: im = imread('stinkbug.png') + +@savefig mystinkbug.png width=4in +In [4]: imshow(im) +Out[4]: + +""", + r""" + +In [1]: x = 'hello world' + +# string methods can be +# used to alter the string +@doctest +In [2]: x.upper() +Out[2]: 'HELLO WORLD' + +@verbatim +In [3]: x.st +x.startswith x.strip +""", + r""" + +In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\ + .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv' + +In [131]: print url.split('&') +['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv'] + +In [60]: import urllib + +""", + r"""\ + +In [133]: import numpy.random + +@suppress +In [134]: numpy.random.seed(2358) + +@doctest +In [135]: numpy.random.rand(10,2) +Out[135]: +array([[ 0.64524308, 0.59943846], + [ 0.47102322, 0.8715456 ], + [ 0.29370834, 0.74776844], + [ 0.99539577, 0.1313423 ], + [ 0.16250302, 0.21103583], + [ 0.81626524, 0.1312433 ], + [ 0.67338089, 0.72302393], + [ 0.7566368 , 0.07033696], + [ 0.22591016, 0.77731835], + [ 0.0072729 , 0.34273127]]) + +""", + + r""" +In [106]: print x +jdh + +In [109]: for i in range(10): + .....: print i + .....: + .....: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +""", + + r""" + +In [144]: from pylab import * + +In [145]: ion() + +# use a semicolon to suppress the output +@savefig test_hist.png width=4in +In [151]: hist(np.random.randn(10000), 100); + + +@savefig test_plot.png width=4in +In [151]: plot(np.random.randn(10000), 'o'); + """, + + r""" +# use a semicolon to suppress the output +In [151]: plt.clf() + +@savefig plot_simple.png width=4in +In [151]: plot([1,2,3]) + +@savefig hist_simple.png width=4in +In [151]: hist(np.random.randn(10000), 100); + +""", + r""" +# update the current fig +In [151]: ylabel('number') + +In [152]: title('normal distribution') + + +@savefig hist_with_text.png +In [153]: grid(True) + + """, + ] + # skip local-file depending first example: + examples = examples[1:] + + #ipython_directive.DEBUG = True # dbg + #options = dict(suppress=True) # dbg + options = dict() + for example in examples: + content = example.split('\n') + ipython_directive('debug', arguments=None, options=options, + content=content, lineno=0, + content_offset=None, block_text=None, + state=None, state_machine=None, + ) + +# Run test suite as a script +if __name__=='__main__': + if not os.path.isdir('_static'): + os.mkdir('_static') + test() + print 'All OK? Check figures in _static/' + + From 844298fa088e2b952e6d3e39bf362163a3c4157b Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:31:51 +0000 Subject: [PATCH 130/197] Adding ipython requirements (temporary) and removing unnecessary mock requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ebe31175..dd6c9fa8 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['mock', 'sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + install_requires=['ipython', 'sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx'], cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From 50850baa210caa06656b22e1e7fd328d4239af73 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:34:11 +0000 Subject: [PATCH 131/197] More docs testing --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 47bb52fb..ac3e8b39 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -62,9 +62,9 @@ print "Importing extensions" extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.viewcode', - 'sphinx.ext.pngmath', - 'ipython_directive', - 'ipython_console_highlighting.py' + 'sphinx.ext.pngmath' + #'ipython_directive', + #'ipython_console_highlighting.py' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', From 69acbf42af769c418962175a1298b18ffce63183 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:41:57 +0000 Subject: [PATCH 132/197] More docs --- doc/conf.py | 5 ++++- doc/doc-requirements.txt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/doc-requirements.txt diff --git a/doc/conf.py b/doc/conf.py index ac3e8b39..af714471 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -60,7 +60,7 @@ sys.path.append(os.path.abspath('sphinxext')) print "Importing extensions" extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', + #'sphinx.ext.doctest' 'sphinx.ext.viewcode', 'sphinx.ext.pngmath' #'ipython_directive', @@ -107,6 +107,9 @@ for mod_name in MOCK_MODULES: # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +import GPy +version = GPy.__version__ + if on_rtd: sys.path.append("../GPy") os.system("pwd") diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt new file mode 100644 index 00000000..49a7ffe2 --- /dev/null +++ b/doc/doc-requirements.txt @@ -0,0 +1 @@ +ipython From 131a4b507f7c1f46ea3ace696df17927b4004623 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:46:28 +0000 Subject: [PATCH 133/197] Adding requirements removing mock.py --- doc/doc-requirements.txt | 3 + doc/mock.py | 2366 -------------------------------------- setup.py | 2 +- 3 files changed, 4 insertions(+), 2367 deletions(-) delete mode 100644 doc/mock.py diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt index 49a7ffe2..b0704d61 100644 --- a/doc/doc-requirements.txt +++ b/doc/doc-requirements.txt @@ -1 +1,4 @@ ipython +matplotlib +numpy +scipy diff --git a/doc/mock.py b/doc/mock.py deleted file mode 100644 index cc4aa653..00000000 --- a/doc/mock.py +++ /dev/null @@ -1,2366 +0,0 @@ -# mock.py -# Test tools for mocking and patching. -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk - -# mock 1.0.1 -# http://www.voidspace.org.uk/python/mock/ - -# Released subject to the BSD License -# Please see http://www.voidspace.org.uk/python/license.shtml - -__all__ = ( - 'Mock', - 'MagicMock', - 'patch', - 'sentinel', - 'DEFAULT', - 'ANY', - 'call', - 'create_autospec', - 'FILTER_DIR', - 'NonCallableMock', - 'NonCallableMagicMock', - 'mock_open', - 'PropertyMock', -) - - -__version__ = '1.0.1' - - -import pprint -import sys - -try: - import inspect -except ImportError: - # for alternative platforms that - # may not have inspect - inspect = None - -try: - from functools import wraps as original_wraps -except ImportError: - # Python 2.4 compatibility - def wraps(original): - def inner(f): - f.__name__ = original.__name__ - f.__doc__ = original.__doc__ - f.__module__ = original.__module__ - wrapped = getattr(original, '__wrapped__', original) - f.__wrapped__ = wrapped - return f - return inner -else: - if sys.version_info[:2] >= (3, 2): - wraps = original_wraps - else: - def wraps(func): - def inner(f): - f = original_wraps(func)(f) - wrapped = getattr(func, '__wrapped__', func) - f.__wrapped__ = wrapped - return f - return inner - -try: - unicode -except NameError: - # Python 3 - basestring = unicode = str - -try: - long -except NameError: - # Python 3 - long = int - -try: - BaseException -except NameError: - # Python 2.4 compatibility - BaseException = Exception - -try: - next -except NameError: - def next(obj): - return obj.next() - - -BaseExceptions = (BaseException,) -if 'java' in sys.platform: - # jython - import java - BaseExceptions = (BaseException, java.lang.Throwable) - -try: - _isidentifier = str.isidentifier -except AttributeError: - # Python 2.X - import keyword - import re - regex = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) - def _isidentifier(string): - if string in keyword.kwlist: - return False - return regex.match(string) - - -inPy3k = sys.version_info[0] == 3 - -# Needed to work around Python 3 bug where use of "super" interferes with -# defining __class__ as a descriptor -_super = super - -self = 'im_self' -builtin = '__builtin__' -if inPy3k: - self = '__self__' - builtin = 'builtins' - -FILTER_DIR = True - - -def _is_instance_mock(obj): - # can't use isinstance on Mock objects because they override __class__ - # The base class for all mocks is NonCallableMock - return issubclass(type(obj), NonCallableMock) - - -def _is_exception(obj): - return ( - isinstance(obj, BaseExceptions) or - isinstance(obj, ClassTypes) and issubclass(obj, BaseExceptions) - ) - - -class _slotted(object): - __slots__ = ['a'] - - -DescriptorTypes = ( - type(_slotted.a), - property, -) - - -def _getsignature(func, skipfirst, instance=False): - if inspect is None: - raise ImportError('inspect module not available') - - if isinstance(func, ClassTypes) and not instance: - try: - func = func.__init__ - except AttributeError: - return - skipfirst = True - elif not isinstance(func, FunctionTypes): - # for classes where instance is True we end up here too - try: - func = func.__call__ - except AttributeError: - return - - if inPy3k: - try: - argspec = inspect.getfullargspec(func) - except TypeError: - # C function / method, possibly inherited object().__init__ - return - regargs, varargs, varkw, defaults, kwonly, kwonlydef, ann = argspec - else: - try: - regargs, varargs, varkwargs, defaults = inspect.getargspec(func) - except TypeError: - # C function / method, possibly inherited object().__init__ - return - - # instance methods and classmethods need to lose the self argument - if getattr(func, self, None) is not None: - regargs = regargs[1:] - if skipfirst: - # this condition and the above one are never both True - why? - regargs = regargs[1:] - - if inPy3k: - signature = inspect.formatargspec( - regargs, varargs, varkw, defaults, - kwonly, kwonlydef, ann, formatvalue=lambda value: "") - else: - signature = inspect.formatargspec( - regargs, varargs, varkwargs, defaults, - formatvalue=lambda value: "") - return signature[1:-1], func - - -def _check_signature(func, mock, skipfirst, instance=False): - if not _callable(func): - return - - result = _getsignature(func, skipfirst, instance) - if result is None: - return - signature, func = result - - # can't use self because "self" is common as an argument name - # unfortunately even not in the first place - src = "lambda _mock_self, %s: None" % signature - checksig = eval(src, {}) - _copy_func_details(func, checksig) - type(mock)._mock_check_sig = checksig - - -def _copy_func_details(func, funcopy): - funcopy.__name__ = func.__name__ - funcopy.__doc__ = func.__doc__ - #funcopy.__dict__.update(func.__dict__) - funcopy.__module__ = func.__module__ - if not inPy3k: - funcopy.func_defaults = func.func_defaults - return - funcopy.__defaults__ = func.__defaults__ - funcopy.__kwdefaults__ = func.__kwdefaults__ - - -def _callable(obj): - if isinstance(obj, ClassTypes): - return True - if getattr(obj, '__call__', None) is not None: - return True - return False - - -def _is_list(obj): - # checks for list or tuples - # XXXX badly named! - return type(obj) in (list, tuple) - - -def _instance_callable(obj): - """Given an object, return True if the object is callable. - For classes, return True if instances would be callable.""" - if not isinstance(obj, ClassTypes): - # already an instance - return getattr(obj, '__call__', None) is not None - - klass = obj - # uses __bases__ instead of __mro__ so that we work with old style classes - if klass.__dict__.get('__call__') is not None: - return True - - for base in klass.__bases__: - if _instance_callable(base): - return True - return False - - -def _set_signature(mock, original, instance=False): - # creates a function with signature (*args, **kwargs) that delegates to a - # mock. It still does signature checking by calling a lambda with the same - # signature as the original. - if not _callable(original): - return - - skipfirst = isinstance(original, ClassTypes) - result = _getsignature(original, skipfirst, instance) - if result is None: - # was a C function (e.g. object().__init__ ) that can't be mocked - return - - signature, func = result - - src = "lambda %s: None" % signature - checksig = eval(src, {}) - _copy_func_details(func, checksig) - - name = original.__name__ - if not _isidentifier(name): - name = 'funcopy' - context = {'_checksig_': checksig, 'mock': mock} - src = """def %s(*args, **kwargs): - _checksig_(*args, **kwargs) - return mock(*args, **kwargs)""" % name - exec (src, context) - funcopy = context[name] - _setup_func(funcopy, mock) - return funcopy - - -def _setup_func(funcopy, mock): - funcopy.mock = mock - - # can't use isinstance with mocks - if not _is_instance_mock(mock): - return - - def assert_called_with(*args, **kwargs): - return mock.assert_called_with(*args, **kwargs) - def assert_called_once_with(*args, **kwargs): - return mock.assert_called_once_with(*args, **kwargs) - def assert_has_calls(*args, **kwargs): - return mock.assert_has_calls(*args, **kwargs) - def assert_any_call(*args, **kwargs): - return mock.assert_any_call(*args, **kwargs) - def reset_mock(): - funcopy.method_calls = _CallList() - funcopy.mock_calls = _CallList() - mock.reset_mock() - ret = funcopy.return_value - if _is_instance_mock(ret) and not ret is mock: - ret.reset_mock() - - funcopy.called = False - funcopy.call_count = 0 - funcopy.call_args = None - funcopy.call_args_list = _CallList() - funcopy.method_calls = _CallList() - funcopy.mock_calls = _CallList() - - funcopy.return_value = mock.return_value - funcopy.side_effect = mock.side_effect - funcopy._mock_children = mock._mock_children - - funcopy.assert_called_with = assert_called_with - funcopy.assert_called_once_with = assert_called_once_with - funcopy.assert_has_calls = assert_has_calls - funcopy.assert_any_call = assert_any_call - funcopy.reset_mock = reset_mock - - mock._mock_delegate = funcopy - - -def _is_magic(name): - return '__%s__' % name[2:-2] == name - - -class _SentinelObject(object): - "A unique, named, sentinel object." - def __init__(self, name): - self.name = name - - def __repr__(self): - return 'sentinel.%s' % self.name - - -class _Sentinel(object): - """Access attributes to return a named object, usable as a sentinel.""" - def __init__(self): - self._sentinels = {} - - def __getattr__(self, name): - if name == '__bases__': - # Without this help(mock) raises an exception - raise AttributeError - return self._sentinels.setdefault(name, _SentinelObject(name)) - - -sentinel = _Sentinel() - -DEFAULT = sentinel.DEFAULT -_missing = sentinel.MISSING -_deleted = sentinel.DELETED - - -class OldStyleClass: - pass -ClassType = type(OldStyleClass) - - -def _copy(value): - if type(value) in (dict, list, tuple, set): - return type(value)(value) - return value - - -ClassTypes = (type,) -if not inPy3k: - ClassTypes = (type, ClassType) - -_allowed_names = set( - [ - 'return_value', '_mock_return_value', 'side_effect', - '_mock_side_effect', '_mock_parent', '_mock_new_parent', - '_mock_name', '_mock_new_name' - ] -) - - -def _delegating_property(name): - _allowed_names.add(name) - _the_name = '_mock_' + name - def _get(self, name=name, _the_name=_the_name): - sig = self._mock_delegate - if sig is None: - return getattr(self, _the_name) - return getattr(sig, name) - def _set(self, value, name=name, _the_name=_the_name): - sig = self._mock_delegate - if sig is None: - self.__dict__[_the_name] = value - else: - setattr(sig, name, value) - - return property(_get, _set) - - - -class _CallList(list): - - def __contains__(self, value): - if not isinstance(value, list): - return list.__contains__(self, value) - len_value = len(value) - len_self = len(self) - if len_value > len_self: - return False - - for i in range(0, len_self - len_value + 1): - sub_list = self[i:i+len_value] - if sub_list == value: - return True - return False - - def __repr__(self): - return pprint.pformat(list(self)) - - -def _check_and_set_parent(parent, value, name, new_name): - if not _is_instance_mock(value): - return False - if ((value._mock_name or value._mock_new_name) or - (value._mock_parent is not None) or - (value._mock_new_parent is not None)): - return False - - _parent = parent - while _parent is not None: - # setting a mock (value) as a child or return value of itself - # should not modify the mock - if _parent is value: - return False - _parent = _parent._mock_new_parent - - if new_name: - value._mock_new_parent = parent - value._mock_new_name = new_name - if name: - value._mock_parent = parent - value._mock_name = name - return True - - - -class Base(object): - _mock_return_value = DEFAULT - _mock_side_effect = None - def __init__(self, *args, **kwargs): - pass - - - -class NonCallableMock(Base): - """A non-callable version of `Mock`""" - - def __new__(cls, *args, **kw): - # every instance has its own class - # so we can create magic methods on the - # class without stomping on other mocks - new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__}) - instance = object.__new__(new) - return instance - - - def __init__( - self, spec=None, wraps=None, name=None, spec_set=None, - parent=None, _spec_state=None, _new_name='', _new_parent=None, - **kwargs - ): - if _new_parent is None: - _new_parent = parent - - __dict__ = self.__dict__ - __dict__['_mock_parent'] = parent - __dict__['_mock_name'] = name - __dict__['_mock_new_name'] = _new_name - __dict__['_mock_new_parent'] = _new_parent - - if spec_set is not None: - spec = spec_set - spec_set = True - - self._mock_add_spec(spec, spec_set) - - __dict__['_mock_children'] = {} - __dict__['_mock_wraps'] = wraps - __dict__['_mock_delegate'] = None - - __dict__['_mock_called'] = False - __dict__['_mock_call_args'] = None - __dict__['_mock_call_count'] = 0 - __dict__['_mock_call_args_list'] = _CallList() - __dict__['_mock_mock_calls'] = _CallList() - - __dict__['method_calls'] = _CallList() - - if kwargs: - self.configure_mock(**kwargs) - - _super(NonCallableMock, self).__init__( - spec, wraps, name, spec_set, parent, - _spec_state - ) - - - def attach_mock(self, mock, attribute): - """ - Attach a mock as an attribute of this one, replacing its name and - parent. Calls to the attached mock will be recorded in the - `method_calls` and `mock_calls` attributes of this one.""" - mock._mock_parent = None - mock._mock_new_parent = None - mock._mock_name = '' - mock._mock_new_name = None - - setattr(self, attribute, mock) - - - def mock_add_spec(self, spec, spec_set=False): - """Add a spec to a mock. `spec` can either be an object or a - list of strings. Only attributes on the `spec` can be fetched as - attributes from the mock. - - If `spec_set` is True then only attributes on the spec can be set.""" - self._mock_add_spec(spec, spec_set) - - - def _mock_add_spec(self, spec, spec_set): - _spec_class = None - - if spec is not None and not _is_list(spec): - if isinstance(spec, ClassTypes): - _spec_class = spec - else: - _spec_class = _get_class(spec) - - spec = dir(spec) - - __dict__ = self.__dict__ - __dict__['_spec_class'] = _spec_class - __dict__['_spec_set'] = spec_set - __dict__['_mock_methods'] = spec - - - def __get_return_value(self): - ret = self._mock_return_value - if self._mock_delegate is not None: - ret = self._mock_delegate.return_value - - if ret is DEFAULT: - ret = self._get_child_mock( - _new_parent=self, _new_name='()' - ) - self.return_value = ret - return ret - - - def __set_return_value(self, value): - if self._mock_delegate is not None: - self._mock_delegate.return_value = value - else: - self._mock_return_value = value - _check_and_set_parent(self, value, None, '()') - - __return_value_doc = "The value to be returned when the mock is called." - return_value = property(__get_return_value, __set_return_value, - __return_value_doc) - - - @property - def __class__(self): - if self._spec_class is None: - return type(self) - return self._spec_class - - called = _delegating_property('called') - call_count = _delegating_property('call_count') - call_args = _delegating_property('call_args') - call_args_list = _delegating_property('call_args_list') - mock_calls = _delegating_property('mock_calls') - - - def __get_side_effect(self): - sig = self._mock_delegate - if sig is None: - return self._mock_side_effect - return sig.side_effect - - def __set_side_effect(self, value): - value = _try_iter(value) - sig = self._mock_delegate - if sig is None: - self._mock_side_effect = value - else: - sig.side_effect = value - - side_effect = property(__get_side_effect, __set_side_effect) - - - def reset_mock(self): - "Restore the mock object to its initial state." - self.called = False - self.call_args = None - self.call_count = 0 - self.mock_calls = _CallList() - self.call_args_list = _CallList() - self.method_calls = _CallList() - - for child in self._mock_children.values(): - if isinstance(child, _SpecState): - continue - child.reset_mock() - - ret = self._mock_return_value - if _is_instance_mock(ret) and ret is not self: - ret.reset_mock() - - - def configure_mock(self, **kwargs): - """Set attributes on the mock through keyword arguments. - - Attributes plus return values and side effects can be set on child - mocks using standard dot notation and unpacking a dictionary in the - method call: - - >>> attrs = {'method.return_value': 3, 'other.side_effect': KeyError} - >>> mock.configure_mock(**attrs)""" - for arg, val in sorted(kwargs.items(), - # we sort on the number of dots so that - # attributes are set before we set attributes on - # attributes - key=lambda entry: entry[0].count('.')): - args = arg.split('.') - final = args.pop() - obj = self - for entry in args: - obj = getattr(obj, entry) - setattr(obj, final, val) - - - def __getattr__(self, name): - if name == '_mock_methods': - raise AttributeError(name) - elif self._mock_methods is not None: - if name not in self._mock_methods or name in _all_magics: - raise AttributeError("Mock object has no attribute %r" % name) - elif _is_magic(name): - raise AttributeError(name) - - result = self._mock_children.get(name) - if result is _deleted: - raise AttributeError(name) - elif result is None: - wraps = None - if self._mock_wraps is not None: - # XXXX should we get the attribute without triggering code - # execution? - wraps = getattr(self._mock_wraps, name) - - result = self._get_child_mock( - parent=self, name=name, wraps=wraps, _new_name=name, - _new_parent=self - ) - self._mock_children[name] = result - - elif isinstance(result, _SpecState): - result = create_autospec( - result.spec, result.spec_set, result.instance, - result.parent, result.name - ) - self._mock_children[name] = result - - return result - - - def __repr__(self): - _name_list = [self._mock_new_name] - _parent = self._mock_new_parent - last = self - - dot = '.' - if _name_list == ['()']: - dot = '' - seen = set() - while _parent is not None: - last = _parent - - _name_list.append(_parent._mock_new_name + dot) - dot = '.' - if _parent._mock_new_name == '()': - dot = '' - - _parent = _parent._mock_new_parent - - # use ids here so as not to call __hash__ on the mocks - if id(_parent) in seen: - break - seen.add(id(_parent)) - - _name_list = list(reversed(_name_list)) - _first = last._mock_name or 'mock' - if len(_name_list) > 1: - if _name_list[1] not in ('()', '().'): - _first += '.' - _name_list[0] = _first - name = ''.join(_name_list) - - name_string = '' - if name not in ('mock', 'mock.'): - name_string = ' name=%r' % name - - spec_string = '' - if self._spec_class is not None: - spec_string = ' spec=%r' - if self._spec_set: - spec_string = ' spec_set=%r' - spec_string = spec_string % self._spec_class.__name__ - return "<%s%s%s id='%s'>" % ( - type(self).__name__, - name_string, - spec_string, - id(self) - ) - - - def __dir__(self): - """Filter the output of `dir(mock)` to only useful members. - XXXX - """ - extras = self._mock_methods or [] - from_type = dir(type(self)) - from_dict = list(self.__dict__) - - if FILTER_DIR: - from_type = [e for e in from_type if not e.startswith('_')] - from_dict = [e for e in from_dict if not e.startswith('_') or - _is_magic(e)] - return sorted(set(extras + from_type + from_dict + - list(self._mock_children))) - - - def __setattr__(self, name, value): - if name in _allowed_names: - # property setters go through here - return object.__setattr__(self, name, value) - elif (self._spec_set and self._mock_methods is not None and - name not in self._mock_methods and - name not in self.__dict__): - raise AttributeError("Mock object has no attribute '%s'" % name) - elif name in _unsupported_magics: - msg = 'Attempting to set unsupported magic method %r.' % name - raise AttributeError(msg) - elif name in _all_magics: - if self._mock_methods is not None and name not in self._mock_methods: - raise AttributeError("Mock object has no attribute '%s'" % name) - - if not _is_instance_mock(value): - setattr(type(self), name, _get_method(name, value)) - original = value - value = lambda *args, **kw: original(self, *args, **kw) - else: - # only set _new_name and not name so that mock_calls is tracked - # but not method calls - _check_and_set_parent(self, value, None, name) - setattr(type(self), name, value) - self._mock_children[name] = value - elif name == '__class__': - self._spec_class = value - return - else: - if _check_and_set_parent(self, value, name, name): - self._mock_children[name] = value - return object.__setattr__(self, name, value) - - - def __delattr__(self, name): - if name in _all_magics and name in type(self).__dict__: - delattr(type(self), name) - if name not in self.__dict__: - # for magic methods that are still MagicProxy objects and - # not set on the instance itself - return - - if name in self.__dict__: - object.__delattr__(self, name) - - obj = self._mock_children.get(name, _missing) - if obj is _deleted: - raise AttributeError(name) - if obj is not _missing: - del self._mock_children[name] - self._mock_children[name] = _deleted - - - - def _format_mock_call_signature(self, args, kwargs): - name = self._mock_name or 'mock' - return _format_call_signature(name, args, kwargs) - - - def _format_mock_failure_message(self, args, kwargs): - message = 'Expected call: %s\nActual call: %s' - expected_string = self._format_mock_call_signature(args, kwargs) - call_args = self.call_args - if len(call_args) == 3: - call_args = call_args[1:] - actual_string = self._format_mock_call_signature(*call_args) - return message % (expected_string, actual_string) - - - def assert_called_with(_mock_self, *args, **kwargs): - """assert that the mock was called with the specified arguments. - - Raises an AssertionError if the args and keyword args passed in are - different to the last call to the mock.""" - self = _mock_self - if self.call_args is None: - expected = self._format_mock_call_signature(args, kwargs) - raise AssertionError('Expected call: %s\nNot called' % (expected,)) - - if self.call_args != (args, kwargs): - msg = self._format_mock_failure_message(args, kwargs) - raise AssertionError(msg) - - - def assert_called_once_with(_mock_self, *args, **kwargs): - """assert that the mock was called exactly once and with the specified - arguments.""" - self = _mock_self - if not self.call_count == 1: - msg = ("Expected to be called once. Called %s times." % - self.call_count) - raise AssertionError(msg) - return self.assert_called_with(*args, **kwargs) - - - def assert_has_calls(self, calls, any_order=False): - """assert the mock has been called with the specified calls. - The `mock_calls` list is checked for the calls. - - If `any_order` is False (the default) then the calls must be - sequential. There can be extra calls before or after the - specified calls. - - If `any_order` is True then the calls can be in any order, but - they must all appear in `mock_calls`.""" - if not any_order: - if calls not in self.mock_calls: - raise AssertionError( - 'Calls not found.\nExpected: %r\n' - 'Actual: %r' % (calls, self.mock_calls) - ) - return - - all_calls = list(self.mock_calls) - - not_found = [] - for kall in calls: - try: - all_calls.remove(kall) - except ValueError: - not_found.append(kall) - if not_found: - raise AssertionError( - '%r not all found in call list' % (tuple(not_found),) - ) - - - def assert_any_call(self, *args, **kwargs): - """assert the mock has been called with the specified arguments. - - The assert passes if the mock has *ever* been called, unlike - `assert_called_with` and `assert_called_once_with` that only pass if - the call is the most recent one.""" - kall = call(*args, **kwargs) - if kall not in self.call_args_list: - expected_string = self._format_mock_call_signature(args, kwargs) - raise AssertionError( - '%s call not found' % expected_string - ) - - - def _get_child_mock(self, **kw): - """Create the child mocks for attributes and return value. - By default child mocks will be the same type as the parent. - Subclasses of Mock may want to override this to customize the way - child mocks are made. - - For non-callable mocks the callable variant will be used (rather than - any custom subclass).""" - _type = type(self) - if not issubclass(_type, CallableMixin): - if issubclass(_type, NonCallableMagicMock): - klass = MagicMock - elif issubclass(_type, NonCallableMock) : - klass = Mock - else: - klass = _type.__mro__[1] - return klass(**kw) - - - -def _try_iter(obj): - if obj is None: - return obj - if _is_exception(obj): - return obj - if _callable(obj): - return obj - try: - return iter(obj) - except TypeError: - # XXXX backwards compatibility - # but this will blow up on first call - so maybe we should fail early? - return obj - - - -class CallableMixin(Base): - - def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, - wraps=None, name=None, spec_set=None, parent=None, - _spec_state=None, _new_name='', _new_parent=None, **kwargs): - self.__dict__['_mock_return_value'] = return_value - - _super(CallableMixin, self).__init__( - spec, wraps, name, spec_set, parent, - _spec_state, _new_name, _new_parent, **kwargs - ) - - self.side_effect = side_effect - - - def _mock_check_sig(self, *args, **kwargs): - # stub method that can be replaced with one with a specific signature - pass - - - def __call__(_mock_self, *args, **kwargs): - # can't use self in-case a function / method we are mocking uses self - # in the signature - _mock_self._mock_check_sig(*args, **kwargs) - return _mock_self._mock_call(*args, **kwargs) - - - def _mock_call(_mock_self, *args, **kwargs): - self = _mock_self - self.called = True - self.call_count += 1 - self.call_args = _Call((args, kwargs), two=True) - self.call_args_list.append(_Call((args, kwargs), two=True)) - - _new_name = self._mock_new_name - _new_parent = self._mock_new_parent - self.mock_calls.append(_Call(('', args, kwargs))) - - seen = set() - skip_next_dot = _new_name == '()' - do_method_calls = self._mock_parent is not None - name = self._mock_name - while _new_parent is not None: - this_mock_call = _Call((_new_name, args, kwargs)) - if _new_parent._mock_new_name: - dot = '.' - if skip_next_dot: - dot = '' - - skip_next_dot = False - if _new_parent._mock_new_name == '()': - skip_next_dot = True - - _new_name = _new_parent._mock_new_name + dot + _new_name - - if do_method_calls: - if _new_name == name: - this_method_call = this_mock_call - else: - this_method_call = _Call((name, args, kwargs)) - _new_parent.method_calls.append(this_method_call) - - do_method_calls = _new_parent._mock_parent is not None - if do_method_calls: - name = _new_parent._mock_name + '.' + name - - _new_parent.mock_calls.append(this_mock_call) - _new_parent = _new_parent._mock_new_parent - - # use ids here so as not to call __hash__ on the mocks - _new_parent_id = id(_new_parent) - if _new_parent_id in seen: - break - seen.add(_new_parent_id) - - ret_val = DEFAULT - effect = self.side_effect - if effect is not None: - if _is_exception(effect): - raise effect - - if not _callable(effect): - result = next(effect) - if _is_exception(result): - raise result - return result - - ret_val = effect(*args, **kwargs) - if ret_val is DEFAULT: - ret_val = self.return_value - - if (self._mock_wraps is not None and - self._mock_return_value is DEFAULT): - return self._mock_wraps(*args, **kwargs) - if ret_val is DEFAULT: - ret_val = self.return_value - return ret_val - - - -class Mock(CallableMixin, NonCallableMock): - """ - Create a new `Mock` object. `Mock` takes several optional arguments - that specify the behaviour of the Mock object: - - * `spec`: This can be either a list of strings or an existing object (a - class or instance) that acts as the specification for the mock object. If - you pass in an object then a list of strings is formed by calling dir on - the object (excluding unsupported magic attributes and methods). Accessing - any attribute not in this list will raise an `AttributeError`. - - If `spec` is an object (rather than a list of strings) then - `mock.__class__` returns the class of the spec object. This allows mocks - to pass `isinstance` tests. - - * `spec_set`: A stricter variant of `spec`. If used, attempting to *set* - or get an attribute on the mock that isn't on the object passed as - `spec_set` will raise an `AttributeError`. - - * `side_effect`: A function to be called whenever the Mock is called. See - the `side_effect` attribute. Useful for raising exceptions or - dynamically changing return values. The function is called with the same - arguments as the mock, and unless it returns `DEFAULT`, the return - value of this function is used as the return value. - - Alternatively `side_effect` can be an exception class or instance. In - this case the exception will be raised when the mock is called. - - If `side_effect` is an iterable then each call to the mock will return - the next value from the iterable. If any of the members of the iterable - are exceptions they will be raised instead of returned. - - * `return_value`: The value returned when the mock is called. By default - this is a new Mock (created on first access). See the - `return_value` attribute. - - * `wraps`: Item for the mock object to wrap. If `wraps` is not None then - calling the Mock will pass the call through to the wrapped object - (returning the real result). Attribute access on the mock will return a - Mock object that wraps the corresponding attribute of the wrapped object - (so attempting to access an attribute that doesn't exist will raise an - `AttributeError`). - - If the mock has an explicit `return_value` set then calls are not passed - to the wrapped object and the `return_value` is returned instead. - - * `name`: If the mock has a name then it will be used in the repr of the - mock. This can be useful for debugging. The name is propagated to child - mocks. - - Mocks can also be called with arbitrary keyword arguments. These will be - used to set attributes on the mock after it is created. - """ - - - -def _dot_lookup(thing, comp, import_path): - try: - return getattr(thing, comp) - except AttributeError: - __import__(import_path) - return getattr(thing, comp) - - -def _importer(target): - components = target.split('.') - import_path = components.pop(0) - thing = __import__(import_path) - - for comp in components: - import_path += ".%s" % comp - thing = _dot_lookup(thing, comp, import_path) - return thing - - -def _is_started(patcher): - # XXXX horrible - return hasattr(patcher, 'is_local') - - -class _patch(object): - - attribute_name = None - _active_patches = set() - - def __init__( - self, getter, attribute, new, spec, create, - spec_set, autospec, new_callable, kwargs - ): - if new_callable is not None: - if new is not DEFAULT: - raise ValueError( - "Cannot use 'new' and 'new_callable' together" - ) - if autospec is not None: - raise ValueError( - "Cannot use 'autospec' and 'new_callable' together" - ) - - self.getter = getter - self.attribute = attribute - self.new = new - self.new_callable = new_callable - self.spec = spec - self.create = create - self.has_local = False - self.spec_set = spec_set - self.autospec = autospec - self.kwargs = kwargs - self.additional_patchers = [] - - - def copy(self): - patcher = _patch( - self.getter, self.attribute, self.new, self.spec, - self.create, self.spec_set, - self.autospec, self.new_callable, self.kwargs - ) - patcher.attribute_name = self.attribute_name - patcher.additional_patchers = [ - p.copy() for p in self.additional_patchers - ] - return patcher - - - def __call__(self, func): - if isinstance(func, ClassTypes): - return self.decorate_class(func) - return self.decorate_callable(func) - - - def decorate_class(self, klass): - for attr in dir(klass): - if not attr.startswith(patch.TEST_PREFIX): - continue - - attr_value = getattr(klass, attr) - if not hasattr(attr_value, "__call__"): - continue - - patcher = self.copy() - setattr(klass, attr, patcher(attr_value)) - return klass - - - def decorate_callable(self, func): - if hasattr(func, 'patchings'): - func.patchings.append(self) - return func - - @wraps(func) - def patched(*args, **keywargs): - # don't use a with here (backwards compatability with Python 2.4) - extra_args = [] - entered_patchers = [] - - # can't use try...except...finally because of Python 2.4 - # compatibility - exc_info = tuple() - try: - try: - for patching in patched.patchings: - arg = patching.__enter__() - entered_patchers.append(patching) - if patching.attribute_name is not None: - keywargs.update(arg) - elif patching.new is DEFAULT: - extra_args.append(arg) - - args += tuple(extra_args) - return func(*args, **keywargs) - except: - if (patching not in entered_patchers and - _is_started(patching)): - # the patcher may have been started, but an exception - # raised whilst entering one of its additional_patchers - entered_patchers.append(patching) - # Pass the exception to __exit__ - exc_info = sys.exc_info() - # re-raise the exception - raise - finally: - for patching in reversed(entered_patchers): - patching.__exit__(*exc_info) - - patched.patchings = [self] - if hasattr(func, 'func_code'): - # not in Python 3 - patched.compat_co_firstlineno = getattr( - func, "compat_co_firstlineno", - func.func_code.co_firstlineno - ) - return patched - - - def get_original(self): - target = self.getter() - name = self.attribute - - original = DEFAULT - local = False - - try: - original = target.__dict__[name] - except (AttributeError, KeyError): - original = getattr(target, name, DEFAULT) - else: - local = True - - if not self.create and original is DEFAULT: - raise AttributeError( - "%s does not have the attribute %r" % (target, name) - ) - return original, local - - - def __enter__(self): - """Perform the patch.""" - new, spec, spec_set = self.new, self.spec, self.spec_set - autospec, kwargs = self.autospec, self.kwargs - new_callable = self.new_callable - self.target = self.getter() - - # normalise False to None - if spec is False: - spec = None - if spec_set is False: - spec_set = None - if autospec is False: - autospec = None - - if spec is not None and autospec is not None: - raise TypeError("Can't specify spec and autospec") - if ((spec is not None or autospec is not None) and - spec_set not in (True, None)): - raise TypeError("Can't provide explicit spec_set *and* spec or autospec") - - original, local = self.get_original() - - if new is DEFAULT and autospec is None: - inherit = False - if spec is True: - # set spec to the object we are replacing - spec = original - if spec_set is True: - spec_set = original - spec = None - elif spec is not None: - if spec_set is True: - spec_set = spec - spec = None - elif spec_set is True: - spec_set = original - - if spec is not None or spec_set is not None: - if original is DEFAULT: - raise TypeError("Can't use 'spec' with create=True") - if isinstance(original, ClassTypes): - # If we're patching out a class and there is a spec - inherit = True - - Klass = MagicMock - _kwargs = {} - if new_callable is not None: - Klass = new_callable - elif spec is not None or spec_set is not None: - this_spec = spec - if spec_set is not None: - this_spec = spec_set - if _is_list(this_spec): - not_callable = '__call__' not in this_spec - else: - not_callable = not _callable(this_spec) - if not_callable: - Klass = NonCallableMagicMock - - if spec is not None: - _kwargs['spec'] = spec - if spec_set is not None: - _kwargs['spec_set'] = spec_set - - # add a name to mocks - if (isinstance(Klass, type) and - issubclass(Klass, NonCallableMock) and self.attribute): - _kwargs['name'] = self.attribute - - _kwargs.update(kwargs) - new = Klass(**_kwargs) - - if inherit and _is_instance_mock(new): - # we can only tell if the instance should be callable if the - # spec is not a list - this_spec = spec - if spec_set is not None: - this_spec = spec_set - if (not _is_list(this_spec) and not - _instance_callable(this_spec)): - Klass = NonCallableMagicMock - - _kwargs.pop('name') - new.return_value = Klass(_new_parent=new, _new_name='()', - **_kwargs) - elif autospec is not None: - # spec is ignored, new *must* be default, spec_set is treated - # as a boolean. Should we check spec is not None and that spec_set - # is a bool? - if new is not DEFAULT: - raise TypeError( - "autospec creates the mock for you. Can't specify " - "autospec and new." - ) - if original is DEFAULT: - raise TypeError("Can't use 'autospec' with create=True") - spec_set = bool(spec_set) - if autospec is True: - autospec = original - - new = create_autospec(autospec, spec_set=spec_set, - _name=self.attribute, **kwargs) - elif kwargs: - # can't set keyword args when we aren't creating the mock - # XXXX If new is a Mock we could call new.configure_mock(**kwargs) - raise TypeError("Can't pass kwargs to a mock we aren't creating") - - new_attr = new - - self.temp_original = original - self.is_local = local - setattr(self.target, self.attribute, new_attr) - if self.attribute_name is not None: - extra_args = {} - if self.new is DEFAULT: - extra_args[self.attribute_name] = new - for patching in self.additional_patchers: - arg = patching.__enter__() - if patching.new is DEFAULT: - extra_args.update(arg) - return extra_args - - return new - - - def __exit__(self, *exc_info): - """Undo the patch.""" - if not _is_started(self): - raise RuntimeError('stop called on unstarted patcher') - - if self.is_local and self.temp_original is not DEFAULT: - setattr(self.target, self.attribute, self.temp_original) - else: - delattr(self.target, self.attribute) - if not self.create and not hasattr(self.target, self.attribute): - # needed for proxy objects like django settings - setattr(self.target, self.attribute, self.temp_original) - - del self.temp_original - del self.is_local - del self.target - for patcher in reversed(self.additional_patchers): - if _is_started(patcher): - patcher.__exit__(*exc_info) - - - def start(self): - """Activate a patch, returning any created mock.""" - result = self.__enter__() - self._active_patches.add(self) - return result - - - def stop(self): - """Stop an active patch.""" - self._active_patches.discard(self) - return self.__exit__() - - - -def _get_target(target): - try: - target, attribute = target.rsplit('.', 1) - except (TypeError, ValueError): - raise TypeError("Need a valid target to patch. You supplied: %r" % - (target,)) - getter = lambda: _importer(target) - return getter, attribute - - -def _patch_object( - target, attribute, new=DEFAULT, spec=None, - create=False, spec_set=None, autospec=None, - new_callable=None, **kwargs - ): - """ - patch.object(target, attribute, new=DEFAULT, spec=None, create=False, - spec_set=None, autospec=None, new_callable=None, **kwargs) - - patch the named member (`attribute`) on an object (`target`) with a mock - object. - - `patch.object` can be used as a decorator, class decorator or a context - manager. Arguments `new`, `spec`, `create`, `spec_set`, - `autospec` and `new_callable` have the same meaning as for `patch`. Like - `patch`, `patch.object` takes arbitrary keyword arguments for configuring - the mock object it creates. - - When used as a class decorator `patch.object` honours `patch.TEST_PREFIX` - for choosing which methods to wrap. - """ - getter = lambda: target - return _patch( - getter, attribute, new, spec, create, - spec_set, autospec, new_callable, kwargs - ) - - -def _patch_multiple(target, spec=None, create=False, spec_set=None, - autospec=None, new_callable=None, **kwargs): - """Perform multiple patches in a single call. It takes the object to be - patched (either as an object or a string to fetch the object by importing) - and keyword arguments for the patches:: - - with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'): - ... - - Use `DEFAULT` as the value if you want `patch.multiple` to create - mocks for you. In this case the created mocks are passed into a decorated - function by keyword, and a dictionary is returned when `patch.multiple` is - used as a context manager. - - `patch.multiple` can be used as a decorator, class decorator or a context - manager. The arguments `spec`, `spec_set`, `create`, - `autospec` and `new_callable` have the same meaning as for `patch`. These - arguments will be applied to *all* patches done by `patch.multiple`. - - When used as a class decorator `patch.multiple` honours `patch.TEST_PREFIX` - for choosing which methods to wrap. - """ - if type(target) in (unicode, str): - getter = lambda: _importer(target) - else: - getter = lambda: target - - if not kwargs: - raise ValueError( - 'Must supply at least one keyword argument with patch.multiple' - ) - # need to wrap in a list for python 3, where items is a view - items = list(kwargs.items()) - attribute, new = items[0] - patcher = _patch( - getter, attribute, new, spec, create, spec_set, - autospec, new_callable, {} - ) - patcher.attribute_name = attribute - for attribute, new in items[1:]: - this_patcher = _patch( - getter, attribute, new, spec, create, spec_set, - autospec, new_callable, {} - ) - this_patcher.attribute_name = attribute - patcher.additional_patchers.append(this_patcher) - return patcher - - -def patch( - target, new=DEFAULT, spec=None, create=False, - spec_set=None, autospec=None, new_callable=None, **kwargs - ): - """ - `patch` acts as a function decorator, class decorator or a context - manager. Inside the body of the function or with statement, the `target` - is patched with a `new` object. When the function/with statement exits - the patch is undone. - - If `new` is omitted, then the target is replaced with a - `MagicMock`. If `patch` is used as a decorator and `new` is - omitted, the created mock is passed in as an extra argument to the - decorated function. If `patch` is used as a context manager the created - mock is returned by the context manager. - - `target` should be a string in the form `'package.module.ClassName'`. The - `target` is imported and the specified object replaced with the `new` - object, so the `target` must be importable from the environment you are - calling `patch` from. The target is imported when the decorated function - is executed, not at decoration time. - - The `spec` and `spec_set` keyword arguments are passed to the `MagicMock` - if patch is creating one for you. - - In addition you can pass `spec=True` or `spec_set=True`, which causes - patch to pass in the object being mocked as the spec/spec_set object. - - `new_callable` allows you to specify a different class, or callable object, - that will be called to create the `new` object. By default `MagicMock` is - used. - - A more powerful form of `spec` is `autospec`. If you set `autospec=True` - then the mock with be created with a spec from the object being replaced. - All attributes of the mock will also have the spec of the corresponding - attribute of the object being replaced. Methods and functions being - mocked will have their arguments checked and will raise a `TypeError` if - they are called with the wrong signature. For mocks replacing a class, - their return value (the 'instance') will have the same spec as the class. - - Instead of `autospec=True` you can pass `autospec=some_object` to use an - arbitrary object as the spec instead of the one being replaced. - - By default `patch` will fail to replace attributes that don't exist. If - you pass in `create=True`, and the attribute doesn't exist, patch will - create the attribute for you when the patched function is called, and - delete it again afterwards. This is useful for writing tests against - attributes that your production code creates at runtime. It is off by by - default because it can be dangerous. With it switched on you can write - passing tests against APIs that don't actually exist! - - Patch can be used as a `TestCase` class decorator. It works by - decorating each test method in the class. This reduces the boilerplate - code when your test methods share a common patchings set. `patch` finds - tests by looking for method names that start with `patch.TEST_PREFIX`. - By default this is `test`, which matches the way `unittest` finds tests. - You can specify an alternative prefix by setting `patch.TEST_PREFIX`. - - Patch can be used as a context manager, with the with statement. Here the - patching applies to the indented block after the with statement. If you - use "as" then the patched object will be bound to the name after the - "as"; very useful if `patch` is creating a mock object for you. - - `patch` takes arbitrary keyword arguments. These will be passed to - the `Mock` (or `new_callable`) on construction. - - `patch.dict(...)`, `patch.multiple(...)` and `patch.object(...)` are - available for alternate use-cases. - """ - getter, attribute = _get_target(target) - return _patch( - getter, attribute, new, spec, create, - spec_set, autospec, new_callable, kwargs - ) - - -class _patch_dict(object): - """ - Patch a dictionary, or dictionary like object, and restore the dictionary - to its original state after the test. - - `in_dict` can be a dictionary or a mapping like container. If it is a - mapping then it must at least support getting, setting and deleting items - plus iterating over keys. - - `in_dict` can also be a string specifying the name of the dictionary, which - will then be fetched by importing it. - - `values` can be a dictionary of values to set in the dictionary. `values` - can also be an iterable of `(key, value)` pairs. - - If `clear` is True then the dictionary will be cleared before the new - values are set. - - `patch.dict` can also be called with arbitrary keyword arguments to set - values in the dictionary:: - - with patch.dict('sys.modules', mymodule=Mock(), other_module=Mock()): - ... - - `patch.dict` can be used as a context manager, decorator or class - decorator. When used as a class decorator `patch.dict` honours - `patch.TEST_PREFIX` for choosing which methods to wrap. - """ - - def __init__(self, in_dict, values=(), clear=False, **kwargs): - if isinstance(in_dict, basestring): - in_dict = _importer(in_dict) - self.in_dict = in_dict - # support any argument supported by dict(...) constructor - self.values = dict(values) - self.values.update(kwargs) - self.clear = clear - self._original = None - - - def __call__(self, f): - if isinstance(f, ClassTypes): - return self.decorate_class(f) - @wraps(f) - def _inner(*args, **kw): - self._patch_dict() - try: - return f(*args, **kw) - finally: - self._unpatch_dict() - - return _inner - - - def decorate_class(self, klass): - for attr in dir(klass): - attr_value = getattr(klass, attr) - if (attr.startswith(patch.TEST_PREFIX) and - hasattr(attr_value, "__call__")): - decorator = _patch_dict(self.in_dict, self.values, self.clear) - decorated = decorator(attr_value) - setattr(klass, attr, decorated) - return klass - - - def __enter__(self): - """Patch the dict.""" - self._patch_dict() - - - def _patch_dict(self): - values = self.values - in_dict = self.in_dict - clear = self.clear - - try: - original = in_dict.copy() - except AttributeError: - # dict like object with no copy method - # must support iteration over keys - original = {} - for key in in_dict: - original[key] = in_dict[key] - self._original = original - - if clear: - _clear_dict(in_dict) - - try: - in_dict.update(values) - except AttributeError: - # dict like object with no update method - for key in values: - in_dict[key] = values[key] - - - def _unpatch_dict(self): - in_dict = self.in_dict - original = self._original - - _clear_dict(in_dict) - - try: - in_dict.update(original) - except AttributeError: - for key in original: - in_dict[key] = original[key] - - - def __exit__(self, *args): - """Unpatch the dict.""" - self._unpatch_dict() - return False - - start = __enter__ - stop = __exit__ - - -def _clear_dict(in_dict): - try: - in_dict.clear() - except AttributeError: - keys = list(in_dict) - for key in keys: - del in_dict[key] - - -def _patch_stopall(): - """Stop all active patches.""" - for patch in list(_patch._active_patches): - patch.stop() - - -patch.object = _patch_object -patch.dict = _patch_dict -patch.multiple = _patch_multiple -patch.stopall = _patch_stopall -patch.TEST_PREFIX = 'test' - -magic_methods = ( - "lt le gt ge eq ne " - "getitem setitem delitem " - "len contains iter " - "hash str sizeof " - "enter exit " - "divmod neg pos abs invert " - "complex int float index " - "trunc floor ceil " -) - -numerics = "add sub mul div floordiv mod lshift rshift and xor or pow " -inplace = ' '.join('i%s' % n for n in numerics.split()) -right = ' '.join('r%s' % n for n in numerics.split()) -extra = '' -if inPy3k: - extra = 'bool next ' -else: - extra = 'unicode long nonzero oct hex truediv rtruediv ' - -# not including __prepare__, __instancecheck__, __subclasscheck__ -# (as they are metaclass methods) -# __del__ is not supported at all as it causes problems if it exists - -_non_defaults = set('__%s__' % method for method in [ - 'cmp', 'getslice', 'setslice', 'coerce', 'subclasses', - 'format', 'get', 'set', 'delete', 'reversed', - 'missing', 'reduce', 'reduce_ex', 'getinitargs', - 'getnewargs', 'getstate', 'setstate', 'getformat', - 'setformat', 'repr', 'dir' -]) - - -def _get_method(name, func): - "Turns a callable object (like a mock) into a real function" - def method(self, *args, **kw): - return func(self, *args, **kw) - method.__name__ = name - return method - - -_magics = set( - '__%s__' % method for method in - ' '.join([magic_methods, numerics, inplace, right, extra]).split() -) - -_all_magics = _magics | _non_defaults - -_unsupported_magics = set([ - '__getattr__', '__setattr__', - '__init__', '__new__', '__prepare__' - '__instancecheck__', '__subclasscheck__', - '__del__' -]) - -_calculate_return_value = { - '__hash__': lambda self: object.__hash__(self), - '__str__': lambda self: object.__str__(self), - '__sizeof__': lambda self: object.__sizeof__(self), - '__unicode__': lambda self: unicode(object.__str__(self)), -} - -_return_values = { - '__lt__': NotImplemented, - '__gt__': NotImplemented, - '__le__': NotImplemented, - '__ge__': NotImplemented, - '__int__': 1, - '__contains__': False, - '__len__': 0, - '__exit__': False, - '__complex__': 1j, - '__float__': 1.0, - '__bool__': True, - '__nonzero__': True, - '__oct__': '1', - '__hex__': '0x1', - '__long__': long(1), - '__index__': 1, -} - - -def _get_eq(self): - def __eq__(other): - ret_val = self.__eq__._mock_return_value - if ret_val is not DEFAULT: - return ret_val - return self is other - return __eq__ - -def _get_ne(self): - def __ne__(other): - if self.__ne__._mock_return_value is not DEFAULT: - return DEFAULT - return self is not other - return __ne__ - -def _get_iter(self): - def __iter__(): - ret_val = self.__iter__._mock_return_value - if ret_val is DEFAULT: - return iter([]) - # if ret_val was already an iterator, then calling iter on it should - # return the iterator unchanged - return iter(ret_val) - return __iter__ - -_side_effect_methods = { - '__eq__': _get_eq, - '__ne__': _get_ne, - '__iter__': _get_iter, -} - - - -def _set_return_value(mock, method, name): - fixed = _return_values.get(name, DEFAULT) - if fixed is not DEFAULT: - method.return_value = fixed - return - - return_calulator = _calculate_return_value.get(name) - if return_calulator is not None: - try: - return_value = return_calulator(mock) - except AttributeError: - # XXXX why do we return AttributeError here? - # set it as a side_effect instead? - return_value = AttributeError(name) - method.return_value = return_value - return - - side_effector = _side_effect_methods.get(name) - if side_effector is not None: - method.side_effect = side_effector(mock) - - - -class MagicMixin(object): - def __init__(self, *args, **kw): - _super(MagicMixin, self).__init__(*args, **kw) - self._mock_set_magics() - - - def _mock_set_magics(self): - these_magics = _magics - - if self._mock_methods is not None: - these_magics = _magics.intersection(self._mock_methods) - - remove_magics = set() - remove_magics = _magics - these_magics - - for entry in remove_magics: - if entry in type(self).__dict__: - # remove unneeded magic methods - delattr(self, entry) - - # don't overwrite existing attributes if called a second time - these_magics = these_magics - set(type(self).__dict__) - - _type = type(self) - for entry in these_magics: - setattr(_type, entry, MagicProxy(entry, self)) - - - -class NonCallableMagicMock(MagicMixin, NonCallableMock): - """A version of `MagicMock` that isn't callable.""" - def mock_add_spec(self, spec, spec_set=False): - """Add a spec to a mock. `spec` can either be an object or a - list of strings. Only attributes on the `spec` can be fetched as - attributes from the mock. - - If `spec_set` is True then only attributes on the spec can be set.""" - self._mock_add_spec(spec, spec_set) - self._mock_set_magics() - - - -class MagicMock(MagicMixin, Mock): - """ - MagicMock is a subclass of Mock with default implementations - of most of the magic methods. You can use MagicMock without having to - configure the magic methods yourself. - - If you use the `spec` or `spec_set` arguments then *only* magic - methods that exist in the spec will be created. - - Attributes and the return value of a `MagicMock` will also be `MagicMocks`. - """ - def mock_add_spec(self, spec, spec_set=False): - """Add a spec to a mock. `spec` can either be an object or a - list of strings. Only attributes on the `spec` can be fetched as - attributes from the mock. - - If `spec_set` is True then only attributes on the spec can be set.""" - self._mock_add_spec(spec, spec_set) - self._mock_set_magics() - - - -class MagicProxy(object): - def __init__(self, name, parent): - self.name = name - self.parent = parent - - def __call__(self, *args, **kwargs): - m = self.create_mock() - return m(*args, **kwargs) - - def create_mock(self): - entry = self.name - parent = self.parent - m = parent._get_child_mock(name=entry, _new_name=entry, - _new_parent=parent) - setattr(parent, entry, m) - _set_return_value(parent, m, entry) - return m - - def __get__(self, obj, _type=None): - return self.create_mock() - - - -class _ANY(object): - "A helper object that compares equal to everything." - - def __eq__(self, other): - return True - - def __ne__(self, other): - return False - - def __repr__(self): - return '' - -ANY = _ANY() - - - -def _format_call_signature(name, args, kwargs): - message = '%s(%%s)' % name - formatted_args = '' - args_string = ', '.join([repr(arg) for arg in args]) - kwargs_string = ', '.join([ - '%s=%r' % (key, value) for key, value in kwargs.items() - ]) - if args_string: - formatted_args = args_string - if kwargs_string: - if formatted_args: - formatted_args += ', ' - formatted_args += kwargs_string - - return message % formatted_args - - - -class _Call(tuple): - """ - A tuple for holding the results of a call to a mock, either in the form - `(args, kwargs)` or `(name, args, kwargs)`. - - If args or kwargs are empty then a call tuple will compare equal to - a tuple without those values. This makes comparisons less verbose:: - - _Call(('name', (), {})) == ('name',) - _Call(('name', (1,), {})) == ('name', (1,)) - _Call(((), {'a': 'b'})) == ({'a': 'b'},) - - The `_Call` object provides a useful shortcut for comparing with call:: - - _Call(((1, 2), {'a': 3})) == call(1, 2, a=3) - _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3) - - If the _Call has no name then it will match any name. - """ - def __new__(cls, value=(), name=None, parent=None, two=False, - from_kall=True): - name = '' - args = () - kwargs = {} - _len = len(value) - if _len == 3: - name, args, kwargs = value - elif _len == 2: - first, second = value - if isinstance(first, basestring): - name = first - if isinstance(second, tuple): - args = second - else: - kwargs = second - else: - args, kwargs = first, second - elif _len == 1: - value, = value - if isinstance(value, basestring): - name = value - elif isinstance(value, tuple): - args = value - else: - kwargs = value - - if two: - return tuple.__new__(cls, (args, kwargs)) - - return tuple.__new__(cls, (name, args, kwargs)) - - - def __init__(self, value=(), name=None, parent=None, two=False, - from_kall=True): - self.name = name - self.parent = parent - self.from_kall = from_kall - - - def __eq__(self, other): - if other is ANY: - return True - try: - len_other = len(other) - except TypeError: - return False - - self_name = '' - if len(self) == 2: - self_args, self_kwargs = self - else: - self_name, self_args, self_kwargs = self - - other_name = '' - if len_other == 0: - other_args, other_kwargs = (), {} - elif len_other == 3: - other_name, other_args, other_kwargs = other - elif len_other == 1: - value, = other - if isinstance(value, tuple): - other_args = value - other_kwargs = {} - elif isinstance(value, basestring): - other_name = value - other_args, other_kwargs = (), {} - else: - other_args = () - other_kwargs = value - else: - # len 2 - # could be (name, args) or (name, kwargs) or (args, kwargs) - first, second = other - if isinstance(first, basestring): - other_name = first - if isinstance(second, tuple): - other_args, other_kwargs = second, {} - else: - other_args, other_kwargs = (), second - else: - other_args, other_kwargs = first, second - - if self_name and other_name != self_name: - return False - - # this order is important for ANY to work! - return (other_args, other_kwargs) == (self_args, self_kwargs) - - - def __ne__(self, other): - return not self.__eq__(other) - - - def __call__(self, *args, **kwargs): - if self.name is None: - return _Call(('', args, kwargs), name='()') - - name = self.name + '()' - return _Call((self.name, args, kwargs), name=name, parent=self) - - - def __getattr__(self, attr): - if self.name is None: - return _Call(name=attr, from_kall=False) - name = '%s.%s' % (self.name, attr) - return _Call(name=name, parent=self, from_kall=False) - - - def __repr__(self): - if not self.from_kall: - name = self.name or 'call' - if name.startswith('()'): - name = 'call%s' % name - return name - - if len(self) == 2: - name = 'call' - args, kwargs = self - else: - name, args, kwargs = self - if not name: - name = 'call' - elif not name.startswith('()'): - name = 'call.%s' % name - else: - name = 'call%s' % name - return _format_call_signature(name, args, kwargs) - - - def call_list(self): - """For a call object that represents multiple calls, `call_list` - returns a list of all the intermediate calls as well as the - final call.""" - vals = [] - thing = self - while thing is not None: - if thing.from_kall: - vals.append(thing) - thing = thing.parent - return _CallList(reversed(vals)) - - -call = _Call(from_kall=False) - - - -def create_autospec(spec, spec_set=False, instance=False, _parent=None, - _name=None, **kwargs): - """Create a mock object using another object as a spec. Attributes on the - mock will use the corresponding attribute on the `spec` object as their - spec. - - Functions or methods being mocked will have their arguments checked - to check that they are called with the correct signature. - - If `spec_set` is True then attempting to set attributes that don't exist - on the spec object will raise an `AttributeError`. - - If a class is used as a spec then the return value of the mock (the - instance of the class) will have the same spec. You can use a class as the - spec for an instance object by passing `instance=True`. The returned mock - will only be callable if instances of the mock are callable. - - `create_autospec` also takes arbitrary keyword arguments that are passed to - the constructor of the created mock.""" - if _is_list(spec): - # can't pass a list instance to the mock constructor as it will be - # interpreted as a list of strings - spec = type(spec) - - is_type = isinstance(spec, ClassTypes) - - _kwargs = {'spec': spec} - if spec_set: - _kwargs = {'spec_set': spec} - elif spec is None: - # None we mock with a normal mock without a spec - _kwargs = {} - - _kwargs.update(kwargs) - - Klass = MagicMock - if type(spec) in DescriptorTypes: - # descriptors don't have a spec - # because we don't know what type they return - _kwargs = {} - elif not _callable(spec): - Klass = NonCallableMagicMock - elif is_type and instance and not _instance_callable(spec): - Klass = NonCallableMagicMock - - _new_name = _name - if _parent is None: - # for a top level object no _new_name should be set - _new_name = '' - - mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name, - name=_name, **_kwargs) - - if isinstance(spec, FunctionTypes): - # should only happen at the top level because we don't - # recurse for functions - mock = _set_signature(mock, spec) - else: - _check_signature(spec, mock, is_type, instance) - - if _parent is not None and not instance: - _parent._mock_children[_name] = mock - - if is_type and not instance and 'return_value' not in kwargs: - mock.return_value = create_autospec(spec, spec_set, instance=True, - _name='()', _parent=mock) - - for entry in dir(spec): - if _is_magic(entry): - # MagicMock already does the useful magic methods for us - continue - - if isinstance(spec, FunctionTypes) and entry in FunctionAttributes: - # allow a mock to actually be a function - continue - - # XXXX do we need a better way of getting attributes without - # triggering code execution (?) Probably not - we need the actual - # object to mock it so we would rather trigger a property than mock - # the property descriptor. Likewise we want to mock out dynamically - # provided attributes. - # XXXX what about attributes that raise exceptions other than - # AttributeError on being fetched? - # we could be resilient against it, or catch and propagate the - # exception when the attribute is fetched from the mock - try: - original = getattr(spec, entry) - except AttributeError: - continue - - kwargs = {'spec': original} - if spec_set: - kwargs = {'spec_set': original} - - if not isinstance(original, FunctionTypes): - new = _SpecState(original, spec_set, mock, entry, instance) - mock._mock_children[entry] = new - else: - parent = mock - if isinstance(spec, FunctionTypes): - parent = mock.mock - - new = MagicMock(parent=parent, name=entry, _new_name=entry, - _new_parent=parent, **kwargs) - mock._mock_children[entry] = new - skipfirst = _must_skip(spec, entry, is_type) - _check_signature(original, new, skipfirst=skipfirst) - - # so functions created with _set_signature become instance attributes, - # *plus* their underlying mock exists in _mock_children of the parent - # mock. Adding to _mock_children may be unnecessary where we are also - # setting as an instance attribute? - if isinstance(new, FunctionTypes): - setattr(mock, entry, new) - - return mock - - -def _must_skip(spec, entry, is_type): - if not isinstance(spec, ClassTypes): - if entry in getattr(spec, '__dict__', {}): - # instance attribute - shouldn't skip - return False - spec = spec.__class__ - if not hasattr(spec, '__mro__'): - # old style class: can't have descriptors anyway - return is_type - - for klass in spec.__mro__: - result = klass.__dict__.get(entry, DEFAULT) - if result is DEFAULT: - continue - if isinstance(result, (staticmethod, classmethod)): - return False - return is_type - - # shouldn't get here unless function is a dynamically provided attribute - # XXXX untested behaviour - return is_type - - -def _get_class(obj): - try: - return obj.__class__ - except AttributeError: - # in Python 2, _sre.SRE_Pattern objects have no __class__ - return type(obj) - - -class _SpecState(object): - - def __init__(self, spec, spec_set=False, parent=None, - name=None, ids=None, instance=False): - self.spec = spec - self.ids = ids - self.spec_set = spec_set - self.parent = parent - self.instance = instance - self.name = name - - -FunctionTypes = ( - # python function - type(create_autospec), - # instance method - type(ANY.__eq__), - # unbound method - type(_ANY.__eq__), -) - -FunctionAttributes = set([ - 'func_closure', - 'func_code', - 'func_defaults', - 'func_dict', - 'func_doc', - 'func_globals', - 'func_name', -]) - - -file_spec = None - - -def mock_open(mock=None, read_data=''): - """ - A helper function to create a mock to replace the use of `open`. It works - for `open` called directly or used as a context manager. - - The `mock` argument is the mock object to configure. If `None` (the - default) then a `MagicMock` will be created for you, with the API limited - to methods or attributes available on standard file handles. - - `read_data` is a string for the `read` method of the file handle to return. - This is an empty string by default. - """ - global file_spec - if file_spec is None: - # set on first use - if inPy3k: - import _io - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - else: - file_spec = file - - if mock is None: - mock = MagicMock(name='open', spec=open) - - handle = MagicMock(spec=file_spec) - handle.write.return_value = None - handle.__enter__.return_value = handle - handle.read.return_value = read_data - - mock.return_value = handle - return mock - - -class PropertyMock(Mock): - """ - A mock intended to be used as a property, or other descriptor, on a class. - `PropertyMock` provides `__get__` and `__set__` methods so you can specify - a return value when it is fetched. - - Fetching a `PropertyMock` instance from an object calls the mock, with - no args. Setting it calls the mock with the value being set. - """ - def _get_child_mock(self, **kwargs): - return MagicMock(**kwargs) - - def __get__(self, obj, obj_type): - return self() - def __set__(self, obj, val): - self(val) - diff --git a/setup.py b/setup.py index dd6c9fa8..432b8b13 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['ipython', 'sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx'], cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From b92bdbbc0e70f7662bd8dc7be19e9886a01e7271 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 11:46:59 +0000 Subject: [PATCH 134/197] Removed version check --- doc/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index af714471..1b1fe219 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -108,7 +108,6 @@ for mod_name in MOCK_MODULES: on_rtd = os.environ.get('READTHEDOCS', None) == 'True' import GPy -version = GPy.__version__ if on_rtd: sys.path.append("../GPy") From 76fda8e5b37c82a25197ba48e4562cfa14b310af Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:12:50 +0000 Subject: [PATCH 135/197] Fixed some broken imports (likelihoods has moved), remember to tell everyone to delete their pyc file --- GPy/models/generalized_FITC.py | 4 ++-- GPy/models/uncollapsed_sparse_GP.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GPy/models/generalized_FITC.py b/GPy/models/generalized_FITC.py index 57ae2407..5c88d44d 100644 --- a/GPy/models/generalized_FITC.py +++ b/GPy/models/generalized_FITC.py @@ -10,8 +10,8 @@ from ..core import model from ..util.linalg import pdinv,mdot from ..util.plot import gpplot #from ..inference.Expectation_Propagation import FITC -from ..inference.EP import FITC -from ..inference.likelihoods import likelihood,probit +from ..likelihoods.EP import FITC +from ..likelihoods import likelihood,probit class generalized_FITC(model): def __init__(self,X,likelihood,kernel=None,inducing=10,epsilon_ep=1e-3,powerep=[1.,1.]): diff --git a/GPy/models/uncollapsed_sparse_GP.py b/GPy/models/uncollapsed_sparse_GP.py index 3bb72e60..0fccfc71 100644 --- a/GPy/models/uncollapsed_sparse_GP.py +++ b/GPy/models/uncollapsed_sparse_GP.py @@ -6,7 +6,7 @@ import pylab as pb from ..util.linalg import mdot, jitchol, chol_inv, pdinv from ..util.plot import gpplot from .. import kern -from ..inference.likelihoods import likelihood +from ..likelihoods import likelihood from sparse_GP_regression import sparse_GP_regression class uncollapsed_sparse_GP(sparse_GP_regression): @@ -136,8 +136,8 @@ class uncollapsed_sparse_GP(sparse_GP_regression): #dL_dm = np.dot(self.Kmmi,self.psi1V) - np.dot(self.Lambda,self.q_u_mean) dL_dm = np.dot(self.Kmmi,self.psi1V) - self.q_u_canonical[0] - #dL_dSim = - #dL_dmhSi = + #dL_dSim = + #dL_dmhSi = return np.hstack((dL_dm.flatten(),dL_dmmT_S.flatten())) # natgrad only, grad TODO From ac4f50f44d0cf4a8e935ec44b8ae178ca7d2f739 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:13:09 +0000 Subject: [PATCH 136/197] added ordering of methods --- doc/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 1b1fe219..175a4bae 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -390,3 +390,5 @@ epub_copyright = u'2013, Author' #def setup(app): #app.connect("autodoc-skip-member", skip) + +autodoc_member_order = "source" From f80d65cc8d943ba9c97d38c3bf36adefc3325c92 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:22:45 +0000 Subject: [PATCH 137/197] removing GPy import from conf (may put back in) --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 175a4bae..ec91b3f8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -107,7 +107,7 @@ for mod_name in MOCK_MODULES: # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -import GPy +#import GPy if on_rtd: sys.path.append("../GPy") From 54698f091b8f314f9f312e151b59812cf6e5009e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:28:09 +0000 Subject: [PATCH 138/197] trying to rid buildsphinx error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 432b8b13..3c3d150d 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup(name = 'GPy', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx'], - cmdclass = {'build_sphinx': BuildDoc}, + #cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ "Development Status :: 1 - Alpha", "Topic :: Machine Learning", From 51f4762d47b3e2c4ef85efca44095bedc9f4716e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:38:03 +0000 Subject: [PATCH 139/197] Force up to date version of sphinx --- doc/conf.py | 2 +- doc/doc-requirements.txt | 1 - setup.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ec91b3f8..43a9f652 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -53,7 +53,7 @@ sys.path.append(os.path.abspath('sphinxext')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +needs_sphinx = '1.1.3' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt index b0704d61..0b5ac59b 100644 --- a/doc/doc-requirements.txt +++ b/doc/doc-requirements.txt @@ -1,4 +1,3 @@ ipython -matplotlib numpy scipy diff --git a/setup.py b/setup.py index 3c3d150d..7fa4d28e 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup(name = 'GPy', #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], - setup_requires=['sphinx'], + setup_requires=['sphinx>=1.1.3'], #cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ "Development Status :: 1 - Alpha", From 62efa7e66c79bb4839702928349c842de2391d22 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:41:13 +0000 Subject: [PATCH 140/197] Force up to date version of sphinx --- doc/conf.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 43a9f652..7ba26c70 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -19,7 +19,7 @@ print "sys.path:", sys.path try: import numpy print "numpy: %s, %s" % (numpy.__version__, numpy.__file__) -except ImportError: +except importerror: print "no numpy" try: import matplotlib @@ -27,10 +27,15 @@ try: except ImportError: print "no matplotlib" try: - import IPython - print "ipython: %s, %s" % (IPython.__version__, IPython.__file__) -except ImportError: + import ipython + print "ipython: %s, %s" % (ipython.__version__, ipython.__file__) +except importerror: print "no ipython" +try: + import sphinx + print "sphinx: %s, %s" % (sphinx.__version__, sphinx.__file__) +except importerror: + print "no sphinx" sys.path.insert(0, os.getcwd() + "/..") @@ -53,7 +58,7 @@ sys.path.append(os.path.abspath('sphinxext')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.1.3' +#needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. From d619de2ef4a5acef6c389e38888e30a222255fc5 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:42:30 +0000 Subject: [PATCH 141/197] Silly me copying and pasting --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 7ba26c70..c719e130 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -19,7 +19,7 @@ print "sys.path:", sys.path try: import numpy print "numpy: %s, %s" % (numpy.__version__, numpy.__file__) -except importerror: +except ImportError: print "no numpy" try: import matplotlib @@ -29,12 +29,12 @@ except ImportError: try: import ipython print "ipython: %s, %s" % (ipython.__version__, ipython.__file__) -except importerror: +except ImportError: print "no ipython" try: import sphinx print "sphinx: %s, %s" % (sphinx.__version__, sphinx.__file__) -except importerror: +except ImportError: print "no sphinx" sys.path.insert(0, os.getcwd() + "/..") From a5962119319190505941235f8f7ffabaf2fca3c5 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:46:48 +0000 Subject: [PATCH 142/197] Got rid of ipython example for a sec --- doc/tuto_GP_regression.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 417bb1bd..08931834 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -1,14 +1,12 @@ - -.. ipython:: python - - print "Hello world" - X = [[1, 10], [1, 20], [1, -2]] - - ************************************* Gaussian process regression tutorial ************************************* +#.. ipython:: python +# +# print "Hello world" +# X = [[1, 10], [1, 20], [1, -2]] + We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. We first import the libraries we will need: :: From b2ba4fdfe3bbf1dbdc645bfacdcf84f4217864cb Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 12:58:04 +0000 Subject: [PATCH 143/197] Removed import of builddoc --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 7fa4d28e..f0d38044 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ import os from numpy.distutils.core import Extension, setup -from sphinx.setup_command import BuildDoc # Version number version = '0.1.3' @@ -28,7 +27,6 @@ setup(name = 'GPy', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], setup_requires=['sphinx>=1.1.3'], - #cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ "Development Status :: 1 - Alpha", "Topic :: Machine Learning", From b28c4e415e13cf2f7561a2865a723c485dea4a36 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:10:27 +0000 Subject: [PATCH 144/197] Debugging in make.bat --- doc/make.bat | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/make.bat b/doc/make.bat index 40a74901..3d35ac01 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -13,6 +13,11 @@ if NOT "%PAPER%" == "" ( set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) + +echo "Sphinx details" +echo "%SPHINXBUILD%" +echo "%ALLSPHINXOPTS%" + if "%1" == "" goto help if "%1" == "help" ( diff --git a/setup.py b/setup.py index f0d38044..ed81250e 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9', 'matplotlib>=1.1'], setup_requires=['sphinx>=1.1.3'], classifiers=[ "Development Status :: 1 - Alpha", From 67f2c5633639ca4bdbed8463ff5f253da4df32b6 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:15:56 +0000 Subject: [PATCH 145/197] Reverting setup.py --- doc/make.bat | 5 ----- setup.py | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/doc/make.bat b/doc/make.bat index 3d35ac01..40a74901 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -13,11 +13,6 @@ if NOT "%PAPER%" == "" ( set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) - -echo "Sphinx details" -echo "%SPHINXBUILD%" -echo "%ALLSPHINXOPTS%" - if "%1" == "" goto help if "%1" == "help" ( diff --git a/setup.py b/setup.py index ed81250e..432b8b13 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import os from numpy.distutils.core import Extension, setup +from sphinx.setup_command import BuildDoc # Version number version = '0.1.3' @@ -25,8 +26,9 @@ setup(name = 'GPy', long_description=read('README.md'), #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], - install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9', 'matplotlib>=1.1'], - setup_requires=['sphinx>=1.1.3'], + install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + setup_requires=['sphinx'], + cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ "Development Status :: 1 - Alpha", "Topic :: Machine Learning", From 633bfde9964afd9f2cb6fc8784c76a7520910cb7 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:24:25 +0000 Subject: [PATCH 146/197] removed sphinx from setup --- doc/conf.py | 2 +- setup.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index c719e130..df15b3fc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ try: except ImportError: print "no sphinx" -sys.path.insert(0, os.getcwd() + "/..") +#sys.path.insert(0, os.getcwd() + "/..") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/setup.py b/setup.py index 432b8b13..1c696b86 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import os from numpy.distutils.core import Extension, setup -from sphinx.setup_command import BuildDoc +#from sphinx.setup_command import BuildDoc # Version number version = '0.1.3' @@ -27,8 +27,8 @@ setup(name = 'GPy', #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], - setup_requires=['sphinx'], - cmdclass = {'build_sphinx': BuildDoc}, + #setup_requires=['sphinx'], + #cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ "Development Status :: 1 - Alpha", "Topic :: Machine Learning", From 04c716c3d23eee36420a5a7e2b9a3bcf2c481250 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:49:09 +0000 Subject: [PATCH 147/197] Added root path back --- GPy/core/model.py | 4 ++-- doc/conf.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index 5e228b15..a0628c42 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -83,7 +83,7 @@ class model(parameterised): def get(self,name, return_names=False): """ - Get a model parameter by name. The name is applied as a regular expression and all parameters that match that regular expression are returned. + Get a model parameter by name. The name is applied as a regular expression and all parameters that match that regular expression are returned. """ matches = self.grep_param_names(name) if len(matches): @@ -108,7 +108,7 @@ class model(parameterised): def get_gradient(self,name, return_names=False): """ - Get model gradient(s) by name. The name is applied as a regular expression and all parameters that match that regular expression are returned. + Get model gradient(s) by name. The name is applied as a regular expression and all parameters that match that regular expression are returned. """ matches = self.grep_param_names(name) if len(matches): diff --git a/doc/conf.py b/doc/conf.py index df15b3fc..c719e130 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ try: except ImportError: print "no sphinx" -#sys.path.insert(0, os.getcwd() + "/..") +sys.path.insert(0, os.getcwd() + "/..") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From 2422945f6c39ef87372f560a30935254488d1683 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:56:53 +0000 Subject: [PATCH 148/197] Changed path to append it rather than inset it --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index c719e130..c9c560db 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,12 +37,12 @@ try: except ImportError: print "no sphinx" -sys.path.insert(0, os.getcwd() + "/..") +#sys.path.insert(0, os.getcwd() + "/..") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it From 7de5896e05f24ac87a56e0a416350f5c6cf5b9bc Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 13:59:30 +0000 Subject: [PATCH 149/197] Added GPy module to path --- doc/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index c9c560db..7cdaea39 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -42,7 +42,10 @@ except ImportError: # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../GPy')) + +print "sys.path.after:", sys.path + # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it From 6f55358a45689147d68657c1fe86b8c9683bf1df Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 8 Feb 2013 14:00:13 +0000 Subject: [PATCH 150/197] Bug found and fixed in plots for normalized X. --- GPy/models/GP.py | 21 ++++++++++++++++----- GPy/models/sparse_GP.py | 3 +-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/GPy/models/GP.py b/GPy/models/GP.py index 5e400c52..c4c37e44 100644 --- a/GPy/models/GP.py +++ b/GPy/models/GP.py @@ -30,7 +30,7 @@ class GP(model): .. Note:: Multiple independent outputs are allowed using columns of Y """ - + #FIXME normalize vs normalise def __init__(self, X, likelihood, kernel, normalize_X=False, Xslices=None): # parse arguments @@ -216,9 +216,14 @@ class GP(model): gpplot(Xnew,m,m-2*np.sqrt(np.diag(v)[:,None]),m+2*np.sqrt(np.diag(v))[:,None]) for i in range(samples): pb.plot(Xnew,Ysim[i,:],Tango.coloursHex['darkBlue'],linewidth=0.25) - pb.plot(self.X[which_data],self.likelihood.Y[which_data],'kx',mew=1.5) pb.xlim(xmin,xmax) + ymin,ymax = min(np.append(self.likelihood.Y,m-2*np.sqrt(np.diag(v)[:,None]))), max(np.append(self.likelihood.Y,m+2*np.sqrt(np.diag(v)[:,None]))) + ymin, ymax = ymin - 0.1*(ymax - ymin), ymax + 0.1*(ymax - ymin) + pb.ylim(ymin,ymax) + if hasattr(self,'Z'): + pb.plot(self.Z,self.Z*0+pb.ylim()[0],'r|',mew=1.5,markersize=12) + elif self.X.shape[1] == 2: resolution = resolution or 50 Xnew, xmin, xmax, xx, yy = x_frame2D(self.X, plot_limits,resolution) @@ -239,16 +244,22 @@ class GP(model): which_data = slice(None) if self.X.shape[1] == 1: - Xnew, xmin, xmax = x_frame1D(self.X, plot_limits=plot_limits) + + Xu = self.X * self._Xstd + self._Xmean #NOTE self.X are the normalized values now + + Xnew, xmin, xmax = x_frame1D(Xu, plot_limits=plot_limits) m, var, lower, upper = self.predict(Xnew, slices=which_functions) gpplot(Xnew,m, lower, upper) - pb.plot(self.X[which_data],self.likelihood.data[which_data],'kx',mew=1.5) + pb.plot(Xu[which_data],self.likelihood.data[which_data],'kx',mew=1.5) ymin,ymax = min(np.append(self.likelihood.data,lower)), max(np.append(self.likelihood.data,upper)) ymin, ymax = ymin - 0.1*(ymax - ymin), ymax + 0.1*(ymax - ymin) pb.xlim(xmin,xmax) pb.ylim(ymin,ymax) + if hasattr(self,'Z'): + Zu = self.Z*self._Xstd + self._Xmean + pb.plot(Zu,Zu*0+pb.ylim()[0],'r|',mew=1.5,markersize=12) - elif self.X.shape[1]==2: + elif self.X.shape[1]==2: #FIXME resolution = resolution or 50 Xnew, xx, yy, xmin, xmax = x_frame2D(self.X, plot_limits,resolution) x, y = np.linspace(xmin[0],xmax[0],resolution), np.linspace(xmin[1],xmax[1],resolution) diff --git a/GPy/models/sparse_GP.py b/GPy/models/sparse_GP.py index a90f73cb..3239d462 100644 --- a/GPy/models/sparse_GP.py +++ b/GPy/models/sparse_GP.py @@ -198,7 +198,7 @@ class sparse_GP(GP): mu = mdot(Kx.T, self.C/self.scale_factor, self.psi1V) if full_cov: Kxx = self.kern.K(Xnew) - var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) #NOTE thiswon't work for plotting + var = Kxx - mdot(Kx.T, (self.Kmmi - self.C/self.scale_factor**2), Kx) #NOTE this won't work for plotting else: Kxx = self.kern.Kdiag(Xnew) var = Kxx - np.sum(Kx*np.dot(self.Kmmi - self.C/self.scale_factor**2, Kx),0) @@ -211,7 +211,6 @@ class sparse_GP(GP): """ GP.plot(self,*args,**kwargs) if self.Q==1: - pb.plot(self.Z,self.Z*0+pb.ylim()[0],'k|',mew=1.5,markersize=12) if self.has_uncertain_inputs: pb.errorbar(self.X[:,0], pb.ylim()[0]+np.zeros(self.N), xerr=2*np.sqrt(self.X_uncertainty.flatten())) if self.Q==2: From ffd4bbbe547bff748e19b35ba0d57f49ddf5db09 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:03:48 +0000 Subject: [PATCH 151/197] Removed path again --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 7cdaea39..ed7e5f65 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -42,9 +42,9 @@ except ImportError: # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../GPy')) +#sys.path.insert(0, os.path.abspath('../GPy')) -print "sys.path.after:", sys.path +#print "sys.path.after:", sys.path # If your extensions are in another directory, add it here. If the directory @@ -115,7 +115,7 @@ for mod_name in MOCK_MODULES: # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -#import GPy +import GPy if on_rtd: sys.path.append("../GPy") From cd0363cbeeb1b3d8ba8aa626620f14cf3d39b830 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:07:32 +0000 Subject: [PATCH 152/197] Moved GPy import --- doc/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ed7e5f65..91599d1b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -103,7 +103,6 @@ class Mock(object): else: return Mock() -#sys.path.append("../GPy") #import mock print "Mocking" @@ -115,10 +114,9 @@ for mod_name in MOCK_MODULES: # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -import GPy - if on_rtd: sys.path.append("../GPy") + import GPy os.system("pwd") os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") From 915d8fc25ac8b1594e54b7c249b61bae4e1f21d3 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:12:18 +0000 Subject: [PATCH 153/197] Adding more paths...: --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 91599d1b..69fde65e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -115,8 +115,8 @@ for mod_name in MOCK_MODULES: on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: - sys.path.append("../GPy") - import GPy + sys.path.insert(0, os.getcwd() + "/../GPy") + #sys.path.append("../GPy") os.system("pwd") os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") From 461be357fba0cfadc28286a888f27e9128afbeae Mon Sep 17 00:00:00 2001 From: Ricardo Andrade Date: Fri, 8 Feb 2013 14:18:59 +0000 Subject: [PATCH 154/197] Broken file removed until new notice. --- GPy/models/__init__.py | 1 - GPy/models/generalized_FITC.py | 242 --------------------------------- 2 files changed, 243 deletions(-) delete mode 100644 GPy/models/generalized_FITC.py diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index 9cc8fa68..8e2c5d84 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -8,7 +8,6 @@ from sparse_GP import sparse_GP from sparse_GP_regression import sparse_GP_regression from GPLVM import GPLVM from warped_GP import warpedGP -# TODO: from generalized_FITC import generalized_FITC from sparse_GPLVM import sparse_GPLVM #from uncollapsed_sparse_GP import uncollapsed_sparse_GP from BGPLVM import Bayesian_GPLVM diff --git a/GPy/models/generalized_FITC.py b/GPy/models/generalized_FITC.py deleted file mode 100644 index 5c88d44d..00000000 --- a/GPy/models/generalized_FITC.py +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright (c) 2012, GPy authors (see AUTHORS.txt). -# Licensed under the BSD 3-clause license (see LICENSE.txt) - - -import numpy as np -import pylab as pb -from scipy import stats, linalg -from .. import kern -from ..core import model -from ..util.linalg import pdinv,mdot -from ..util.plot import gpplot -#from ..inference.Expectation_Propagation import FITC -from ..likelihoods.EP import FITC -from ..likelihoods import likelihood,probit - -class generalized_FITC(model): - def __init__(self,X,likelihood,kernel=None,inducing=10,epsilon_ep=1e-3,powerep=[1.,1.]): - """ - Naish-Guzman, A. and Holden, S. (2008) implemantation of EP with FITC. - - :param X: input observations - :param likelihood: Output's likelihood (likelihood class) - :param kernel: a GPy kernel - :param inducing: Either an array specifying the inducing points location or a scalar defining their number. - :param epsilon_ep: EP convergence criterion, maximum squared difference allowed between mean updates to stop iterations (float) - :param powerep: Power-EP parameters (eta,delta) - 2x1 numpy array (floats) - """ - assert isinstance(kernel,kern.kern) - self.likelihood = likelihood - self.Y = self.likelihood.Y - self.kernel = kernel - self.X = X - self.N, self.D = self.X.shape - assert self.Y.shape[0] == self.N - if type(inducing) == int: - self.M = inducing - self.Z = (np.random.random_sample(self.D*self.M)*(self.X.max()-self.X.min())+self.X.min()).reshape(self.M,-1) - elif type(inducing) == np.ndarray: - self.Z = inducing - self.M = self.Z.shape[0] - self.eta,self.delta = powerep - self.epsilon_ep = epsilon_ep - self.jitter = 1e-12 - model.__init__(self) - - def _set_params(self,p): - self.kernel._set_params_transformed(p[0:-self.Z.size]) - self.Z = p[-self.Z.size:].reshape(self.M,self.D) - - def _get_params(self): - return np.hstack([self.kernel._get_params_transformed(),self.Z.flatten()]) - - def _get_param_names(self): - return self.kernel._get_param_names_transformed()+['iip_%i'%i for i in range(self.Z.size)] - - def approximate_likelihood(self): - self.Kmm = self.kernel.K(self.Z) - self.Knm = self.kernel.K(self.X,self.Z) - self.Knn_diag = self.kernel.Kdiag(self.X) - self.ep_approx = FITC(self.Kmm,self.likelihood,self.Knm.T,self.Knn_diag,epsilon=self.epsilon_ep,powerep=[self.eta,self.delta]) - self.ep_approx.fit_EP() - - def posterior_param(self): - self.Knn_diag = self.kernel.Kdiag(self.X) - self.Kmm = self.kernel.K(self.Z) - self.Kmmi, self.Lmm, self.Lmmi, self.Kmm_logdet = pdinv(self.Kmm) - self.Knm = self.kernel.K(self.X,self.Z) - self.KmmiKmn = np.dot(self.Kmmi,self.Knm.T) - self.Qnn = np.dot(self.Knm,self.KmmiKmn) - self.Diag0 = self.Knn_diag - np.diag(self.Qnn) - self.R0 = np.linalg.cholesky(self.Kmmi).T - - self.Taut = self.ep_approx.tau_tilde/(1.+ self.ep_approx.tau_tilde*self.Diag0) - self.KmnTaut = self.Knm.T*self.Taut[None,:] - self.KmnTautKnm = np.dot(self.KmnTaut, self.Knm) - self.Woodbury_inv, self.Wood_L, self.Wood_Li, self.Woodbury_logdet = pdinv(self.Kmm + self.KmnTautKnm) - self.Qnn_diag = self.Knn_diag - np.diag(self.Qnn) + 1./self.ep_approx.tau_tilde - self.Qi = -np.dot(self.KmnTaut.T, np.dot(self.Woodbury_inv,self.KmnTaut)) + np.diag(self.Taut) - self.hld = 0.5*np.sum(np.log(self.Diag0 + 1./self.ep_approx.tau_tilde)) - 0.5*self.Kmm_logdet + 0.5*self.Woodbury_logdet - - self.Diag = self.Diag0/(1.+ self.Diag0 * self.ep_approx.tau_tilde) - self.P = (self.Diag / self.Diag0)[:,None] * self.Knm - self.RPT0 = np.dot(self.R0,self.Knm.T) - self.L = np.linalg.cholesky(np.eye(self.M) + np.dot(self.RPT0,(1./self.Diag0 - self.Diag/(self.Diag0**2))[:,None]*self.RPT0.T)) - self.R,info = linalg.flapack.dtrtrs(self.L,self.R0,lower=1) - self.RPT = np.dot(self.R,self.P.T) - self.Sigma = np.diag(self.Diag) + np.dot(self.RPT.T,self.RPT) - self.w = self.Diag * self.ep_approx.v_tilde - self.gamma = np.dot(self.R.T, np.dot(self.RPT,self.ep_approx.v_tilde)) - self.mu = self.w + np.dot(self.P,self.gamma) - self.mu_tilde = (self.ep_approx.v_tilde/self.ep_approx.tau_tilde)[:,None] - - def log_likelihood(self): - self.posterior_param() - self.YYT = np.dot(self.mu_tilde,self.mu_tilde.T) - A = -self.hld - B = -.5*np.sum(self.Qi*self.YYT) - C = sum(np.log(self.ep_approx.Z_hat)) - D = .5*np.sum(np.log(1./self.ep_approx.tau_tilde + 1./self.ep_approx.tau_)) - E = .5*np.sum((self.ep_approx.v_/self.ep_approx.tau_ - self.mu_tilde.flatten())**2/(1./self.ep_approx.tau_ + 1./self.ep_approx.tau_tilde)) - return A + B + C + D + E - - def _log_likelihood_gradients(self): - dKmm_dtheta = self.kernel.dK_dtheta(self.Z) - dKnn_dtheta = self.kernel.dK_dtheta(self.X) - dKmn_dtheta = self.kernel.dK_dtheta(self.Z,self.X) - dKmm_dZ = -self.kernel.dK_dX(self.Z) - dKnm_dZ = -self.kernel.dK_dX(self.X,self.Z) - tmp = [np.dot(dKmn_dtheta_i,self.KmmiKmn) for dKmn_dtheta_i in dKmn_dtheta.T] - dQnn_dtheta = [tmp_i + tmp_i.T - np.dot(np.dot(self.KmmiKmn.T,dKmm_dtheta_i),self.KmmiKmn) for tmp_i,dKmm_dtheta_i in zip(tmp,dKmm_dtheta.T)] - dDiag0_dtheta = [np.diag(dKnn_dtheta_i) - np.diag(dQnn_dtheta_i) for dKnn_dtheta_i,dQnn_dtheta_i in zip(dKnn_dtheta.T,dQnn_dtheta)] - dQ_dtheta = [np.diag(dDiag0_dtheta_i) + dQnn_dtheta_i for dDiag0_dtheta_i,dQnn_dtheta_i in zip(dDiag0_dtheta,dQnn_dtheta)] - dW_dtheta = [dKmm_dtheta_i + 2*np.dot(self.KmnTaut,dKmn_dtheta_i) - np.dot(self.KmnTaut*dDiag0_dtheta_i,self.KmnTaut.T) for dKmm_dtheta_i,dDiag0_dtheta_i,dKmn_dtheta_i in zip(dKmm_dtheta.T,dDiag0_dtheta,dKmn_dtheta.T)] - - QiY = np.dot(self.Qi, self.mu_tilde) - QiYYQi = np.outer(QiY,QiY) - WiKmnTaut = np.dot(self.Woodbury_inv,self.KmnTaut) - K_Y = np.dot(self.KmmiKmn,QiY) - # gradient - theta - Atheta = [-0.5*np.dot(self.Taut,dDiag0_dtheta_i) + 0.5*np.sum(self.Kmmi*dKmm_dtheta_i) - 0.5*np.sum(self.Woodbury_inv*dW_dtheta_i) for dDiag0_dtheta_i,dKmm_dtheta_i,dW_dtheta_i in zip(dDiag0_dtheta,dKmm_dtheta.T,dW_dtheta)] - Btheta = np.array([0.5*np.sum(QiYYQi*dQ_dtheta_i) for dQ_dtheta_i in dQ_dtheta]) - dL_dtheta = Atheta + Btheta - # gradient - Z - # Az - dQnn_dZ_diag_a2 = (np.array([d[:,:,None]*self.KmmiKmn[:,:,None] for d in dKnm_dZ.transpose(2,0,1)]).reshape(self.D,self.M,self.N)).transpose(1,2,0) - dQnn_dZ_diag_b2 = (np.array([(self.KmmiKmn*np.sum(d[:,:,None]*self.KmmiKmn,-2))[:,:,None] for d in dKmm_dZ.transpose(2,0,1)]).reshape(self.D,self.M,self.N)).transpose(1,2,0) - dQnn_dZ_diag = dQnn_dZ_diag_a2 - dQnn_dZ_diag_b2 - d_hld_Diag1_dZ = -np.sum(np.dot(self.KmmiKmn*self.Taut,self.KmmiKmn.T)[:,:,None]*dKmm_dZ,-2) + np.sum((self.KmmiKmn*self.Taut)[:,:,None]*dKnm_dZ,-2) - d_hld_Kmm_dZ = np.sum(self.Kmmi[:,:,None]*dKmm_dZ,-2) - d_hld_W_dZ1 = np.sum(WiKmnTaut[:,:,None]*dKnm_dZ,-2) - d_hld_W_dZ3 = np.sum(self.Woodbury_inv[:,:,None]*dKmm_dZ,-2) - d_hld_W_dZ2 = np.array([np.sum(np.sum(WiKmnTaut.T*d[:,:,None]*self.KmnTaut.T,-2),-1) for d in dQnn_dZ_diag.transpose(2,0,1)]).T - Az = d_hld_Diag1_dZ + d_hld_Kmm_dZ - d_hld_W_dZ1 - d_hld_W_dZ2 - d_hld_W_dZ3 - # Bz - Bz2 = np.sum(np.dot(K_Y,QiY.T)[:,:,None]*dKnm_dZ,-2) - Bz3 = - np.sum(np.dot(K_Y,K_Y.T)[:,:,None]*dKmm_dZ,-2) - Bz1 = -np.array([np.sum((QiY**2)*d[:,:,None],-2) for d in dQnn_dZ_diag.transpose(2,0,1)]).reshape(self.D,self.M).T - Bz = Bz1 + Bz2 + Bz3 - dL_dZ = (Az + Bz).flatten() - return np.hstack([dL_dtheta, dL_dZ]) - - def predict(self,X): - """ - Make a prediction for the vsGP model - - Arguments - --------- - X : Input prediction data - Nx1 numpy array (floats) - """ - #TODO: check output dimensions - K_x = self.kernel.K(self.Z,X) - Kxx = self.kernel.K(X) - #K_x = self.kernM.cross.K(X) - # q(u|f) = N(u| R0i*mu_u*f, R0i*C*R0i.T) - - # Ci = I + (RPT0)Di(RPT0).T - # C = I - [RPT0] * (D+[RPT0].T*[RPT0])^-1*[RPT0].T - # = I - [RPT0] * (D + self.Qnn)^-1 * [RPT0].T - # = I - [RPT0] * (U*U.T)^-1 * [RPT0].T - # = I - V.T * V - U = np.linalg.cholesky(np.diag(self.Diag0) + self.Qnn) - V,info = linalg.flapack.dtrtrs(U,self.RPT0.T,lower=1) - C = np.eye(self.M) - np.dot(V.T,V) - mu_u = np.dot(C,self.RPT0)*(1./self.Diag0[None,:]) - #self.C = C - #self.RPT0 = np.dot(self.R0,self.Knm.T) P0.T - #self.mu_u = mu_u - #self.U = U - # q(u|y) = N(u| R0i*mu_H,R0i*Sigma_H*R0i.T) - mu_H = np.dot(mu_u,self.mu) - self.mu_H = mu_H - Sigma_H = C + np.dot(mu_u,np.dot(self.Sigma,mu_u.T)) - # q(f_star|y) = N(f_star|mu_star,sigma2_star) - KR0T = np.dot(K_x.T,self.R0.T) - mu_star = np.dot(KR0T,mu_H) - sigma2_star = Kxx + np.dot(KR0T,np.dot(Sigma_H - np.eye(self.M),KR0T.T)) - vdiag = np.diag(sigma2_star) - # q(y_star|y) = non-gaussian posterior probability of class membership - p = self.likelihood.predictive_mean(mu_star,vdiag) - return mu_star,vdiag,p - - def plot(self): - """ - Plot the fitted model: training function values, inducing points used, mean estimate and confidence intervals. - """ - if self.X.shape[1]==1: - pb.figure() - xmin,xmax = np.r_[self.X,self.Z].min(),np.r_[self.X,self.Z].max() - xmin, xmax = xmin-0.2*(xmax-xmin), xmax+0.2*(xmax-xmin) - Xnew = np.linspace(xmin,xmax,100)[:,None] - mu_f, var_f, mu_phi = self.predict(Xnew) - self.mu_inducing,self.var_diag_inducing,self.phi_inducing = self.predict(self.Z) - pb.subplot(211) - self.likelihood.plot1Da(X_new=Xnew,Mean_new=mu_f,Var_new=var_f,X_u=self.Z,Mean_u=self.mu_inducing,Var_u=self.var_diag_inducing) - pb.subplot(212) - self.likelihood.plot1Db(self.X,Xnew,mu_phi,self.Z) - elif self.X.shape[1]==2: - pb.figure() - x1min,x1max = self.X[:,0].min(0),self.X[:,0].max(0) - x2min,x2max = self.X[:,1].min(0),self.X[:,1].max(0) - x1min, x1max = x1min-0.2*(x1max-x1min), x1max+0.2*(x1max-x1min) - x2min, x2max = x2min-0.2*(x2max-x2min), x2max+0.2*(x1max-x1min) - axis1 = np.linspace(x1min,x1max,50) - axis2 = np.linspace(x2min,x2max,50) - XX1, XX2 = [e.flatten() for e in np.meshgrid(axis1,axis2)] - Xnew = np.c_[XX1.flatten(),XX2.flatten()] - f,v,p = self.predict(Xnew) - self.likelihood.plot2D(self.X,Xnew,p,self.Z) - else: - raise NotImplementedError, "Cannot plot GPs with more than two input dimensions" - - def em(self,max_f_eval=1e4,epsilon=.1,plot_all=False): #TODO check this makes sense - """ - Fits sparse_EP and optimizes the hyperparametes iteratively until convergence is achieved. - """ - self.epsilon_em = epsilon - log_likelihood_change = self.epsilon_em + 1. - self.parameters_path = [self.kernel._get_params()] - self.approximate_likelihood() - self.site_approximations_path = [[self.ep_approx.tau_tilde,self.ep_approx.v_tilde]] - self.inducing_inputs_path = [self.Z] - self.log_likelihood_path = [self.log_likelihood()] - iteration = 0 - while log_likelihood_change > self.epsilon_em: - print 'EM iteration', iteration - self.optimize(max_f_eval = max_f_eval) - log_likelihood_new = self.log_likelihood() - log_likelihood_change = log_likelihood_new - self.log_likelihood_path[-1] - if log_likelihood_change < 0: - print 'log_likelihood decrement' - self.kernel._set_params_transformed(self.parameters_path[-1]) - self.kernM = self.kernel.copy() - slef.kernM.expand_X(self.iducing_inputs_path[-1]) - self.__init__(self.kernel,self.likelihood,kernM=self.kernM,powerep=[self.eta,self.delta],epsilon_ep = self.epsilon_ep, epsilon_em = self.epsilon_em) - - else: - self.approximate_likelihood() - self.log_likelihood_path.append(self.log_likelihood()) - self.parameters_path.append(self.kernel._get_params()) - self.site_approximations_path.append([self.ep_approx.tau_tilde,self.ep_approx.v_tilde]) - self.inducing_inputs_path.append(self.Z) - iteration += 1 From 9e7f33d6f14c7fbd8d7dc5acc163fe63ff705be9 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:19:29 +0000 Subject: [PATCH 155/197] Just testing an absolute import --- GPy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index a0628c42..3ac8d622 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -from .. import likelihoods +from GPy import likelihoods class model(parameterised): def __init__(self): From e59266af6eb01d0aabd2a40e5c3e9ae9e88ae03d Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:21:37 +0000 Subject: [PATCH 156/197] Back to original --- GPy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index 3ac8d622..a0628c42 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -from GPy import likelihoods +from .. import likelihoods class model(parameterised): def __init__(self): From e421ce664700d5a8388d25a47dee142284e4af32 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:24:26 +0000 Subject: [PATCH 157/197] Make paths more relative --- doc/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 69fde65e..d532e447 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -115,10 +115,10 @@ for mod_name in MOCK_MODULES: on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: - sys.path.insert(0, os.getcwd() + "/../GPy") - #sys.path.append("../GPy") + #sys.path.insert(0, os.getcwd() + "/../GPy") + sys.path.append(os.getcwd() + "/../GPy") os.system("pwd") - os.system("sphinx-apidoc -f -o . ../GPy") + os.system("sphinx-apidoc -f -o . " + os.getcwd() + "/../GPy") #os.system("cd ..") #os.system("cd ./docs") From c7f73c7aa12674730fd2cdb78a4fcda4c06f11d7 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:31:48 +0000 Subject: [PATCH 158/197] Again playing with relative to absolute paths, test --- GPy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index a0628c42..0137f4a5 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -from .. import likelihoods +import GPy.likelihoods class model(parameterised): def __init__(self): From e4ecbe2e8898e414d804c9a1d29a772c2f0fffb7 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:32:43 +0000 Subject: [PATCH 159/197] Revert --- GPy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index 0137f4a5..a0628c42 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -import GPy.likelihoods +from .. import likelihoods class model(parameterised): def __init__(self): From 6cd72f7690b301ee3a1a13fea2e7299c3881296e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:37:33 +0000 Subject: [PATCH 160/197] Added normpath... and debugging --- doc/conf.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index d532e447..ae3c67be 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,6 +37,9 @@ try: except ImportError: print "no sphinx" +APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../..')) +sys.path.insert(0, APP_DIR) + #sys.path.insert(0, os.getcwd() + "/..") # If extensions (or modules to document with autodoc) are in another directory, @@ -116,9 +119,11 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: #sys.path.insert(0, os.getcwd() + "/../GPy") - sys.path.append(os.getcwd() + "/../GPy") + #sys.path.append(os.getcwd() + "/../GPy") + print "I am here" os.system("pwd") - os.system("sphinx-apidoc -f -o . " + os.getcwd() + "/../GPy") + os.system("ls ../") + os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") #os.system("cd ./docs") From e657f532c7f78303f0865d8f6db71e230c26856d Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:42:19 +0000 Subject: [PATCH 161/197] More path debugging --- doc/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ae3c67be..c08c6516 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,6 +39,8 @@ except ImportError: APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../..')) sys.path.insert(0, APP_DIR) +sys.path.insert(0, os.path.abspath('../GPy')) +print "sys.path:", sys.path #sys.path.insert(0, os.getcwd() + "/..") @@ -121,8 +123,8 @@ if on_rtd: #sys.path.insert(0, os.getcwd() + "/../GPy") #sys.path.append(os.getcwd() + "/../GPy") print "I am here" - os.system("pwd") - os.system("ls ../") + print(os.system("pwd")) + print(os.system("ls ../")) os.system("sphinx-apidoc -f -o . ../GPy") #os.system("cd ..") #os.system("cd ./docs") From 6580aecfe01a560bc0e8d77ab990bc9991e81d1a Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 14:54:39 +0000 Subject: [PATCH 162/197] Added debuggin, and interestingly likelihood rst.. shouldnt make a difference: --- doc/GPy.examples.rst | 16 +++++++++++++++ doc/GPy.inference.rst | 16 --------------- doc/GPy.likelihoods.rst | 43 +++++++++++++++++++++++++++++++++++++++++ doc/GPy.models.rst | 16 +++++++-------- doc/GPy.rst | 1 + doc/conf.py | 19 +++++++++++++++--- 6 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 doc/GPy.likelihoods.rst diff --git a/doc/GPy.examples.rst b/doc/GPy.examples.rst index 244e3012..59ffd43d 100644 --- a/doc/GPy.examples.rst +++ b/doc/GPy.examples.rst @@ -33,6 +33,14 @@ examples Package :undoc-members: :show-inheritance: +:mod:`poisson` Module +--------------------- + +.. automodule:: GPy.examples.poisson + :members: + :undoc-members: + :show-inheritance: + :mod:`regression` Module ------------------------ @@ -57,6 +65,14 @@ examples Package :undoc-members: :show-inheritance: +:mod:`sparse_ep_fix` Module +--------------------------- + +.. automodule:: GPy.examples.sparse_ep_fix + :members: + :undoc-members: + :show-inheritance: + :mod:`uncertain_input_GP_regression_demo` Module ------------------------------------------------ diff --git a/doc/GPy.inference.rst b/doc/GPy.inference.rst index 6f4ab691..357e70c7 100644 --- a/doc/GPy.inference.rst +++ b/doc/GPy.inference.rst @@ -1,22 +1,6 @@ inference Package ================= -:mod:`Expectation_Propagation` Module -------------------------------------- - -.. automodule:: GPy.inference.Expectation_Propagation - :members: - :undoc-members: - :show-inheritance: - -:mod:`likelihoods` Module -------------------------- - -.. automodule:: GPy.inference.likelihoods - :members: - :undoc-members: - :show-inheritance: - :mod:`optimization` Module -------------------------- diff --git a/doc/GPy.likelihoods.rst b/doc/GPy.likelihoods.rst new file mode 100644 index 00000000..34672d11 --- /dev/null +++ b/doc/GPy.likelihoods.rst @@ -0,0 +1,43 @@ +likelihoods Package +=================== + +:mod:`likelihoods` Package +-------------------------- + +.. automodule:: GPy.likelihoods + :members: + :undoc-members: + :show-inheritance: + +:mod:`EP` Module +---------------- + +.. automodule:: GPy.likelihoods.EP + :members: + :undoc-members: + :show-inheritance: + +:mod:`Gaussian` Module +---------------------- + +.. automodule:: GPy.likelihoods.Gaussian + :members: + :undoc-members: + :show-inheritance: + +:mod:`likelihood` Module +------------------------ + +.. automodule:: GPy.likelihoods.likelihood + :members: + :undoc-members: + :show-inheritance: + +:mod:`likelihood_functions` Module +---------------------------------- + +.. automodule:: GPy.likelihoods.likelihood_functions + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/GPy.models.rst b/doc/GPy.models.rst index b0a7a298..8837ac4e 100644 --- a/doc/GPy.models.rst +++ b/doc/GPy.models.rst @@ -17,18 +17,18 @@ models Package :undoc-members: :show-inheritance: -:mod:`GPLVM` Module -------------------- +:mod:`GP` Module +---------------- -.. automodule:: GPy.models.GPLVM +.. automodule:: GPy.models.GP :members: :undoc-members: :show-inheritance: -:mod:`GP_EP` Module +:mod:`GPLVM` Module ------------------- -.. automodule:: GPy.models.GP_EP +.. automodule:: GPy.models.GPLVM :members: :undoc-members: :show-inheritance: @@ -41,10 +41,10 @@ models Package :undoc-members: :show-inheritance: -:mod:`generalized_FITC` Module ------------------------------- +:mod:`sparse_GP` Module +----------------------- -.. automodule:: GPy.models.generalized_FITC +.. automodule:: GPy.models.sparse_GP :members: :undoc-members: :show-inheritance: diff --git a/doc/GPy.rst b/doc/GPy.rst index d3c1e843..3fd4bcfd 100644 --- a/doc/GPy.rst +++ b/doc/GPy.rst @@ -18,6 +18,7 @@ Subpackages GPy.examples GPy.inference GPy.kern + GPy.likelihoods GPy.models GPy.util diff --git a/doc/conf.py b/doc/conf.py index c08c6516..e1ce8796 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -119,13 +119,26 @@ for mod_name in MOCK_MODULES: # ----------------------- READTHEDOCS ------------------ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = True if on_rtd: #sys.path.insert(0, os.getcwd() + "/../GPy") #sys.path.append(os.getcwd() + "/../GPy") print "I am here" - print(os.system("pwd")) - print(os.system("ls ../")) - os.system("sphinx-apidoc -f -o . ../GPy") + #print(os.system("pwd")) + #print(os.system("ls ../")) + #os.system("sphinx-apidoc -f -o . ../GPy") + + import subprocess + + proc = subprocess.Popen("pwd", stdout=subprocess.PIPE, shell=True) + (out, err) = proc.communicate() + print "program output:", out + proc = subprocess.Popen("ls ../", stdout=subprocess.PIPE, shell=True) + (out, err) = proc.communicate() + print "program output:", out + proc = subprocess.Popen("sphinx-apidoc -f -o . ../GPy", stdout=subprocess.PIPE, shell=True) + (out, err) = proc.communicate() + print "program output:", out #os.system("cd ..") #os.system("cd ./docs") From 95d4890e84babbb3ec8b0581962f39b324909767 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:08:00 +0000 Subject: [PATCH 163/197] Changed path down a level --- doc/conf.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index e1ce8796..1d67f6c1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ try: except ImportError: print "no sphinx" -APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../..')) +APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) sys.path.insert(0, APP_DIR) sys.path.insert(0, os.path.abspath('../GPy')) print "sys.path:", sys.path @@ -121,12 +121,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = True if on_rtd: - #sys.path.insert(0, os.getcwd() + "/../GPy") - #sys.path.append(os.getcwd() + "/../GPy") print "I am here" - #print(os.system("pwd")) - #print(os.system("ls ../")) - #os.system("sphinx-apidoc -f -o . ../GPy") import subprocess From 6ded811f79b0bdd443264cdbc7e55747b69fbc0e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:08:53 +0000 Subject: [PATCH 164/197] Added paths back --- doc/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 1d67f6c1..63070df2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -38,7 +38,9 @@ except ImportError: print "no sphinx" APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) +PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) sys.path.insert(0, APP_DIR) +sys.path.insert(0, PACKAGE_DIR1) sys.path.insert(0, os.path.abspath('../GPy')) print "sys.path:", sys.path From ced1318689b6d66f11e8ddf43449bd2306f1a847 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:11:22 +0000 Subject: [PATCH 165/197] Changed paths and model.py --- GPy/core/model.py | 4 ++-- doc/conf.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index a0628c42..c62037a6 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -from .. import likelihoods +from ..likelihoods import EP class model(parameterised): def __init__(self): @@ -402,7 +402,7 @@ class model(parameterised): :type optimzer: string TODO: valid strings? """ - assert isinstance(self.likelihood,likelihoods.EP), "EM is not available for Gaussian likelihoods" + assert isinstance(self.likelihood,EP), "EM is not available for Gaussian likelihoods" log_change = epsilon + 1. self.log_likelihood_record = [] self.gp_params_record = [] diff --git a/doc/conf.py b/doc/conf.py index 63070df2..03a941ff 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -38,10 +38,10 @@ except ImportError: print "no sphinx" APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) -PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) +#PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) sys.path.insert(0, APP_DIR) -sys.path.insert(0, PACKAGE_DIR1) -sys.path.insert(0, os.path.abspath('../GPy')) +#sys.path.insert(0, PACKAGE_DIR1) +#sys.path.insert(0, os.path.abspath('../GPy')) print "sys.path:", sys.path #sys.path.insert(0, os.getcwd() + "/..") From 716c31e416eed286d7099ee9e9c627b65bd4518f Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:12:40 +0000 Subject: [PATCH 166/197] Removed all paths adding --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 03a941ff..afca1e57 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,9 +37,9 @@ try: except ImportError: print "no sphinx" -APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) +#APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) #PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) -sys.path.insert(0, APP_DIR) +#sys.path.insert(0, APP_DIR) #sys.path.insert(0, PACKAGE_DIR1) #sys.path.insert(0, os.path.abspath('../GPy')) print "sys.path:", sys.path From 96971da3a7a2ed3fa02f2ace69093fc6b4f2db31 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:14:41 +0000 Subject: [PATCH 167/197] Appended path --- doc/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index afca1e57..89bf102c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -125,6 +125,8 @@ on_rtd = True if on_rtd: print "I am here" + sys.path.append('../GPy') + import subprocess proc = subprocess.Popen("pwd", stdout=subprocess.PIPE, shell=True) From 1c6313261dc7ab45a8cd9898c6834564a974973b Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:20:07 +0000 Subject: [PATCH 168/197] Changed path back, think we're closer when its in --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 89bf102c..141a3355 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,6 +37,9 @@ try: except ImportError: print "no sphinx" +parent = os.path.dirname(os.path.dirname(__file__)) +sys.path.append(os.path.abspath(parent)) + #APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) #PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) #sys.path.insert(0, APP_DIR) @@ -125,8 +128,6 @@ on_rtd = True if on_rtd: print "I am here" - sys.path.append('../GPy') - import subprocess proc = subprocess.Popen("pwd", stdout=subprocess.PIPE, shell=True) From e9e0187a985b921e16191e1cbf9aebde8f898dec Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:22:55 +0000 Subject: [PATCH 169/197] With insert --- doc/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 141a3355..44e908e0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -38,7 +38,10 @@ except ImportError: print "no sphinx" parent = os.path.dirname(os.path.dirname(__file__)) -sys.path.append(os.path.abspath(parent)) +sys.path.insert(0, os.path.abspath(parent)) + +parent = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy') +sys.path.insert(0, os.path.abspath(parent)) #APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) #PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) From 3d7bf6698afb531bf19f87cdc2b412d7c264f7e1 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:30:51 +0000 Subject: [PATCH 170/197] Now hacking makefile..: --- doc/Makefile | 4 ++++ doc/conf.py | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index faa4ed65..ffc600c8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -41,6 +41,10 @@ help: clean: -rm -rf $(BUILDDIR)/* +CWD=$(shell sphinx-apidoc -f -o . ../GPy) +all: + @echo api build: $(CWD). + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff --git a/doc/conf.py b/doc/conf.py index 44e908e0..c3c9cde8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,11 +37,13 @@ try: except ImportError: print "no sphinx" -parent = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(0, os.path.abspath(parent)) +parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +sys.path.insert(0, parent) +sys.path.append(parent) -parent = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy') -sys.path.insert(0, os.path.abspath(parent)) +parent = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy')) +sys.path.insert(0, parent) +sys.path.append(parent) #APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) #PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) From dda0198cc0657e5a0c75ff3683669b51c713048c Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:36:23 +0000 Subject: [PATCH 171/197] more api doc hacking --- doc/Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index ffc600c8..9388998e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,6 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = +SPHINXAPI_DOC = sphinx-apidoc SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build @@ -41,11 +42,12 @@ help: clean: -rm -rf $(BUILDDIR)/* -CWD=$(shell sphinx-apidoc -f -o . ../GPy) -all: - @echo api build: $(CWD). html: + $(SPHINXAPI_DOC) -f -o . ../GPy + @echo + @echo "API doc finished." + @echo $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." From b9325711000a725398214de91cefc82e7eadc7a0 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 15:42:02 +0000 Subject: [PATCH 172/197] Try changing the setup... --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1c696b86..1aa2f0f0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- import os -from numpy.distutils.core import Extension, setup +#from numpy.distutils.core import Extension, setup +from setuptools import setup #from sphinx.setup_command import BuildDoc # Version number @@ -27,6 +28,9 @@ setup(name = 'GPy', #ext_modules = [Extension(name = 'GPy.kern.lfmUpsilonf2py', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], + extras_require = { + 'docs':['Sphinx'], + }, #setup_requires=['sphinx'], #cmdclass = {'build_sphinx': BuildDoc}, classifiers=[ From af3210396e9ff67b03f4729688642a086866596c Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:04:26 +0000 Subject: [PATCH 173/197] Fixed the fecking subpackage... --- doc/conf.py | 8 ++++---- setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index c3c9cde8..0c26700c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,11 +39,11 @@ except ImportError: parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, parent) -sys.path.append(parent) +#sys.path.append(parent) -parent = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy')) -sys.path.insert(0, parent) -sys.path.append(parent) +#parent = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy')) +#sys.path.insert(0, parent) +#sys.path.append(parent) #APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) #PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) diff --git a/setup.py b/setup.py index 1aa2f0f0..acebd624 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup(name = 'GPy', license = "BSD 3-clause", keywords = "machine-learning gaussian-processes kernels", url = "http://ml.sheffield.ac.uk/GPy/", - packages = ['GPy', 'GPy.core', 'GPy.kern', 'GPy.util', 'GPy.models', 'GPy.inference', 'GPy.examples'], + packages = ['GPy', 'GPy.core', 'GPy.kern', 'GPy.util', 'GPy.models', 'GPy.inference', 'GPy.examples', 'GPy.likelihoods'], package_dir={'GPy': 'GPy'}, package_data = {'GPy': ['GPy/examples']}, py_modules = ['GPy.__init__'], From b9414c1b011919500431b81a4341ba8c1b9c5806 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:05:01 +0000 Subject: [PATCH 174/197] Reverted model.py --- GPy/core/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GPy/core/model.py b/GPy/core/model.py index c62037a6..a0628c42 100644 --- a/GPy/core/model.py +++ b/GPy/core/model.py @@ -10,7 +10,7 @@ from parameterised import parameterised, truncate_pad import priors from ..util.linalg import jitchol from ..inference import optimization -from ..likelihoods import EP +from .. import likelihoods class model(parameterised): def __init__(self): @@ -402,7 +402,7 @@ class model(parameterised): :type optimzer: string TODO: valid strings? """ - assert isinstance(self.likelihood,EP), "EM is not available for Gaussian likelihoods" + assert isinstance(self.likelihood,likelihoods.EP), "EM is not available for Gaussian likelihoods" log_change = epsilon + 1. self.log_likelihood_record = [] self.gp_params_record = [] From cb9bb210b4655b0a7c51c48342d81f435fed8799 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:06:11 +0000 Subject: [PATCH 175/197] Removed api make from makefile (although maybe it belongs there? --- doc/Makefile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index 9388998e..95018f47 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,7 +3,6 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXAPI_DOC = sphinx-apidoc SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build @@ -44,10 +43,6 @@ clean: html: - $(SPHINXAPI_DOC) -f -o . ../GPy - @echo - @echo "API doc finished." - @echo $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." From ed7812ae469e644f56dbfaba7e76c71228be9347 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:08:09 +0000 Subject: [PATCH 176/197] No path inset or append --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 0c26700c..ab72789e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,8 +37,8 @@ try: except ImportError: print "no sphinx" -parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -sys.path.insert(0, parent) +#parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +#sys.path.insert(0, parent) #sys.path.append(parent) #parent = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy')) @@ -132,6 +132,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = True if on_rtd: print "I am here" + #sys.path.append(os.path.abspath('../GPy')) import subprocess From bc62b79e49be94fcb72a726072487396daebc088 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 8 Feb 2013 16:10:58 +0000 Subject: [PATCH 177/197] New tutorial draft called 'A kernel overview' --- doc/Figures/kern-def.png | Bin 0 -> 38628 bytes doc/Figures/tuto_GP_regression_m1.png | Bin 54971 -> 32641 bytes doc/Figures/tuto_GP_regression_m2.png | Bin 32176 -> 51925 bytes doc/Figures/tuto_kern_overview_add_orth.png | Bin 0 -> 64730 bytes doc/Figures/tuto_kern_overview_allkern.png | Bin 0 -> 132224 bytes doc/Figures/tuto_kern_overview_basicdef.png | Bin 0 -> 38628 bytes doc/Figures/tuto_kern_overview_mANOVA.png | Bin 0 -> 56323 bytes doc/Figures/tuto_kern_overview_mANOVAdec.png | Bin 0 -> 86347 bytes doc/index.rst | 3 +- doc/tuto_GP_regression.rst | 4 +- doc/tuto_kernel_overview.rst | 169 +++++++++++++++++++ grid_parameters.py | 64 ------- 12 files changed, 172 insertions(+), 68 deletions(-) create mode 100644 doc/Figures/kern-def.png create mode 100644 doc/Figures/tuto_kern_overview_add_orth.png create mode 100644 doc/Figures/tuto_kern_overview_allkern.png create mode 100644 doc/Figures/tuto_kern_overview_basicdef.png create mode 100644 doc/Figures/tuto_kern_overview_mANOVA.png create mode 100644 doc/Figures/tuto_kern_overview_mANOVAdec.png create mode 100644 doc/tuto_kernel_overview.rst delete mode 100644 grid_parameters.py diff --git a/doc/Figures/kern-def.png b/doc/Figures/kern-def.png new file mode 100644 index 0000000000000000000000000000000000000000..bad43b09a26c2ca1566cb59fc763449bb5ae8819 GIT binary patch literal 38628 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 YqCBSGx#a$eJ_lSwMF(D{WEt{507p-mga7~l literal 54971 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 XS?s>ZT>xGp%fR62>gTe~DWM4fDpmQq diff --git a/doc/Figures/tuto_GP_regression_m2.png b/doc/Figures/tuto_GP_regression_m2.png index bf28f4e0b135647ecda1d454cd97ecc86065305d..7e54e9195692d4a56cb5bb960903b38b88f2ff80 100644 GIT binary patch literal 51925 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%0AJs+pdonR)7*}NfaOS{ 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 literal 0 HcmV?d00001 diff --git a/doc/Figures/tuto_kern_overview_allkern.png b/doc/Figures/tuto_kern_overview_allkern.png new file mode 100644 index 0000000000000000000000000000000000000000..f3406b07c07ec42da700e7bcbeda0fbc5b2fccfd GIT binary patch literal 132224 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�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 Kv_Rzf>;DHsk<8ct literal 0 HcmV?d00001 diff --git a/doc/Figures/tuto_kern_overview_basicdef.png b/doc/Figures/tuto_kern_overview_basicdef.png new file mode 100644 index 0000000000000000000000000000000000000000..bad43b09a26c2ca1566cb59fc763449bb5ae8819 GIT binary patch literal 38628 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 literal 0 HcmV?d00001 diff --git a/doc/Figures/tuto_kern_overview_mANOVAdec.png b/doc/Figures/tuto_kern_overview_mANOVAdec.png new file mode 100644 index 0000000000000000000000000000000000000000..ef154263780329375839ebbd149ccd20ebb390e5 GIT binary patch literal 86347 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%0DMImM+(V`wujmbRyP7m6~ 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∈*NUcz3rN?>v@-%Pl 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 literal 0 HcmV?d00001 diff --git a/doc/index.rst b/doc/index.rst index 28690e99..b62ff6a7 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,8 +8,8 @@ Welcome to GPy's documentation! For a quick start, you can have a look at one of the tutorials: * `Basic Gaussian process regression `_ +* `A kernel overview `_ * Advanced GP regression (Forthcoming) -* Kernel manipulation (Forthcoming) * Writting kernels (Forthcoming) You may also be interested by some examples in the GPy/examples folder. @@ -28,4 +28,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 3527e86f..f14afe62 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -12,7 +12,7 @@ We first import the libraries we will need: :: import numpy as np import GPy -1 dimensional model +1-dimensional model =================== For this toy example, we assume we have the following inputs and outputs:: @@ -99,7 +99,7 @@ Once again, we can use ``print(m)`` and ``m.plot()`` to look at the resulting mo GP regression model after optimization of the parameters. -2 dimensional example +2-dimensional example ===================== Here is a 2 dimensional example:: diff --git a/doc/tuto_kernel_overview.rst b/doc/tuto_kernel_overview.rst new file mode 100644 index 00000000..9ef86589 --- /dev/null +++ b/doc/tuto_kernel_overview.rst @@ -0,0 +1,169 @@ + +**************************** +tutorial : A kernel overview +**************************** + +First we import the libraries we will need :: + + import pylab as pb + import numpy as np + import GPy + pb.ion() + +For most kernels, the dimension is the only mandatory parameter to define a kernel object. However, it is also possible to specify the values of the parameters. For example, the three following commands are valid for defining a squared exponential kernel (ie rbf or Gaussian) :: + + ker1 = GPy.kern.rbf(D=1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) + ker2 = GPy.kern.rbf(D=1, variance = 1.5, lengthscale=2.) + ker3 = GPy.kern.rbf(1, .5, .5) + +A `plot` and a `print` functions are implemented to represent kernel objects :: + + print ker1 + + ker1.plot() + ker2.plot() + ker3.plot() + +.. figure:: Figures/tuto_kern_overview_basicdef.png + :align: center + :height: 350px + +:: + + import pylab as pb + import numpy as np + import GPy + pb.ion() + + ker1 = GPy.kern.rbf(D=1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) + ker2 = GPy.kern.rbf(D=1, variance = .75, lengthscale=3.) + ker3 = GPy.kern.rbf(1, .5, .25) + + ker1.plot() + ker2.plot() + ker3.plot() + #pb.savefig("Figures/tuto_kern_overview_basicdef.png") + + kernels = [GPy.kern.rbf(1), GPy.kern.exponential(1), GPy.kern.Matern32(1), GPy.kern.Matern52(1), GPy.kern.Brownian(1), GPy.kern.bias(1), GPy.kern.linear(1), GPy.kern.spline(1), GPy.kern.periodic_exponential(1), GPy.kern.periodic_Matern32(1), GPy.kern.periodic_Matern52(1), GPy.kern.white(1)] + kernel_names = ["GPy.kern.rbf", "GPy.kern.exponential", "GPy.kern.Matern32", "GPy.kern.Matern52", "GPy.kern.Brownian", "GPy.kern.bias", "GPy.kern.linear", "GPy.kern.spline", "GPy.kern.periodic_exponential", "GPy.kern.periodic_Matern32", "GPy.kern.periodic_Matern52", "GPy.kern.white"] + + pb.figure(figsize=(16,12)) + pb.subplots_adjust(wspace=.5, hspace=.5) + for i, kern in enumerate(kernels): + pb.subplot(3,4,i+1) + kern.plot(x=7.5,plot_limits=[0.00001,15.]) + pb.title(kernel_names[i]+ '\n') + #pb.axes([.1,.1,.8,.7]) + #pb.figtext(.5,.9,'Foo Bar', fontsize=18, ha='center') + #pb.figtext(.5,.85,'Lorem ipsum dolor sit amet, consectetur adipiscing elit',fontsize=10,ha='center') + + # actual plot for the noise + i = 11 + X = np.linspace(0.,15.,201) + WN = 0*X + WN[100] = 1. + pb.subplot(3,4,i+1) + pb.plot(X,WN,'b') + +Implemented kernels +=================== + +Many kernels are already implemented in GPy. Here is a summary of most of them: + +.. figure:: Figures/tuto_kern_overview_allkern.png + :align: center + :height: 800px + +On the other hand, it is possible to use the `sympy` package to build new kernels. This will be the subject of another tutorial. + +Operations to combine kernel +============================ + +In ``GPy``, kernel objects can be combined with the usual ``+`` and ``*`` operators. :: + + k1 = GPy.kern.rbf(1,variance=1., lengthscale=2) + k2 = GPy.kern.Matern32(1,variance=1., lengthscale=2) + + ker_add = k1 + k2 + print ker_add + + ker_prod = k1 * k2 + print ker_prod + +Note that by default, the operator ``+`` adds kernels defined on the same input space whereas ``*`` assumes that the kernels are defined on different input spaces. :: + + ker_add.D + ker_prod.D + +In order to add kernels defined on the different input spaces, the required command is:: + + ker_add_orth = k1.add_orthogonal(k2) + +The resulting kernel is + ker_add_orth.plot(plot_limits=[[-10,-10],[10,10]]) + +.. figure:: Figures/tuto_kern_overview_add_orth.png + :align: center + :height: 350px + +Example : Building an ANOVA kernel +================================== + +In two dimensions ANOVA kernels have the following form: + +.. math:: + + k_{ANOVA}(x,y) = \prod_{i=1}^2 (1 + k_i(x_i,y_i)) = 1 + k_1(x_1,y_1) + k_2(x_2,y_2) + k_1(x_1,y_1) \times k_2(x_2,y_2). + +Let us assume that we want to define an ANOVA kernel with a Matern 3/2 kernel for :math:`k_i`. As seen previously, we can define this kernel as follow:: + + k_cst = GPy.kern.bias(1,variance=1.) + k_mat = GPy.kern.Matern52(1,variance=1., lengthscale=3) + Kanova = (k_cst + k_mat) * (k_cst + k_mat) + print Kanova + +Note the ties between the lengthscales of ``Kanova`` to keep the number of lengthscales equal to 2. On the other hand, there are four variance terms in the new parameterization: one for each term of the right hand sign of the equation above. We can illustrate the use of this kernel on a toy example:: + + # sample inputs and outputs + X = np.random.uniform(-3.,3.,(40,2)) + Y = 0.5*X[:,:1] + 0.5*X[:,1:] + 2*np.sin(X[:,:1]) * np.sin(X[:,1:]) + + # Create GP regression model + m = GPy.models.GP_regression(X,Y,Kanova) + m.plot() + + +.. figure:: Figures/tuto_kern_overview_mANOVA.png + :align: center + :height: 350px + +As :math:`k_{ANOVA}` corresponds to the sum of 4 kernels, the best predictor can be splited in a sum of 4 functions + +.. math:: + + bp(x) & = k(x)^t K^{-1} Y \\ + & = (1 + k_1(x_1) + k_2(x_2) + k_1(x_1)k_2(x_2))^t K^{-1} Y \\ + & = 1^t K^{-1} Y + k_1(x_1)^t K^{-1} Y + k_2(x_2)^t K^{-1} Y + (k_1(x_1)k_2(x_2))^t K^{-1} Y + +The submodels can be represented with the option ``which_function`` of ``plot``: :: + + pb.figure(figsize=(20,5)) + pb.subplots_adjust(wspace=0.5) + pb.subplot(1,5,1) + m.plot() + pb.subplot(1,5,2) + pb.ylabel("= ",rotation='horizontal',fontsize='30') + pb.subplot(1,5,3) + m.plot(which_functions=[False,True,False,False]) + pb.ylabel("cst +",rotation='horizontal',fontsize='30') + pb.subplot(1,5,4) + m.plot(which_functions=[False,False,True,False]) + pb.ylabel("+ ",rotation='horizontal',fontsize='30') + pb.subplot(1,5,5) + pb.ylabel("+ ",rotation='horizontal',fontsize='30') + m.plot(which_functions=[False,False,False,True]) + + +.. figure:: Figures/tuto_kern_overview_mANOVAdec.png + :align: center + :height: 200px diff --git a/grid_parameters.py b/grid_parameters.py deleted file mode 100644 index 64d82755..00000000 --- a/grid_parameters.py +++ /dev/null @@ -1,64 +0,0 @@ -import numpy as np -import pylab as pb -pb.ion() -import sys -import GPy - -pb.close('all') - -N = 200 -M = 15 -resolution=5 - -X = np.linspace(0,12,N)[:,None] -Z = np.linspace(0,12,M)[:,None] # inducing points (fixed for now) -Y = np.sin(X) + np.random.randn(*X.shape)/np.sqrt(50.) -#k = GPy.kern.rbf(1) -k = GPy.kern.Matern32(1) + GPy.kern.white(1) - -models = [GPy.models.sparse_GP_regression(X,Y,Z=Z,kernel=k) - ,GPy.models.sparse_GP_regression(X,Y,Z=Z,kernel=k) - ,GPy.models.sparse_GP_regression(X,Y,Z=Z,kernel=k) - ,GPy.models.sparse_GP_regression(X,Y,Z=Z,kernel=k)] -models[0].scale_factor = 1. -models[1].scale_factor = 10. -models[2].scale_factor = 100. -models[3].scale_factor = 1000. - #GPy.models.sgp_debugB(X,Y,Z=Z,kernel=k), - #GPy.models.sgp_debugC(X,Y,Z=Z,kernel=k)]#, - #GPy.models.sgp_debugE(X,Y,Z=Z,kernel=k)] - -[m.constrain_fixed('white',0.1) for m in models] - -#xx,yy = np.mgrid[1.5:4:0+resolution*1j,-2:2:0+resolution*1j] -xx,yy = np.mgrid[3:16:0+resolution*1j,-2:1:0+resolution*1j] - -lls = [] -cgs = [] -grads = [] -count = 0 -for l,v in zip(xx.flatten(),yy.flatten()): - count += 1 - print count, 'of', resolution**2 - sys.stdout.flush() - - [m.set('lengthscale',l) for m in models] - [m.set('_variance',10.**v) for m in models] - lls.append([m.log_likelihood() for m in models]) - grads.append([m.log_likelihood_gradients() for m in models]) - cgs.append([m.checkgrad(verbose=0,return_ratio=True) for m in models]) - -lls = np.array(zip(*lls)).reshape(-1,resolution,resolution) -cgs = np.array(zip(*cgs)).reshape(-1,resolution,resolution) - -for ll,cg in zip(lls,cgs): - pb.figure() - pb.contourf(xx,yy,ll,100,cmap=pb.cm.gray) - pb.colorbar() - try: - pb.contour(xx,yy,np.exp(ll),colors='k') - except: - pass - pb.scatter(xx.flatten(),yy.flatten(),20,np.log(np.abs(cg.flatten())),cmap=pb.cm.jet,linewidth=0) - pb.colorbar() - From eea2fcc4b7e5c844019d99a5464b9a3bfe4e00cf Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:12:12 +0000 Subject: [PATCH 178/197] Added ipython to setup again and went back to numpy.distribute --- doc/conf.py | 2 +- setup.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ab72789e..f7d3f920 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -132,7 +132,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = True if on_rtd: print "I am here" - #sys.path.append(os.path.abspath('../GPy')) + sys.path.append(os.path.abspath('../GPy')) import subprocess diff --git a/setup.py b/setup.py index acebd624..40c89ccb 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,7 @@ # -*- coding: utf-8 -*- import os -#from numpy.distutils.core import Extension, setup -from setuptools import setup +from numpy.distutils.core import Extension, setup #from sphinx.setup_command import BuildDoc # Version number @@ -29,7 +28,7 @@ setup(name = 'GPy', # sources = ['GPy/kern/src/lfmUpsilonf2py.f90'])], install_requires=['sympy', 'numpy>=1.6', 'scipy>=0.9','matplotlib>=1.1'], extras_require = { - 'docs':['Sphinx'], + 'docs':['Sphinx', 'ipython'], }, #setup_requires=['sphinx'], #cmdclass = {'build_sphinx': BuildDoc}, From 6a43359727e6dc55c1f8d43e43f6bf765db86d8e Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:14:39 +0000 Subject: [PATCH 179/197] Added a path back --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index f7d3f920..9d917534 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,6 +37,7 @@ try: except ImportError: print "no sphinx" +sys.path.insert(0, os.getcwd() + "/..") #parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) #sys.path.insert(0, parent) #sys.path.append(parent) From 77cadbbcb5fb743c1d7059bbc1f2908fa4d69eee Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:16:02 +0000 Subject: [PATCH 180/197] Added ipython test code back and extensions loading --- doc/conf.py | 4 ++-- doc/tuto_GP_regression.rst | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 9d917534..bc6f9de1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -87,8 +87,8 @@ extensions = ['sphinx.ext.autodoc', #'sphinx.ext.doctest' 'sphinx.ext.viewcode', 'sphinx.ext.pngmath' - #'ipython_directive', - #'ipython_console_highlighting.py' + 'ipython_directive', + 'ipython_console_highlighting.py' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 08931834..fb367848 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -2,10 +2,10 @@ Gaussian process regression tutorial ************************************* -#.. ipython:: python -# -# print "Hello world" -# X = [[1, 10], [1, 20], [1, -2]] +.. ipython:: python + + print "Hello world" + X = [[1, 10], [1, 20], [1, -2]] We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. From f39d176740d2c4ed949c952128a7a57058c41985 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:17:01 +0000 Subject: [PATCH 181/197] Okay definietely no paths adding... lets see what is required for ipython --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index bc6f9de1..881bfad4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ try: except ImportError: print "no sphinx" -sys.path.insert(0, os.getcwd() + "/..") +#sys.path.insert(0, os.getcwd() + "/..") #parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) #sys.path.insert(0, parent) #sys.path.append(parent) From 6d7699db84da7f7ecf0f594cda008a93a1976a85 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:18:19 +0000 Subject: [PATCH 182/197] Typo --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 881bfad4..124d7fd6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -86,7 +86,7 @@ print "Importing extensions" extensions = ['sphinx.ext.autodoc', #'sphinx.ext.doctest' 'sphinx.ext.viewcode', - 'sphinx.ext.pngmath' + 'sphinx.ext.pngmath', 'ipython_directive', 'ipython_console_highlighting.py' #'matplotlib.sphinxext.mathmpl', From a33dff924b7c46763935b5d53a43ff32719f0c1f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 8 Feb 2013 16:22:29 +0000 Subject: [PATCH 183/197] Few changes to tutorial --- doc/tuto_kernel_overview.rst | 85 +++++++++++++++++------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/doc/tuto_kernel_overview.rst b/doc/tuto_kernel_overview.rst index 9ef86589..0f95da69 100644 --- a/doc/tuto_kernel_overview.rst +++ b/doc/tuto_kernel_overview.rst @@ -12,7 +12,7 @@ First we import the libraries we will need :: For most kernels, the dimension is the only mandatory parameter to define a kernel object. However, it is also possible to specify the values of the parameters. For example, the three following commands are valid for defining a squared exponential kernel (ie rbf or Gaussian) :: - ker1 = GPy.kern.rbf(D=1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) + ker1 = GPy.kern.rbf(1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) ker2 = GPy.kern.rbf(D=1, variance = 1.5, lengthscale=2.) ker3 = GPy.kern.rbf(1, .5, .5) @@ -28,43 +28,6 @@ A `plot` and a `print` functions are implemented to represent kernel objects :: :align: center :height: 350px -:: - - import pylab as pb - import numpy as np - import GPy - pb.ion() - - ker1 = GPy.kern.rbf(D=1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) - ker2 = GPy.kern.rbf(D=1, variance = .75, lengthscale=3.) - ker3 = GPy.kern.rbf(1, .5, .25) - - ker1.plot() - ker2.plot() - ker3.plot() - #pb.savefig("Figures/tuto_kern_overview_basicdef.png") - - kernels = [GPy.kern.rbf(1), GPy.kern.exponential(1), GPy.kern.Matern32(1), GPy.kern.Matern52(1), GPy.kern.Brownian(1), GPy.kern.bias(1), GPy.kern.linear(1), GPy.kern.spline(1), GPy.kern.periodic_exponential(1), GPy.kern.periodic_Matern32(1), GPy.kern.periodic_Matern52(1), GPy.kern.white(1)] - kernel_names = ["GPy.kern.rbf", "GPy.kern.exponential", "GPy.kern.Matern32", "GPy.kern.Matern52", "GPy.kern.Brownian", "GPy.kern.bias", "GPy.kern.linear", "GPy.kern.spline", "GPy.kern.periodic_exponential", "GPy.kern.periodic_Matern32", "GPy.kern.periodic_Matern52", "GPy.kern.white"] - - pb.figure(figsize=(16,12)) - pb.subplots_adjust(wspace=.5, hspace=.5) - for i, kern in enumerate(kernels): - pb.subplot(3,4,i+1) - kern.plot(x=7.5,plot_limits=[0.00001,15.]) - pb.title(kernel_names[i]+ '\n') - #pb.axes([.1,.1,.8,.7]) - #pb.figtext(.5,.9,'Foo Bar', fontsize=18, ha='center') - #pb.figtext(.5,.85,'Lorem ipsum dolor sit amet, consectetur adipiscing elit',fontsize=10,ha='center') - - # actual plot for the noise - i = 11 - X = np.linspace(0.,15.,201) - WN = 0*X - WN[100] = 1. - pb.subplot(3,4,i+1) - pb.plot(X,WN,'b') - Implemented kernels =================== @@ -90,22 +53,18 @@ In ``GPy``, kernel objects can be combined with the usual ``+`` and ``*`` operat ker_prod = k1 * k2 print ker_prod -Note that by default, the operator ``+`` adds kernels defined on the same input space whereas ``*`` assumes that the kernels are defined on different input spaces. :: - - ker_add.D - ker_prod.D +Note that by default, the operator ``+`` adds kernels defined on the same input space whereas ``*`` assumes that the kernels are defined on different input spaces. Here for example ``ker_add.D`` will return ``1`` whereas ``ker_prod.D`` will return ``2``. In order to add kernels defined on the different input spaces, the required command is:: ker_add_orth = k1.add_orthogonal(k2) -The resulting kernel is - ker_add_orth.plot(plot_limits=[[-10,-10],[10,10]]) - .. figure:: Figures/tuto_kern_overview_add_orth.png :align: center :height: 350px + Output of ``ker_add_orth.plot(plot_limits=[[-10,-10],[10,10]])``. + Example : Building an ANOVA kernel ================================== @@ -167,3 +126,39 @@ The submodels can be represented with the option ``which_function`` of ``plot``: .. figure:: Figures/tuto_kern_overview_mANOVAdec.png :align: center :height: 200px + + +.. import pylab as pb + import numpy as np + import GPy + pb.ion() + + ker1 = GPy.kern.rbf(D=1) # Equivalent to ker1 = GPy.kern.rbf(D=1, variance=1., lengthscale=1.) + ker2 = GPy.kern.rbf(D=1, variance = .75, lengthscale=3.) + ker3 = GPy.kern.rbf(1, .5, .25) + + ker1.plot() + ker2.plot() + ker3.plot() + #pb.savefig("Figures/tuto_kern_overview_basicdef.png") + + kernels = [GPy.kern.rbf(1), GPy.kern.exponential(1), GPy.kern.Matern32(1), GPy.kern.Matern52(1), GPy.kern.Brownian(1), GPy.kern.bias(1), GPy.kern.linear(1), GPy.kern.spline(1), GPy.kern.periodic_exponential(1), GPy.kern.periodic_Matern32(1), GPy.kern.periodic_Matern52(1), GPy.kern.white(1)] + kernel_names = ["GPy.kern.rbf", "GPy.kern.exponential", "GPy.kern.Matern32", "GPy.kern.Matern52", "GPy.kern.Brownian", "GPy.kern.bias", "GPy.kern.linear", "GPy.kern.spline", "GPy.kern.periodic_exponential", "GPy.kern.periodic_Matern32", "GPy.kern.periodic_Matern52", "GPy.kern.white"] + + pb.figure(figsize=(16,12)) + pb.subplots_adjust(wspace=.5, hspace=.5) + for i, kern in enumerate(kernels): + pb.subplot(3,4,i+1) + kern.plot(x=7.5,plot_limits=[0.00001,15.]) + pb.title(kernel_names[i]+ '\n') + #pb.axes([.1,.1,.8,.7]) + #pb.figtext(.5,.9,'Foo Bar', fontsize=18, ha='center') + #pb.figtext(.5,.85,'Lorem ipsum dolor sit amet, consectetur adipiscing elit',fontsize=10,ha='center') + + # actual plot for the noise + i = 11 + X = np.linspace(0.,15.,201) + WN = 0*X + WN[100] = 1. + pb.subplot(3,4,i+1) + pb.plot(X,WN,'b') From 88e1b92554237667cff8fdd3c6cedc0fcd7e13c8 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:23:02 +0000 Subject: [PATCH 184/197] Fixed typo --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 124d7fd6..ac828c2c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -88,7 +88,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.pngmath', 'ipython_directive', - 'ipython_console_highlighting.py' + 'ipython_console_highlighting' #'matplotlib.sphinxext.mathmpl', #'matplotlib.sphinxext.only_directives', #'matplotlib.sphinxext.plot_directive', From deb0430d348fe0972a15e1ea7a495ed367792806 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 8 Feb 2013 16:24:11 +0000 Subject: [PATCH 185/197] Few changes to tutorial bis --- doc/tuto_kernel_overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tuto_kernel_overview.rst b/doc/tuto_kernel_overview.rst index 0f95da69..d87da223 100644 --- a/doc/tuto_kernel_overview.rst +++ b/doc/tuto_kernel_overview.rst @@ -81,7 +81,7 @@ Let us assume that we want to define an ANOVA kernel with a Matern 3/2 kernel fo Kanova = (k_cst + k_mat) * (k_cst + k_mat) print Kanova -Note the ties between the lengthscales of ``Kanova`` to keep the number of lengthscales equal to 2. On the other hand, there are four variance terms in the new parameterization: one for each term of the right hand sign of the equation above. We can illustrate the use of this kernel on a toy example:: +Note the ties between the lengthscales of ``Kanova`` to keep the number of lengthscales equal to 2. On the other hand, there are four variance terms in the new parameterization: one for each term of the right hand part of the above equation. We can illustrate the use of this kernel on a toy example:: # sample inputs and outputs X = np.random.uniform(-3.,3.,(40,2)) From d504fd0c7306a02f602cd23c6a75e334558a91e2 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:26:20 +0000 Subject: [PATCH 186/197] Added matplotlib test, probably won't work --- doc/tuto_GP_regression.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index fb367848..1a6e245a 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -7,6 +7,18 @@ Gaussian process regression tutorial print "Hello world" X = [[1, 10], [1, 20], [1, -2]] +.. plot:: + + import matplotlib.pyplot as plt + import numpy as np + x = np.random.randn(1000) + plt.hist( x, 20) + plt.grid() + plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) + plt.show() + + + We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. We first import the libraries we will need: :: From a647e41c8493a217a1ad2f0d3cfe1b9554a2dfef Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 16:46:32 +0000 Subject: [PATCH 187/197] About to exchange sphinxext --- doc/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ac828c2c..78529fe9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -88,10 +88,10 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.pngmath', 'ipython_directive', - 'ipython_console_highlighting' - #'matplotlib.sphinxext.mathmpl', - #'matplotlib.sphinxext.only_directives', - #'matplotlib.sphinxext.plot_directive', + 'ipython_console_highlighting', + #'mathmpl', + 'only_directives', + 'plot_directive', ] print "finished importing" From ee22b77fe9d02f576466207fe4a2c27e54e83a52 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:11:59 +0000 Subject: [PATCH 188/197] Trying to get plotting working --- doc/conf.py | 8 +++++--- doc/tuto_GP_regression.rst | 15 +++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 78529fe9..ed03944c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -90,9 +90,11 @@ extensions = ['sphinx.ext.autodoc', 'ipython_directive', 'ipython_console_highlighting', #'mathmpl', - 'only_directives', - 'plot_directive', + #'only_directives', + 'matplotlib.sphinxext.plot_directive' + #'plot_directive' ] +plot_formats = [('png', 80), ('pdf', 50)] print "finished importing" @@ -122,7 +124,7 @@ class Mock(object): #import mock print "Mocking" -MOCK_MODULES = ['pylab', 'matplotlib', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser'] +MOCK_MODULES = ['pylab', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser'] #'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 1a6e245a..93f5a02f 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -9,14 +9,13 @@ Gaussian process regression tutorial .. plot:: - import matplotlib.pyplot as plt - import numpy as np - x = np.random.randn(1000) - plt.hist( x, 20) - plt.grid() - plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) - plt.show() - + import matplotlib.pyplot as plt + import numpy as np + x = np.random.randn(1000) + plt.hist( x, 20) + plt.grid() + plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) + plt.show() We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. From bfa81644d3ca0bc602875ee206186e54585463cb Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:19:32 +0000 Subject: [PATCH 189/197] Try installing with pip? eek... --- doc/doc-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt index 0b5ac59b..881d299a 100644 --- a/doc/doc-requirements.txt +++ b/doc/doc-requirements.txt @@ -1,3 +1,4 @@ +matplotlib ipython numpy scipy From 240157e0dcd3f9e4e270b6dbc960c6e1b1025237 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:22:35 +0000 Subject: [PATCH 190/197] Cant install with pip --- doc/doc-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt index 881d299a..0b5ac59b 100644 --- a/doc/doc-requirements.txt +++ b/doc/doc-requirements.txt @@ -1,4 +1,3 @@ -matplotlib ipython numpy scipy From 5d60d1a6e393b6d226e9d45f329fcef1e35f1f65 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:35:37 +0000 Subject: [PATCH 191/197] Debugging finding matplotlib... --- doc/conf.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ed03944c..fc86ab11 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,20 +37,6 @@ try: except ImportError: print "no sphinx" -#sys.path.insert(0, os.getcwd() + "/..") -#parent = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -#sys.path.insert(0, parent) -#sys.path.append(parent) - -#parent = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GPy')) -#sys.path.insert(0, parent) -#sys.path.append(parent) - -#APP_DIR = os.path.normpath(os.path.join(os.getcwd(), '../')) -#PACKAGE_DIR1 = os.path.normpath(os.path.join(os.getcwd(), '../')) -#sys.path.insert(0, APP_DIR) -#sys.path.insert(0, PACKAGE_DIR1) -#sys.path.insert(0, os.path.abspath('../GPy')) print "sys.path:", sys.path #sys.path.insert(0, os.getcwd() + "/..") @@ -88,10 +74,10 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.pngmath', 'ipython_directive', - 'ipython_console_highlighting', + 'ipython_console_highlighting' + #'matplotlib.sphinxext.plot_directive' #'mathmpl', #'only_directives', - 'matplotlib.sphinxext.plot_directive' #'plot_directive' ] plot_formats = [('png', 80), ('pdf', 50)] @@ -148,6 +134,9 @@ if on_rtd: proc = subprocess.Popen("sphinx-apidoc -f -o . ../GPy", stdout=subprocess.PIPE, shell=True) (out, err) = proc.communicate() print "program output:", out + proc = subprocess.Popen("locate matplotlib", stdout=subprocess.PIPE, shell=True) + (out, err) = proc.communicate() + print "program output:", out #os.system("cd ..") #os.system("cd ./docs") From 59f767e1afe9f83ad076cfc78fcd27fc757c00ce Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:39:56 +0000 Subject: [PATCH 192/197] More debugging --- doc/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index fc86ab11..0167cb90 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -134,7 +134,11 @@ if on_rtd: proc = subprocess.Popen("sphinx-apidoc -f -o . ../GPy", stdout=subprocess.PIPE, shell=True) (out, err) = proc.communicate() print "program output:", out - proc = subprocess.Popen("locate matplotlib", stdout=subprocess.PIPE, shell=True) + proc = subprocess.Popen("whereis numpy", stdout=subprocess.PIPE, shell=True) + (out, err) = proc.communicate() + print "program output:", out + print "MATPLOTLIB BABY" + proc = subprocess.Popen("whereis matplotlib", stdout=subprocess.PIPE, shell=True) (out, err) = proc.communicate() print "program output:", out #os.system("cd ..") From a272de6cdb4a1ac94c40d2eec0ebedfda65eb17b Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 18:46:40 +0000 Subject: [PATCH 193/197] Cleaning up --- doc/conf.py | 36 ++++++------------------------------ doc/tuto_GP_regression.rst | 16 ---------------- 2 files changed, 6 insertions(+), 46 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 0167cb90..078279d4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,8 +39,6 @@ except ImportError: print "sys.path:", sys.path -#sys.path.insert(0, os.getcwd() + "/..") - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -48,7 +46,6 @@ print "sys.path:", sys.path #print "sys.path.after:", sys.path - # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. @@ -57,7 +54,6 @@ sys.path.append(os.path.abspath('sphinxext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('./sphinxext')) # -- General configuration ----------------------------------------------------- @@ -76,9 +72,6 @@ extensions = ['sphinx.ext.autodoc', 'ipython_directive', 'ipython_console_highlighting' #'matplotlib.sphinxext.plot_directive' - #'mathmpl', - #'only_directives', - #'plot_directive' ] plot_formats = [('png', 80), ('pdf', 50)] @@ -120,7 +113,6 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = True if on_rtd: - print "I am here" sys.path.append(os.path.abspath('../GPy')) import subprocess @@ -134,15 +126,12 @@ if on_rtd: proc = subprocess.Popen("sphinx-apidoc -f -o . ../GPy", stdout=subprocess.PIPE, shell=True) (out, err) = proc.communicate() print "program output:", out - proc = subprocess.Popen("whereis numpy", stdout=subprocess.PIPE, shell=True) - (out, err) = proc.communicate() - print "program output:", out - print "MATPLOTLIB BABY" - proc = subprocess.Popen("whereis matplotlib", stdout=subprocess.PIPE, shell=True) - (out, err) = proc.communicate() - print "program output:", out - #os.system("cd ..") - #os.system("cd ./docs") + #proc = subprocess.Popen("whereis numpy", stdout=subprocess.PIPE, shell=True) + #(out, err) = proc.communicate() + #print "program output:", out + #proc = subprocess.Popen("whereis matplotlib", stdout=subprocess.PIPE, shell=True) + #(out, err) = proc.communicate() + #print "program output:", out print "Compiled files" @@ -406,17 +395,4 @@ epub_copyright = u'2013, Author' # Allow duplicate toc entries. #epub_tocdup = True -############################################################################# -# -# Include constructors in all the docs -# Got this method from: -# http://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc-to-document-a-classs-init-self-method -#def skip(app, what, name, obj, skip, options): - #if name == "__init__": - #return False - #return skip - -#def setup(app): - #app.connect("autodoc-skip-member", skip) - autodoc_member_order = "source" diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 2e307287..92b25bc0 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -2,22 +2,6 @@ Gaussian process regression tutorial ************************************* -.. ipython:: python - - print "Hello world" - X = [[1, 10], [1, 20], [1, -2]] - -.. plot:: - - import matplotlib.pyplot as plt - import numpy as np - x = np.random.randn(1000) - plt.hist( x, 20) - plt.grid() - plt.title(r'Normal: $\mu=%.2f, \sigma=%.2f$'%(x.mean(), x.std())) - plt.show() - - We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. We first import the libraries we will need: :: From dee0052a8057e68f8c01ea8d92ad5112c6e6ed0d Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 22:54:34 +0000 Subject: [PATCH 194/197] Testing ipython on rtd --- doc/conf.py | 2 +- doc/tuto_GP_regression.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 078279d4..8a05f386 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -103,7 +103,7 @@ class Mock(object): #import mock print "Mocking" -MOCK_MODULES = ['pylab', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser'] +MOCK_MODULES = ['pylab', 'sympy', 'sympy.utilities', 'sympy.utilities.codegen', 'sympy.core.cache', 'sympy.core', 'sympy.parsing', 'sympy.parsing.sympy_parser', 'matplotlib'] #'matplotlib', 'matplotlib.color', 'matplotlib.pyplot', 'pylab' ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index 92b25bc0..a78929b3 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -2,6 +2,28 @@ Gaussian process regression tutorial ************************************* + +.. ipython:: python + + import numpy as np + import GPy as gpy + + """run a simple demonstration of a standard gaussian process fitting it to data sampled from an rbf covariance.""" + data = gpy.util.datasets.toy_rbf_1d() + + # create simple gp model + m = gpy.models.GP_regression(data['X'],data['Y']) + + # optimize + m.ensure_default_constraints() + m.optimize() + + print(m) + + # plot + m.plot() + + We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. We first import the libraries we will need: :: From ed84c0722c772c348ae70ba1eb6b1448017e5913 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Fri, 8 Feb 2013 22:57:01 +0000 Subject: [PATCH 195/197] Removed ipython code from tuto --- doc/tuto_GP_regression.rst | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/doc/tuto_GP_regression.rst b/doc/tuto_GP_regression.rst index a78929b3..92b25bc0 100644 --- a/doc/tuto_GP_regression.rst +++ b/doc/tuto_GP_regression.rst @@ -2,28 +2,6 @@ Gaussian process regression tutorial ************************************* - -.. ipython:: python - - import numpy as np - import GPy as gpy - - """run a simple demonstration of a standard gaussian process fitting it to data sampled from an rbf covariance.""" - data = gpy.util.datasets.toy_rbf_1d() - - # create simple gp model - m = gpy.models.GP_regression(data['X'],data['Y']) - - # optimize - m.ensure_default_constraints() - m.optimize() - - print(m) - - # plot - m.plot() - - We will see in this tutorial the basics for building a 1 dimensional and a 2 dimensional Gaussian process regression model, also known as a kriging model. We first import the libraries we will need: :: From ae19ab221092e3e75629b0f0f623841ecc566336 Mon Sep 17 00:00:00 2001 From: Alan Saul Date: Sun, 10 Feb 2013 16:21:19 +0000 Subject: [PATCH 196/197] Added ANOVA kernel print output --- doc/tuto_kernel_overview.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/tuto_kernel_overview.rst b/doc/tuto_kernel_overview.rst index d87da223..80e2bee2 100644 --- a/doc/tuto_kernel_overview.rst +++ b/doc/tuto_kernel_overview.rst @@ -74,13 +74,26 @@ In two dimensions ANOVA kernels have the following form: k_{ANOVA}(x,y) = \prod_{i=1}^2 (1 + k_i(x_i,y_i)) = 1 + k_1(x_1,y_1) + k_2(x_2,y_2) + k_1(x_1,y_1) \times k_2(x_2,y_2). -Let us assume that we want to define an ANOVA kernel with a Matern 3/2 kernel for :math:`k_i`. As seen previously, we can define this kernel as follow:: +Let us assume that we want to define an ANOVA kernel with a Matern 3/2 kernel for :math:`k_i`. As seen previously, we can define this kernel as follows :: k_cst = GPy.kern.bias(1,variance=1.) k_mat = GPy.kern.Matern52(1,variance=1., lengthscale=3) Kanova = (k_cst + k_mat) * (k_cst + k_mat) print Kanova +Printing the resulting kernel outputs the following :: + + Name | Value | Constraints | Ties + --------------------------------------------------------------------------- + biasbias_variance | 1.0000 | | + biasMat52_variance | 1.0000 | | + biasMat52_Mat52_lengthscale | 3.0000 | | (1) + Mat52bias_variance | 1.0000 | | + Mat52bias_Mat52_lengthscale | 3.0000 | | (0) + Mat52Mat52_variance | 1.0000 | | + Mat52Mat52_Mat52_lengthscale | 3.0000 | | (0) + Mat52Mat52_Mat52_lengthscale | 3.0000 | | (1) + Note the ties between the lengthscales of ``Kanova`` to keep the number of lengthscales equal to 2. On the other hand, there are four variance terms in the new parameterization: one for each term of the right hand part of the above equation. We can illustrate the use of this kernel on a toy example:: # sample inputs and outputs From bfd216646912427d7755284cbcd082ca7fe70e71 Mon Sep 17 00:00:00 2001 From: James Hensman Date: Wed, 13 Feb 2013 18:18:29 +0000 Subject: [PATCH 197/197] added a default kernel option in BGPLVM --- GPy/models/BGPLVM.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/GPy/models/BGPLVM.py b/GPy/models/BGPLVM.py index 16115025..45304915 100644 --- a/GPy/models/BGPLVM.py +++ b/GPy/models/BGPLVM.py @@ -22,7 +22,7 @@ class Bayesian_GPLVM(sparse_GP, GPLVM): :type init: 'PCA'|'random' """ - def __init__(self, Y, Q, init='PCA', M=10, Z=None, **kwargs): + def __init__(self, Y, Q, init='PCA', M=10, Z=None, kernel=None, **kwargs): X = self.initialise_latent(init, Q, Y) if Z is None: @@ -30,10 +30,11 @@ class Bayesian_GPLVM(sparse_GP, GPLVM): else: assert Z.shape[1]==X.shape[1] - kernel = kern.rbf(Q) + kern.white(Q) + if kernel is None: + kernel = kern.rbf(Q) + kern.white(Q) S = np.ones_like(X) * 1e-2# - sparse_GP.__init__(self, X, Gaussian(Y), X_uncertainty=S, Z=Z,**kwargs) + sparse_GP.__init__(self, X, Gaussian(Y), kernel, Z=Z, X_uncertainty=S, **kwargs) def _get_param_names(self): X_names = sum([['X_%i_%i'%(n,q) for n in range(self.N)] for q in range(self.Q)],[])