Coverage for /builds/ase/ase/ase/optimize/gpmin/gpmin.py: 69.67%

122 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3import warnings 

4 

5import numpy as np 

6from scipy.optimize import minimize 

7 

8from ase.io.jsonio import write_json 

9from ase.optimize.gpmin.gp import GaussianProcess 

10from ase.optimize.gpmin.kernel import SquaredExponential 

11from ase.optimize.gpmin.prior import ConstantPrior 

12from ase.optimize.optimize import Optimizer 

13from ase.parallel import world 

14 

15 

16class GPMin(Optimizer, GaussianProcess): 

17 def __init__(self, atoms, restart=None, logfile='-', trajectory=None, 

18 prior=None, kernel=None, noise=None, weight=None, scale=None, 

19 batch_size=None, bounds=None, update_prior_strategy="maximum", 

20 update_hyperparams=False, **kwargs): 

21 """Optimize atomic positions using GPMin algorithm, which uses both 

22 potential energies and forces information to build a PES via Gaussian 

23 Process (GP) regression and then minimizes it. 

24 

25 Default behaviour: 

26 -------------------- 

27 The default values of the scale, noise, weight, batch_size and bounds 

28 parameters depend on the value of update_hyperparams. In order to get 

29 the default value of any of them, they should be set up to None. 

30 Default values are: 

31 

32 update_hyperparams = True 

33 scale : 0.3 

34 noise : 0.004 

35 weight: 2. 

36 bounds: 0.1 

37 batch_size: 1 

38 

39 update_hyperparams = False 

40 scale : 0.4 

41 noise : 0.005 

42 weight: 1. 

43 bounds: irrelevant 

44 batch_size: irrelevant 

45 

46 Parameters 

47 ---------- 

48 

49 atoms: :class:`~ase.Atoms` 

50 The Atoms object to relax. 

51 

52 restart: str 

53 JSON file used to store the training set. If set, file with 

54 such a name will be searched and the data in the file incorporated 

55 to the new training set, if the file exists. 

56 

57 logfile: file object or str 

58 If *logfile* is a string, a file with that name will be opened. 

59 Use '-' for stdout 

60 

61 trajectory: str 

62 File used to store trajectory of atomic movement. 

63 

64 prior: Prior object or None 

65 Prior for the GP regression of the PES surface 

66 See ase.optimize.gpmin.prior 

67 If *prior* is None, then it is set as the 

68 ConstantPrior with the constant being updated 

69 using the update_prior_strategy specified as a parameter 

70 

71 kernel: Kernel object or None 

72 Kernel for the GP regression of the PES surface 

73 See ase.optimize.gpmin.kernel 

74 If *kernel* is None the SquaredExponential kernel is used. 

75 Note: It needs to be a kernel with derivatives!!!!! 

76 

77 noise: float 

78 Regularization parameter for the Gaussian Process Regression. 

79 

80 weight: float 

81 Prefactor of the Squared Exponential kernel. 

82 If *update_hyperparams* is False, changing this parameter 

83 has no effect on the dynamics of the algorithm. 

84 

85 update_prior_strategy: str 

86 Strategy to update the constant from the ConstantPrior 

87 when more data is collected. It does only work when 

88 Prior = None 

89 

90 options: 

91 'maximum': update the prior to the maximum sampled energy 

92 'init' : fix the prior to the initial energy 

93 'average': use the average of sampled energies as prior 

94 

95 scale: float 

96 scale of the Squared Exponential Kernel 

97 

98 update_hyperparams: bool 

99 Update the scale of the Squared exponential kernel 

100 every batch_size-th iteration by maximizing the 

101 marginal likelihood. 

102 

103 batch_size: int 

104 Number of new points in the sample before updating 

105 the hyperparameters. 

106 Only relevant if the optimizer is executed in update_hyperparams 

107 mode: (update_hyperparams = True) 

108 

109 bounds: float, 0<bounds<1 

110 Set bounds to the optimization of the hyperparameters. 

111 Let t be a hyperparameter. Then it is optimized under the 

112 constraint (1-bound)*t_0 <= t <= (1+bound)*t_0 

113 where t_0 is the value of the hyperparameter in the previous 

114 step. 

115 If bounds is False, no constraints are set in the optimization of 

116 the hyperparameters. 

117 

118 kwargs : dict, optional 

119 Extra arguments passed to 

120 :class:`~ase.optimize.optimize.Optimizer`. 

121 

122 .. warning:: The memory of the optimizer scales as O(n²N²) where 

123 N is the number of atoms and n the number of steps. 

124 If the number of atoms is sufficiently high, this 

125 may cause a memory issue. 

126 This class prints a warning if the user tries to 

127 run GPMin with more than 100 atoms in the unit cell. 

128 """ 

129 # Warn the user if the number of atoms is very large 

130 if len(atoms) > 100: 

131 warning = ('Possible Memory Issue. There are more than ' 

132 '100 atoms in the unit cell. The memory ' 

133 'of the process will increase with the number ' 

134 'of steps, potentially causing a memory issue. ' 

135 'Consider using a different optimizer.') 

136 

137 warnings.warn(warning) 

138 

139 # Give it default hyperparameters 

140 if update_hyperparams: # Updated GPMin 

141 if scale is None: 

142 scale = 0.3 

143 if noise is None: 

144 noise = 0.004 

145 if weight is None: 

146 weight = 2. 

147 if bounds is None: 

148 self.eps = 0.1 

149 elif bounds is False: 

150 self.eps = None 

151 else: 

152 self.eps = bounds 

153 if batch_size is None: 

154 self.nbatch = 1 

155 else: 

156 self.nbatch = batch_size 

157 else: # GPMin without updates 

158 if scale is None: 

159 scale = 0.4 

160 if noise is None: 

161 noise = 0.001 

162 if weight is None: 

163 weight = 1. 

164 if bounds is not None: 

165 warning = ('The parameter bounds is of no use ' 

166 'if update_hyperparams is False. ' 

167 'The value provided by the user ' 

168 'is being ignored.') 

169 warnings.warn(warning, UserWarning) 

170 if batch_size is not None: 

171 warning = ('The parameter batch_size is of no use ' 

172 'if update_hyperparams is False. ' 

173 'The value provided by the user ' 

174 'is being ignored.') 

175 warnings.warn(warning, UserWarning) 

176 

177 # Set the variables to something anyways 

178 self.eps = False 

179 self.nbatch = None 

180 

181 self.strategy = update_prior_strategy 

182 self.update_hp = update_hyperparams 

183 self.function_calls = 1 

184 self.force_calls = 0 

185 self.x_list = [] # Training set features 

186 self.y_list = [] # Training set targets 

187 

188 Optimizer.__init__(self, atoms, restart=restart, logfile=logfile, 

189 trajectory=trajectory, **kwargs) 

190 if prior is None: 

191 self.update_prior = True 

192 prior = ConstantPrior(constant=None) 

193 else: 

194 self.update_prior = False 

195 

196 if kernel is None: 

197 kernel = SquaredExponential() 

198 GaussianProcess.__init__(self, prior, kernel) 

199 self.set_hyperparams(np.array([weight, scale, noise])) 

200 

201 def acquisition(self, r): 

202 e = self.predict(r) 

203 return e[0], e[1:] 

204 

205 def update(self, r, e, f): 

206 """Update the PES 

207 

208 Update the training set, the prior and the hyperparameters. 

209 Finally, train the model 

210 """ 

211 # update the training set 

212 self.x_list.append(r) 

213 y = np.append(np.array(e).reshape(-1), -f) 

214 self.y_list.append(y) 

215 

216 # Set/update the constant for the prior 

217 if self.update_prior: 

218 if self.strategy == 'average': 

219 av_e = np.mean(np.array(self.y_list)[:, 0]) 

220 self.prior.set_constant(av_e) 

221 elif self.strategy == 'maximum': 

222 max_e = np.max(np.array(self.y_list)[:, 0]) 

223 self.prior.set_constant(max_e) 

224 elif self.strategy == 'init': 

225 self.prior.set_constant(e) 

226 self.update_prior = False 

227 

228 # update hyperparams 

229 if (self.update_hp and self.function_calls % self.nbatch == 0 and 

230 self.function_calls != 0): 

231 self.fit_to_batch() 

232 

233 # build the model 

234 self.train(np.array(self.x_list), np.array(self.y_list)) 

235 

236 def relax_model(self, r0): 

237 result = minimize(self.acquisition, r0, method='L-BFGS-B', jac=True) 

238 if result.success: 

239 return result.x 

240 else: 

241 self.dump() 

242 raise RuntimeError("The minimization of the acquisition function " 

243 "has not converged") 

244 

245 def fit_to_batch(self): 

246 """Fit hyperparameters keeping the ratio noise/weight fixed""" 

247 ratio = self.noise / self.kernel.weight 

248 self.fit_hyperparameters(np.array(self.x_list), 

249 np.array(self.y_list), eps=self.eps) 

250 self.noise = ratio * self.kernel.weight 

251 

252 def step(self, f=None): 

253 optimizable = self.optimizable 

254 if f is None: 

255 f = optimizable.get_gradient().reshape(-1, 3) 

256 

257 r0 = optimizable.get_x() 

258 e0 = optimizable.get_value() 

259 self.update(r0, e0, f) 

260 

261 r1 = self.relax_model(r0) 

262 optimizable.set_x(r1) 

263 e1 = optimizable.get_value() 

264 f1 = optimizable.get_gradient() 

265 self.function_calls += 1 

266 self.force_calls += 1 

267 count = 0 

268 while e1 >= e0: 

269 self.update(r1, e1, f1) 

270 r1 = self.relax_model(r0) 

271 

272 optimizable.set_x(r1) 

273 e1 = optimizable.get_value() 

274 f1 = optimizable.get_gradient() 

275 self.function_calls += 1 

276 self.force_calls += 1 

277 if self.converged(f1): 

278 break 

279 

280 count += 1 

281 if count == 30: 

282 raise RuntimeError("A descent model could not be built") 

283 self.dump() 

284 

285 def dump(self): 

286 """Save the training set""" 

287 if world.rank == 0 and self.restart is not None: 

288 with open(self.restart, 'w') as fd: 

289 write_json(fd, (self.x_list, self.y_list)) 

290 

291 def read(self): 

292 self.x_list, self.y_list = self.load()