Coverage for /builds/kinetik161/ase/ase/optimize/oldqn.py: 72.28%
267 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1# Copyright (C) 2003 CAMP
2# Please see the accompanying LICENSE file for further information.
4"""
5Quasi-Newton algorithm
6"""
8__docformat__ = 'reStructuredText'
10import time
11from typing import IO, Optional, Union
13import numpy as np
14from numpy.linalg import eigh
16from ase import Atoms
17from ase.optimize.optimize import Optimizer
20def f(lamda, Gbar, b, radius):
21 b1 = b - lamda
22 g = radius**2 - np.dot(Gbar / b1, Gbar / b1)
23 return g
26def scale_radius_energy(f, r):
27 scale = 1.0
28# if(r<=0.01):
29# return scale
31 if f < 0.01:
32 scale *= 1.4
33 if f < 0.05:
34 scale *= 1.4
35 if f < 0.10:
36 scale *= 1.4
37 if f < 0.40:
38 scale *= 1.4
40 if f > 0.5:
41 scale *= 1. / 1.4
42 if f > 0.7:
43 scale *= 1. / 1.4
44 if f > 1.0:
45 scale *= 1. / 1.4
47 return scale
50def scale_radius_force(f, r):
51 scale = 1.0
52# if(r<=0.01):
53# return scale
54 g = abs(f - 1)
55 if g < 0.01:
56 scale *= 1.4
57 if g < 0.05:
58 scale *= 1.4
59 if g < 0.10:
60 scale *= 1.4
61 if g < 0.40:
62 scale *= 1.4
64 if g > 0.5:
65 scale *= 1. / 1.4
66 if g > 0.7:
67 scale *= 1. / 1.4
68 if g > 1.0:
69 scale *= 1. / 1.4
71 return scale
74def find_lamda(upperlimit, Gbar, b, radius):
75 lowerlimit = upperlimit
76 step = 0.1
77 while f(lowerlimit, Gbar, b, radius) < 0:
78 lowerlimit -= step
80 converged = False
82 while not converged:
84 midt = (upperlimit + lowerlimit) / 2.
85 lamda = midt
86 fmidt = f(midt, Gbar, b, radius)
87 fupper = f(upperlimit, Gbar, b, radius)
89 if fupper * fmidt < 0:
90 lowerlimit = midt
91 else:
92 upperlimit = midt
94 if abs(upperlimit - lowerlimit) < 1e-6:
95 converged = True
97 return lamda
100class GoodOldQuasiNewton(Optimizer):
102 def __init__(
103 self,
104 atoms: Atoms,
105 restart: Optional[str] = None,
106 logfile: Union[IO, str] = '-',
107 trajectory: Optional[str] = None,
108 fmax=None,
109 converged=None,
110 hessianupdate: str = 'BFGS',
111 hessian=None,
112 forcemin: bool = True,
113 verbosity: bool = False,
114 maxradius: Optional[float] = None,
115 diagonal: float = 20.0,
116 radius: Optional[float] = None,
117 transitionstate: bool = False,
118 master: Optional[bool] = None,
119 ):
120 """Parameters:
122 atoms: Atoms object
123 The Atoms object to relax.
125 restart: string
126 File used to store hessian matrix. If set, file with
127 such a name will be searched and hessian matrix stored will
128 be used, if the file exists.
130 trajectory: string
131 File used to store trajectory of atomic movement.
133 maxstep: float
134 Used to set the maximum distance an atom can move per
135 iteration (default value is 0.2 Angstroms).
138 logfile: file object or str
139 If *logfile* is a string, a file with that name will be opened.
140 Use '-' for stdout.
142 master: boolean
143 Defaults to None, which causes only rank 0 to save files. If
144 set to true, this rank will save files.
145 """
147 Optimizer.__init__(self, atoms, restart, logfile, trajectory, master)
149 self.eps = 1e-12
150 self.hessianupdate = hessianupdate
151 self.forcemin = forcemin
152 self.verbosity = verbosity
153 self.diagonal = diagonal
155 n = len(self.optimizable) * 3
156 if radius is None:
157 self.radius = 0.05 * np.sqrt(n) / 10.0
158 else:
159 self.radius = radius
161 if maxradius is None:
162 self.maxradius = 0.5 * np.sqrt(n)
163 else:
164 self.maxradius = maxradius
166 # 0.01 < radius < maxradius
167 self.radius = max(min(self.radius, self.maxradius), 0.0001)
169 self.transitionstate = transitionstate
171 if self.optimizable.is_neb():
172 self.forcemin = False
174 self.t0 = time.time()
176 def initialize(self):
177 pass
179 def write_log(self, text):
180 if self.logfile is not None:
181 self.logfile.write(text + '\n')
182 self.logfile.flush()
184 def set_hessian(self, hessian):
185 self.hessian = hessian
187 def get_hessian(self):
188 if not hasattr(self, 'hessian'):
189 self.set_default_hessian()
190 return self.hessian
192 def set_default_hessian(self):
193 # set unit matrix
194 n = len(self.optimizable) * 3
195 hessian = np.zeros((n, n))
196 for i in range(n):
197 hessian[i][i] = self.diagonal
198 self.set_hessian(hessian)
200 def update_hessian(self, pos, G):
201 import copy
202 if hasattr(self, 'oldG'):
203 if self.hessianupdate == 'BFGS':
204 self.update_hessian_bfgs(pos, G)
205 elif self.hessianupdate == 'Powell':
206 self.update_hessian_powell(pos, G)
207 else:
208 self.update_hessian_bofill(pos, G)
209 else:
210 if not hasattr(self, 'hessian'):
211 self.set_default_hessian()
213 self.oldpos = copy.copy(pos)
214 self.oldG = copy.copy(G)
216 if self.verbosity:
217 print('hessian ', self.hessian)
219 def update_hessian_bfgs(self, pos, G):
220 n = len(self.hessian)
221 dgrad = G - self.oldG
222 dpos = pos - self.oldpos
223 dotg = np.dot(dgrad, dpos)
224 tvec = np.dot(dpos, self.hessian)
225 dott = np.dot(dpos, tvec)
226 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
227 for i in range(n):
228 for j in range(n):
229 h = dgrad[i] * dgrad[j] / dotg - tvec[i] * tvec[j] / dott
230 self.hessian[i][j] += h
232 def update_hessian_powell(self, pos, G):
233 n = len(self.hessian)
234 dgrad = G - self.oldG
235 dpos = pos - self.oldpos
236 absdpos = np.dot(dpos, dpos)
237 if absdpos < self.eps:
238 return
240 dotg = np.dot(dgrad, dpos)
241 tvec = dgrad - np.dot(dpos, self.hessian)
242 tvecdpos = np.dot(tvec, dpos)
243 ddot = tvecdpos / absdpos
245 dott = np.dot(dpos, tvec)
246 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
247 for i in range(n):
248 for j in range(n):
249 h = tvec[i] * dpos[j] + dpos[i] * \
250 tvec[j] - ddot * dpos[i] * dpos[j]
251 h *= 1. / absdpos
252 self.hessian[i][j] += h
254 def update_hessian_bofill(self, pos, G):
255 print('update Bofill')
256 n = len(self.hessian)
257 dgrad = G - self.oldG
258 dpos = pos - self.oldpos
259 absdpos = np.dot(dpos, dpos)
260 if absdpos < self.eps:
261 return
262 dotg = np.dot(dgrad, dpos)
263 tvec = dgrad - np.dot(dpos, self.hessian)
264 tvecdot = np.dot(tvec, tvec)
265 tvecdpos = np.dot(tvec, dpos)
267 coef1 = 1. - tvecdpos * tvecdpos / (absdpos * tvecdot)
268 coef2 = (1. - coef1) * absdpos / tvecdpos
269 coef3 = coef1 * tvecdpos / absdpos
271 dott = np.dot(dpos, tvec)
272 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
273 for i in range(n):
274 for j in range(n):
275 h = coef1 * (tvec[i] * dpos[j] + dpos[i] * tvec[j]) - \
276 dpos[i] * dpos[j] * coef3 + coef2 * tvec[i] * tvec[j]
277 h *= 1. / absdpos
278 self.hessian[i][j] += h
280 def step(self, forces=None):
281 """ Do one QN step
282 """
284 if forces is None:
285 forces = self.optimizable.get_forces()
287 pos = self.optimizable.get_positions().ravel()
288 G = -self.optimizable.get_forces().ravel()
290 energy = self.optimizable.get_potential_energy()
292 if hasattr(self, 'oldenergy'):
293 self.write_log('energies ' + str(energy) +
294 ' ' + str(self.oldenergy))
296 if self.forcemin:
297 de = 1e-4
298 else:
299 de = 1e-2
301 if self.transitionstate:
302 de = 0.2
304 if (energy - self.oldenergy) > de:
305 self.write_log('reject step')
306 self.optimizable.set_positions(self.oldpos.reshape((-1, 3)))
307 G = self.oldG
308 energy = self.oldenergy
309 self.radius *= 0.5
310 else:
311 self.update_hessian(pos, G)
312 de = energy - self.oldenergy
313 forces = 1.0
314 if self.forcemin:
315 self.write_log(
316 "energy change; actual: %f estimated: %f " %
317 (de, self.energy_estimate))
318 if abs(self.energy_estimate) > self.eps:
319 forces = abs((de / self.energy_estimate) - 1)
320 self.write_log('Energy prediction factor '
321 + str(forces))
322 # fg = self.get_force_prediction(G)
323 self.radius *= scale_radius_energy(forces, self.radius)
325 else:
326 self.write_log("energy change; actual: %f " % (de))
327 self.radius *= 1.5
329 fg = self.get_force_prediction(G)
330 self.write_log("Scale factors %f %f " %
331 (scale_radius_energy(forces, self.radius),
332 scale_radius_force(fg, self.radius)))
334 self.radius = max(min(self.radius, self.maxradius), 0.0001)
335 else:
336 self.update_hessian(pos, G)
338 self.write_log("new radius %f " % (self.radius))
339 self.oldenergy = energy
341 b, V = eigh(self.hessian)
342 V = V.T.copy()
343 self.V = V
345 # calculate projection of G onto eigenvectors V
346 Gbar = np.dot(G, np.transpose(V))
348 lamdas = self.get_lambdas(b, Gbar)
350 D = -Gbar / (b - lamdas)
351 n = len(D)
352 step = np.zeros(n)
353 for i in range(n):
354 step += D[i] * V[i]
356 pos = self.optimizable.get_positions().ravel()
357 pos += step
359 energy_estimate = self.get_energy_estimate(D, Gbar, b)
360 self.energy_estimate = energy_estimate
361 self.gbar_estimate = self.get_gbar_estimate(D, Gbar, b)
362 self.old_gbar = Gbar
364 self.optimizable.set_positions(pos.reshape((-1, 3)))
366 def get_energy_estimate(self, D, Gbar, b):
368 de = 0.0
369 for n in range(len(D)):
370 de += D[n] * Gbar[n] + 0.5 * D[n] * b[n] * D[n]
371 return de
373 def get_gbar_estimate(self, D, Gbar, b):
374 gbar_est = (D * b) + Gbar
375 self.write_log('Abs Gbar estimate ' + str(np.dot(gbar_est, gbar_est)))
376 return gbar_est
378 def get_lambdas(self, b, Gbar):
379 lamdas = np.zeros(len(b))
381 D = -Gbar / b
382 absD = np.sqrt(np.dot(D, D))
384 eps = 1e-12
385 nminus = self.get_hessian_inertia(b)
387 if absD < self.radius:
388 if not self.transitionstate:
389 self.write_log('Newton step')
390 return lamdas
391 else:
392 if nminus == 1:
393 self.write_log('Newton step')
394 return lamdas
395 else:
396 self.write_log(
397 "Wrong inertia of Hessian matrix: %2.2f %2.2f " %
398 (b[0], b[1]))
400 else:
401 self.write_log("Corrected Newton step: abs(D) = %2.2f " % (absD))
403 if not self.transitionstate:
404 # upper limit
405 upperlimit = min(0, b[0]) - eps
406 lamda = find_lamda(upperlimit, Gbar, b, self.radius)
407 lamdas += lamda
408 else:
409 # upperlimit
410 upperlimit = min(-b[0], b[1], 0) - eps
411 lamda = find_lamda(upperlimit, Gbar, b, self.radius)
412 lamdas += lamda
413 lamdas[0] -= 2 * lamda
415 return lamdas
417 def get_hessian_inertia(self, eigenvalues):
418 # return number of negative modes
419 self.write_log("eigenvalues {:2.2f} {:2.2f} {:2.2f} ".format(
420 eigenvalues[0], eigenvalues[1], eigenvalues[2]))
421 n = 0
422 while eigenvalues[n] < 0:
423 n += 1
424 return n
426 def get_force_prediction(self, G):
427 # return measure of how well the forces are predicted
428 Gbar = np.dot(G, np.transpose(self.V))
429 dGbar_actual = Gbar - self.old_gbar
430 dGbar_predicted = Gbar - self.gbar_estimate
432 f = np.dot(dGbar_actual, dGbar_predicted) / \
433 np.dot(dGbar_actual, dGbar_actual)
434 self.write_log('Force prediction factor ' + str(f))
435 return f