Coverage for jetgp/full_degp/degp.py: 94%

151 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-05-07 14:04 -0500

1import numpy as np 

2from numpy.linalg import cholesky, solve 

3from scipy.linalg import cho_solve, cho_factor, solve_triangular 

4from jetgp.full_degp import degp_utils 

5import jetgp.utils as utils 

6from jetgp.kernel_funcs.kernel_funcs import KernelFactory, get_oti_module 

7from jetgp.full_degp.optimizer import Optimizer 

8 

9 

10class degp: 

11 """ 

12 Derivative-Enhanced Gaussian Process (DEGP) model. 

13 

14 Supports coordinate-aligned partial derivatives, hypercomplex representation, 

15 and automatic normalization. Includes methods for training, prediction, 

16 and uncertainty quantification using kernel methods. 

17 

18 Parameters 

19 ---------- 

20 x_train : ndarray 

21 Training input data of shape (n_samples, n_features). 

22 y_train : list or ndarray 

23 Training targets or list of partial derivatives. 

24 n_order : int 

25 Maximum derivative order. 

26 n_bases : int 

27 Number of input dimensions. 

28 der_indices : list of lists 

29 Derivative multi-indices corresponding to each derivative term. 

30 derivative_locations : list of lists 

31 Which training points have which derivatives. 

32 normalize : bool, default=True 

33 Whether to normalize inputs and outputs. 

34 sigma_data : float or array-like, optional 

35 Observation noise standard deviation or diagonal noise values. 

36 kernel : str, default='SE' 

37 Kernel type ('SE', 'RQ', 'Matern', 'SI', etc.). 

38 kernel_type : str, default='anisotropic' 

39 Kernel anisotropy ('anisotropic' or 'isotropic'). 

40 smoothness_parameter : float, optional 

41 Smoothness parameter for Matern kernel. 

42 """ 

43 

44 def __init__( 

45 self, 

46 x_train, 

47 y_train, 

48 n_order, 

49 n_bases, 

50 der_indices, 

51 derivative_locations=None, 

52 normalize=True, 

53 sigma_data=None, 

54 kernel="SE", 

55 kernel_type="anisotropic", 

56 smoothness_parameter=None 

57 ): 

58 if n_order > 0 and derivative_locations is None: 

59 import warnings 

60 # Count total number of derivative components across all orders 

61 n_derivs = sum(len(order_derivs) for order_derivs in der_indices) 

62 n_train = len(x_train) 

63 derivative_locations = [[i for i in range(n_train)] for _ in range(n_derivs)] 

64 warnings.warn( 

65 f"derivative_locations not provided. Assuming all {n_derivs} derivative(s) " 

66 f"are available at all {n_train} training point(s).", 

67 UserWarning 

68 ) 

69 

70 elif der_indices is None and n_order == 0: 

71 der_indices = [] 

72 derivative_locations = [] 

73 self.n_order = n_order 

74 self.n_bases = n_bases 

75 self.dim = x_train.shape[1] 

76 self.num_points = x_train.shape[0] 

77 self.kernel = kernel 

78 self.kernel_type = kernel_type 

79 self.der_indices = der_indices 

80 self.normalize = normalize 

81 self.derivative_locations = derivative_locations 

82 self.oti = get_oti_module(n_bases, n_order) 

83 self.y_train_input = y_train 

84 self.x_train_input = x_train 

85 

86 # Prepare indices and powers 

87 self.flattened_der_indices = utils.flatten_der_indices(der_indices) 

88 self.powers = utils.build_companion_array(n_bases, n_order, der_indices) 

89 

90 # Normalize data if required 

91 if normalize: 

92 ( 

93 self.y_train, 

94 self.mu_y, 

95 self.sigma_y, 

96 self.sigmas_x, 

97 self.mus_x, 

98 sigma_data, 

99 ) = utils.normalize_y_data( 

100 x_train, y_train, sigma_data, self.flattened_der_indices 

101 ) 

102 self.x_train = utils.normalize_x_data_train(x_train) 

103 else: 

104 self.x_train = x_train 

105 self.y_train = utils.reshape_y_train(y_train) 

106 

107 # Compute differences for the kernel 

108 # if kernel == 'SI': 

109 # self.differences_by_dim = degp_utils.differences_by_dim_func_SI( 

110 # self.x_train, self.x_train, n_order 

111 # ) 

112 # else: 

113 self.differences_by_dim = degp_utils.differences_by_dim_func( 

114 self.x_train, self.x_train, n_order, self.oti 

115 ) 

116 

117 # Initialize noise matrix 

118 self.sigma_data = ( 

119 np.zeros((self.y_train.shape[0], self.y_train.shape[0])) 

120 if sigma_data is None 

121 else np.diag(sigma_data) 

122 ) 

123 self.sigma_data_sq_diag = ( 

124 np.zeros(self.y_train.shape[0]) 

125 if sigma_data is None 

126 else np.asarray(sigma_data) ** 2 

127 ) 

128 

129 # Initialize kernel factory and optimizer 

130 self.kernel_factory = KernelFactory( 

131 dim=n_bases, 

132 normalize=normalize, 

133 differences_by_dim=self.differences_by_dim, 

134 n_order=n_order, 

135 smoothness_parameter=smoothness_parameter, 

136 oti_module=self.oti 

137 ) 

138 self.kernel_func = self.kernel_factory.create_kernel( 

139 kernel_name=self.kernel, kernel_type=self.kernel_type 

140 ) 

141 self.bounds = self.kernel_factory.bounds 

142 self.optimizer = Optimizer(self) 

143 

144 def optimize_hyperparameters(self, *args, **kwargs): 

145 """ 

146 Optimize model hyperparameters using the optimizer. 

147 Returns optimized hyperparameter vector. 

148 """ 

149 self.params = self.optimizer.optimize_hyperparameters(*args, **kwargs) 

150 return self.params 

151 

152 def predict(self, X_test, params, calc_cov=False, return_deriv=False, 

153 derivs_to_predict=None, return_full_cov=False): 

154 """ 

155 Compute posterior predictive mean and (optionally) covariance at X_test. 

156 

157 Parameters 

158 ---------- 

159 X_test : ndarray 

160 Test input points of shape (n_test, n_features). 

161 params : ndarray 

162 Log-scaled kernel hyperparameters. 

163 calc_cov : bool, default=False 

164 Whether to compute predictive variance. 

165 return_deriv : bool, default=False 

166 Whether to return derivative predictions. 

167 derivs_to_predict : list, optional 

168 Specific derivatives to predict. Can include derivatives not present in the 

169 training set — the cross-covariance K_* is constructed from kernel derivatives 

170 and does not require the requested derivative to have been observed during 

171 training. Each entry must be a valid derivative spec within n_bases and n_order 

172 (e.g. ``[[3, 1]]`` for df/dx3 in a first-order model). 

173 If None, defaults to all derivatives used in training. 

174 

175 Returns 

176 ------- 

177 f_mean : ndarray 

178 Predictive mean vector. 

179 f_var : ndarray, optional 

180 Predictive variance vector (only if calc_cov=True). 

181 f_cov_full : ndarray, optional 

182 Full predictive covariance matrix (only if calc_cov=True and 

183 return_full_cov=True). 

184 """ 

185 length_scales = params[:-1] 

186 sigma_n = params[-1] 

187 

188 # Set up derivative prediction configuration 

189 if return_deriv: 

190 if derivs_to_predict is not None: 

191 common_derivs = derivs_to_predict 

192 else: 

193 common_derivs = self.flattened_der_indices 

194 

195 # Determine prediction order from requested derivatives 

196 required_order = max( 

197 sum(pair[1] for pair in deriv_spec) 

198 for deriv_spec in common_derivs 

199 ) 

200 predict_order = max(required_order, self.n_order) 

201 

202 if predict_order > self.n_order: 

203 predict_oti = get_oti_module(self.n_bases, predict_order) 

204 smoothness_param = getattr(self.kernel_factory, 'alpha', None) 

205 predict_kernel_factory = KernelFactory( 

206 dim=self.n_bases, 

207 normalize=self.normalize, 

208 differences_by_dim=self.differences_by_dim, 

209 n_order=predict_order, 

210 smoothness_parameter=smoothness_param, 

211 oti_module=predict_oti 

212 ) 

213 predict_kernel_func = predict_kernel_factory.create_kernel( 

214 kernel_name=self.kernel, kernel_type=self.kernel_type 

215 ) 

216 else: 

217 predict_oti = self.oti 

218 predict_kernel_func = self.kernel_func 

219 

220 self.powers_predict = utils.build_companion_array_predict( 

221 self.n_bases, predict_order, common_derivs) 

222 else: 

223 common_derivs = [] 

224 self.powers_predict = None 

225 predict_order = self.n_order 

226 predict_oti = self.oti 

227 predict_kernel_func = self.kernel_func 

228 

229 # Reuse cached Cholesky + alpha from optimizer if available 

230 _cache_hit = ( 

231 hasattr(self, '_cached_params') 

232 and self._cached_params is not None 

233 and np.array_equal(self._cached_params, params) 

234 ) 

235 

236 if _cache_hit: 

237 L = self._cached_L 

238 low = self._cached_low 

239 alpha = self._cached_alpha 

240 cho_solve_failed = False 

241 else: 

242 # Build training kernel matrix (no cache available) 

243 phi_train = self.kernel_func(self.differences_by_dim, length_scales) 

244 

245 if self.n_order > 0: 

246 phi_exp_train = phi_train.get_all_derivs(self.n_bases, 2 * self.n_order) 

247 else: 

248 phi_exp_train = phi_train.real 

249 phi_exp_train = phi_exp_train[np.newaxis, :, :] 

250 

251 K = degp_utils.rbf_kernel( 

252 phi_train, phi_exp_train, self.n_order, self.n_bases, 

253 self.flattened_der_indices, self.powers, 

254 index=self.derivative_locations 

255 ) 

256 K.flat[::K.shape[0] + 1] += (10 ** sigma_n) ** 2 

257 K += self.sigma_data ** 2 

258 

259 # Solve linear system 

260 try: 

261 cho_solve_failed = False 

262 L, low = cho_factor(K, lower=True) 

263 alpha = cho_solve((L, low), self.y_train) 

264 except: 

265 cho_solve_failed = True 

266 alpha = np.linalg.solve(K, self.y_train) 

267 print('Warning: Cholesky decomposition failed via scipy, using standard np solve instead.') 

268 

269 # Normalize test inputs 

270 if self.normalize: 

271 X_test = utils.normalize_x_data_test(X_test, self.sigmas_x, self.mus_x) 

272 

273 # Set up test derivative locations 

274 if return_deriv: 

275 derivative_locations_test = [ 

276 list(range(X_test.shape[0])) for _ in range(len(common_derivs))] 

277 else: 

278 derivative_locations_test = None 

279 

280 # Compute train-test differences 

281 # if self.kernel == 'SI': 

282 # diff_x_test_x_train = degp_utils.differences_by_dim_func_SI( 

283 # self.x_train, X_test, self.n_order, return_deriv=return_deriv 

284 # ) 

285 # else: 

286 diff_x_test_x_train = degp_utils.differences_by_dim_func( 

287 self.x_train, X_test, predict_order, predict_oti, return_deriv=return_deriv 

288 ) 

289 

290 # Compute train-test kernel 

291 phi_train_test = predict_kernel_func(diff_x_test_x_train, length_scales) 

292 if predict_order > 0: 

293 if return_deriv: 

294 phi_exp_train_test = phi_train_test.get_all_derivs(self.n_bases, 2 * predict_order) 

295 else: 

296 phi_exp_train_test = phi_train_test.get_all_derivs(self.n_bases, predict_order) 

297 else: 

298 phi_exp_train_test = phi_train_test.real 

299 phi_exp_train_test = phi_exp_train_test[np.newaxis, :, :] 

300 

301 K_s = degp_utils.rbf_kernel_predictions( 

302 phi_train_test, phi_exp_train_test, predict_order, self.n_bases, 

303 self.flattened_der_indices, self.powers, 

304 return_deriv=return_deriv, 

305 index=self.derivative_locations, 

306 common_derivs=common_derivs, 

307 powers_predict=self.powers_predict 

308 ) 

309 

310 # Compute posterior mean 

311 f_mean = K_s.T @ alpha 

312 

313 # Denormalize predictions 

314 if self.normalize: 

315 if return_deriv: 

316 f_mean = utils.transform_predictions( 

317 f_mean, self.mu_y, self.sigma_y, self.sigmas_x, 

318 common_derivs, X_test) 

319 else: 

320 f_mean = self.mu_y + f_mean * self.sigma_y 

321 

322 # Reshape predictions 

323 f_mean = f_mean.reshape(-1, 1) 

324 n = X_test.shape[0] 

325 m = f_mean.shape[0] 

326 num_derivs = m // n 

327 reshaped_mean = f_mean.reshape(num_derivs, n) 

328 

329 if return_full_cov and not calc_cov: 

330 raise ValueError("return_full_cov=True requires calc_cov=True.") 

331 

332 if not calc_cov: 

333 return reshaped_mean 

334 

335 # Compute test-test differences 

336 diff_x_test_x_test = degp_utils.differences_by_dim_func( 

337 X_test, X_test, predict_order, predict_oti, return_deriv=return_deriv 

338 ) 

339 

340 # Compute test-test kernel 

341 phi_test_test = predict_kernel_func(diff_x_test_x_test, length_scales) 

342 if predict_order > 0: 

343 phi_exp_test_test = phi_test_test.get_all_derivs(self.n_bases, 2 * predict_order) 

344 else: 

345 phi_exp_test_test = phi_test_test.real 

346 phi_exp_test_test = phi_exp_test_test[np.newaxis,:,:] 

347 

348 K_ss = degp_utils.rbf_kernel_predictions( 

349 phi_test_test, phi_exp_test_test, predict_order, self.n_bases, 

350 self.flattened_der_indices, self.powers, 

351 return_deriv=return_deriv, 

352 index=derivative_locations_test, 

353 common_derivs=common_derivs, 

354 calc_cov=True, 

355 powers_predict=self.powers_predict 

356 ) 

357 

358 # Compute predictive covariance 

359 if cho_solve_failed: 

360 f_cov = K_ss - K_s.T @ np.linalg.inv(K) @ K_s 

361 else: 

362 v = solve_triangular(L, K_s, lower=low) 

363 f_cov = K_ss - v.T @ v 

364 

365 # Transform covariance 

366 if self.normalize: 

367 if return_deriv: 

368 f_var = utils.transform_cov( 

369 f_cov, self.sigma_y, self.sigmas_x, 

370 common_derivs, X_test) 

371 scale_vec = np.full(f_cov.shape[0], self.sigma_y, dtype=float) 

372 n_test = X_test.shape[0] 

373 for i, deriv_spec in enumerate(common_derivs): 

374 factor = self.sigma_y 

375 for pair in deriv_spec: 

376 factor = factor / ( 

377 self.sigmas_x[0][pair[0] - 1] ** pair[1] 

378 ) 

379 start = (i + 1) * n_test 

380 stop = (i + 2) * n_test 

381 scale_vec[start:stop] = factor 

382 f_cov_full = f_cov * np.outer(scale_vec, scale_vec) 

383 else: 

384 f_var = self.sigma_y ** 2 * np.diag(np.abs(f_cov)) 

385 f_cov_full = f_cov * self.sigma_y ** 2 

386 else: 

387 f_var = np.diag(np.abs(f_cov)) 

388 f_cov_full = f_cov 

389 

390 reshaped_var = f_var.reshape(num_derivs, n) 

391 if return_full_cov: 

392 return reshaped_mean, reshaped_var, f_cov_full 

393 return reshaped_mean, reshaped_var