Coverage for /builds/ase/ase/ase/optimize/gpmin/gp.py: 53.85%
65 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
4from scipy.linalg import cho_factor, cho_solve, solve_triangular
5from scipy.optimize import minimize
7from ase.optimize.gpmin.kernel import SquaredExponential
8from ase.optimize.gpmin.prior import ZeroPrior
11class GaussianProcess():
12 """Gaussian Process Regression
13 It is recommended to be used with other Priors and Kernels from
14 ase.optimize.gpmin
16 Parameters:
18 prior: Prior class, as in ase.optimize.gpmin.prior
19 Defaults to ZeroPrior
21 kernel: Kernel function for the regression, as in
22 ase.optimize.gpmin.kernel
23 Defaults to the Squared Exponential kernel with derivatives
24 """
26 def __init__(self, prior=None, kernel=None):
27 if kernel is None:
28 self.kernel = SquaredExponential()
29 else:
30 self.kernel = kernel
32 if prior is None:
33 self.prior = ZeroPrior()
34 else:
35 self.prior = prior
37 def set_hyperparams(self, params):
38 """Set hyperparameters of the regression.
39 This is a list containing the parameters of the
40 kernel and the regularization (noise)
41 of the method as the last entry.
42 """
43 self.hyperparams = params
44 self.kernel.set_params(params[:-1])
45 self.noise = params[-1]
47 def train(self, X, Y, noise=None):
48 """Produces a PES model from data.
50 Given a set of observations, X, Y, compute the K matrix
51 of the Kernel given the data (and its cholesky factorization)
52 This method should be executed whenever more data is added.
54 Parameters:
56 X: observations (i.e. positions). numpy array with shape: nsamples x D
57 Y: targets (i.e. energy and forces). numpy array with
58 shape (nsamples, D+1)
59 noise: Noise parameter in the case it needs to be restated.
60 """
61 if noise is not None:
62 self.noise = noise # Set noise attribute to a different value
64 self.X = X.copy() # Store the data in an attribute
65 n = self.X.shape[0]
66 D = self.X.shape[1]
67 regularization = np.array(n * ([self.noise * self.kernel.l] +
68 D * [self.noise]))
70 K = self.kernel.kernel_matrix(X) # Compute the kernel matrix
71 K[range(K.shape[0]), range(K.shape[0])] += regularization**2
73 self.m = self.prior.prior(X)
74 self.a = Y.flatten() - self.m
75 self.L, self.lower = cho_factor(K, lower=True, check_finite=True)
76 cho_solve((self.L, self.lower), self.a, overwrite_b=True,
77 check_finite=True)
79 def predict(self, x, get_variance=False):
80 """Given a trained Gaussian Process, it predicts the value and the
81 uncertainty at point x. It returns f and V:
82 f : prediction: [y, grady]
83 V : Covariance matrix. Its diagonal is the variance of each component
84 of f.
86 Parameters:
88 x (1D np.array): The position at which the prediction is computed
89 get_variance (bool): if False, only the prediction f is returned
90 if True, the prediction f and the variance V are
91 returned: Note V is O(D*nsample2)
92 """
93 n = self.X.shape[0]
94 k = self.kernel.kernel_vector(x, self.X, n)
95 f = self.prior.prior(x) + np.dot(k, self.a)
96 if get_variance:
97 v = solve_triangular(self.L, k.T.copy(), lower=True,
98 check_finite=False)
99 variance = self.kernel.kernel(x, x)
100 # covariance = np.matmul(v.T, v)
101 covariance = np.tensordot(v, v, axes=(0, 0))
102 V = variance - covariance
103 return f, V
104 return f
106 def neg_log_likelihood(self, params, *args):
107 """Negative logarithm of the marginal likelihood and its derivative.
108 It has been built in the form that suits the best its optimization,
109 with the scipy minimize module, to find the optimal hyperparameters.
111 Parameters:
113 l: The scale for which we compute the marginal likelihood
114 *args: Should be a tuple containing the inputs and targets
115 in the training set-
116 """
117 X, Y = args
118 # Come back to this
119 self.kernel.set_params(np.array([params[0], params[1], self.noise]))
120 self.train(X, Y)
121 y = Y.flatten()
123 # Compute log likelihood
124 logP = (-0.5 * np.dot(y - self.m, self.a) -
125 np.sum(np.log(np.diag(self.L))) -
126 X.shape[0] * 0.5 * np.log(2 * np.pi))
128 # Gradient of the loglikelihood
129 grad = self.kernel.gradient(X)
131 # vectorizing the derivative of the log likelihood
132 D_P_input = np.array([np.dot(np.outer(self.a, self.a), g)
133 for g in grad])
134 D_complexity = np.array([cho_solve((self.L, self.lower), g)
135 for g in grad])
137 DlogP = 0.5 * np.trace(D_P_input - D_complexity, axis1=1, axis2=2)
138 return -logP, -DlogP
140 def fit_hyperparameters(self, X, Y, tol=1e-2, eps=None):
141 """Given a set of observations, X, Y; optimize the scale
142 of the Gaussian Process maximizing the marginal log-likelihood.
143 This method calls TRAIN there is no need to call the TRAIN method
144 again. The method also sets the parameters of the Kernel to their
145 optimal value at the end of execution
147 Parameters:
149 X: observations(i.e. positions). numpy array with shape: nsamples x D
150 Y: targets (i.e. energy and forces).
151 numpy array with shape (nsamples, D+1)
152 tol: tolerance on the maximum component of the gradient of the
153 log-likelihood.
154 (See scipy's L-BFGS-B documentation:
155 https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)
156 eps: include bounds to the hyperparameters as a +- a percentage
157 if eps is None there are no bounds in the optimization
159 Returns:
161 result (dict) :
162 result = {'hyperparameters': (numpy.array) New hyperparameters,
163 'converged': (bool) True if it converged,
164 False otherwise
165 }
166 """
167 params = np.copy(self.hyperparams)[:2]
168 arguments = (X, Y)
169 if eps is not None:
170 bounds = [((1 - eps) * p, (1 + eps) * p) for p in params]
171 else:
172 bounds = None
174 result = minimize(self.neg_log_likelihood, params, args=arguments,
175 method='L-BFGS-B', jac=True, bounds=bounds,
176 options={'gtol': tol, 'ftol': 0.01 * tol})
178 if not result.success:
179 converged = False
180 else:
181 converged = True
182 self.hyperparams = np.array([result.x.copy()[0],
183 result.x.copy()[1], self.noise])
184 self.set_hyperparams(self.hyperparams)
185 return {'hyperparameters': self.hyperparams, 'converged': converged}