Coverage for /builds/debichem-team/python-ase/ase/optimize/gpmin/gpmin.py: 69.92%

123 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1import warnings 

2 

3import numpy as np 

4from scipy.optimize import minimize 

5 

6from ase.io.jsonio import write_json 

7from ase.optimize.gpmin.gp import GaussianProcess 

8from ase.optimize.gpmin.kernel import SquaredExponential 

9from ase.optimize.gpmin.prior import ConstantPrior 

10from ase.optimize.optimize import Optimizer 

11from ase.parallel import world 

12 

13 

14class GPMin(Optimizer, GaussianProcess): 

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

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

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

18 update_hyperparams=False, **kwargs): 

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

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

21 Process (GP) regression and then minimizes it. 

22 

23 Default behaviour: 

24 -------------------- 

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

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

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

28 Default values are: 

29 

30 update_hyperparams = True 

31 scale : 0.3 

32 noise : 0.004 

33 weight: 2. 

34 bounds: 0.1 

35 batch_size: 1 

36 

37 update_hyperparams = False 

38 scale : 0.4 

39 noise : 0.005 

40 weight: 1. 

41 bounds: irrelevant 

42 batch_size: irrelevant 

43 

44 Parameters 

45 ---------- 

46 

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

48 The Atoms object to relax. 

49 

50 restart: str 

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

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

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

54 

55 logfile: file object or str 

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

57 Use '-' for stdout 

58 

59 trajectory: str 

60 File used to store trajectory of atomic movement. 

61 

62 prior: Prior object or None 

63 Prior for the GP regression of the PES surface 

64 See ase.optimize.gpmin.prior 

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

66 ConstantPrior with the constant being updated 

67 using the update_prior_strategy specified as a parameter 

68 

69 kernel: Kernel object or None 

70 Kernel for the GP regression of the PES surface 

71 See ase.optimize.gpmin.kernel 

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

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

74 

75 noise: float 

76 Regularization parameter for the Gaussian Process Regression. 

77 

78 weight: float 

79 Prefactor of the Squared Exponential kernel. 

80 If *update_hyperparams* is False, changing this parameter 

81 has no effect on the dynamics of the algorithm. 

82 

83 update_prior_strategy: str 

84 Strategy to update the constant from the ConstantPrior 

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

86 Prior = None 

87 

88 options: 

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

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

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

92 

93 scale: float 

94 scale of the Squared Exponential Kernel 

95 

96 update_hyperparams: bool 

97 Update the scale of the Squared exponential kernel 

98 every batch_size-th iteration by maximizing the 

99 marginal likelihood. 

100 

101 batch_size: int 

102 Number of new points in the sample before updating 

103 the hyperparameters. 

104 Only relevant if the optimizer is executed in update_hyperparams 

105 mode: (update_hyperparams = True) 

106 

107 bounds: float, 0<bounds<1 

108 Set bounds to the optimization of the hyperparameters. 

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

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

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

112 step. 

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

114 the hyperparameters. 

115 

116 kwargs : dict, optional 

117 Extra arguments passed to 

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

119 

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

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

122 If the number of atoms is sufficiently high, this 

123 may cause a memory issue. 

124 This class prints a warning if the user tries to 

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

126 """ 

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

128 if len(atoms) > 100: 

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

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

131 'of the process will increase with the number ' 

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

133 'Consider using a different optimizer.') 

134 

135 warnings.warn(warning) 

136 

137 # Give it default hyperparameters 

138 if update_hyperparams: # Updated GPMin 

139 if scale is None: 

140 scale = 0.3 

141 if noise is None: 

142 noise = 0.004 

143 if weight is None: 

144 weight = 2. 

145 if bounds is None: 

146 self.eps = 0.1 

147 elif bounds is False: 

148 self.eps = None 

149 else: 

150 self.eps = bounds 

151 if batch_size is None: 

152 self.nbatch = 1 

153 else: 

154 self.nbatch = batch_size 

155 else: # GPMin without updates 

156 if scale is None: 

157 scale = 0.4 

158 if noise is None: 

159 noise = 0.001 

160 if weight is None: 

161 weight = 1. 

162 if bounds is not None: 

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

164 'if update_hyperparams is False. ' 

165 'The value provided by the user ' 

166 'is being ignored.') 

167 warnings.warn(warning, UserWarning) 

168 if batch_size is not None: 

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

170 'if update_hyperparams is False. ' 

171 'The value provided by the user ' 

172 'is being ignored.') 

173 warnings.warn(warning, UserWarning) 

174 

175 # Set the variables to something anyways 

176 self.eps = False 

177 self.nbatch = None 

178 

179 self.strategy = update_prior_strategy 

180 self.update_hp = update_hyperparams 

181 self.function_calls = 1 

182 self.force_calls = 0 

183 self.x_list = [] # Training set features 

184 self.y_list = [] # Training set targets 

185 

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

187 trajectory=trajectory, **kwargs) 

188 if prior is None: 

189 self.update_prior = True 

190 prior = ConstantPrior(constant=None) 

191 else: 

192 self.update_prior = False 

193 

194 if kernel is None: 

195 kernel = SquaredExponential() 

196 GaussianProcess.__init__(self, prior, kernel) 

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

198 

199 def acquisition(self, r): 

200 e = self.predict(r) 

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

202 

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

204 """Update the PES 

205 

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

207 Finally, train the model 

208 """ 

209 # update the training set 

210 self.x_list.append(r) 

211 f = f.reshape(-1) 

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

213 self.y_list.append(y) 

214 

215 # Set/update the constant for the prior 

216 if self.update_prior: 

217 if self.strategy == 'average': 

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

219 self.prior.set_constant(av_e) 

220 elif self.strategy == 'maximum': 

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

222 self.prior.set_constant(max_e) 

223 elif self.strategy == 'init': 

224 self.prior.set_constant(e) 

225 self.update_prior = False 

226 

227 # update hyperparams 

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

229 self.function_calls != 0): 

230 self.fit_to_batch() 

231 

232 # build the model 

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

234 

235 def relax_model(self, r0): 

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

237 if result.success: 

238 return result.x 

239 else: 

240 self.dump() 

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

242 "has not converged") 

243 

244 def fit_to_batch(self): 

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

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

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

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

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

250 

251 def step(self, f=None): 

252 optimizable = self.optimizable 

253 if f is None: 

254 f = optimizable.get_forces() 

255 

256 r0 = optimizable.get_positions().reshape(-1) 

257 e0 = optimizable.get_potential_energy() 

258 self.update(r0, e0, f) 

259 

260 r1 = self.relax_model(r0) 

261 optimizable.set_positions(r1.reshape(-1, 3)) 

262 e1 = optimizable.get_potential_energy() 

263 f1 = optimizable.get_forces() 

264 self.function_calls += 1 

265 self.force_calls += 1 

266 count = 0 

267 while e1 >= e0: 

268 self.update(r1, e1, f1) 

269 r1 = self.relax_model(r0) 

270 

271 optimizable.set_positions(r1.reshape(-1, 3)) 

272 e1 = optimizable.get_potential_energy() 

273 f1 = optimizable.get_forces() 

274 self.function_calls += 1 

275 self.force_calls += 1 

276 if self.converged(f1): 

277 break 

278 

279 count += 1 

280 if count == 30: 

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

282 self.dump() 

283 

284 def dump(self): 

285 """Save the training set""" 

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

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

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

289 

290 def read(self): 

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