Coverage for /builds/debichem-team/python-ase/ase/optimize/oldqn.py: 72.18%
266 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +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 **kwargs,
119 ):
120 """
122 Parameters
123 ----------
124 atoms: :class:`~ase.Atoms`
125 The Atoms object to relax.
127 restart: str
128 File used to store hessian matrix. If set, file with
129 such a name will be searched and hessian matrix stored will
130 be used, if the file exists.
132 trajectory: str
133 File used to store trajectory of atomic movement.
135 maxstep: float
136 Used to set the maximum distance an atom can move per
137 iteration (default value is 0.2 Angstroms).
140 logfile: file object or str
141 If *logfile* is a string, a file with that name will be opened.
142 Use '-' for stdout.
144 kwargs : dict, optional
145 Extra arguments passed to
146 :class:`~ase.optimize.optimize.Optimizer`.
148 """
150 Optimizer.__init__(self, atoms, restart, logfile, trajectory, **kwargs)
152 self.eps = 1e-12
153 self.hessianupdate = hessianupdate
154 self.forcemin = forcemin
155 self.verbosity = verbosity
156 self.diagonal = diagonal
158 n = len(self.optimizable) * 3
159 if radius is None:
160 self.radius = 0.05 * np.sqrt(n) / 10.0
161 else:
162 self.radius = radius
164 if maxradius is None:
165 self.maxradius = 0.5 * np.sqrt(n)
166 else:
167 self.maxradius = maxradius
169 # 0.01 < radius < maxradius
170 self.radius = max(min(self.radius, self.maxradius), 0.0001)
172 self.transitionstate = transitionstate
174 if self.optimizable.is_neb():
175 self.forcemin = False
177 self.t0 = time.time()
179 def initialize(self):
180 pass
182 def write_log(self, text):
183 if self.logfile is not None:
184 self.logfile.write(text + '\n')
185 self.logfile.flush()
187 def set_hessian(self, hessian):
188 self.hessian = hessian
190 def get_hessian(self):
191 if not hasattr(self, 'hessian'):
192 self.set_default_hessian()
193 return self.hessian
195 def set_default_hessian(self):
196 # set unit matrix
197 n = len(self.optimizable) * 3
198 hessian = np.zeros((n, n))
199 for i in range(n):
200 hessian[i][i] = self.diagonal
201 self.set_hessian(hessian)
203 def update_hessian(self, pos, G):
204 import copy
205 if hasattr(self, 'oldG'):
206 if self.hessianupdate == 'BFGS':
207 self.update_hessian_bfgs(pos, G)
208 elif self.hessianupdate == 'Powell':
209 self.update_hessian_powell(pos, G)
210 else:
211 self.update_hessian_bofill(pos, G)
212 else:
213 if not hasattr(self, 'hessian'):
214 self.set_default_hessian()
216 self.oldpos = copy.copy(pos)
217 self.oldG = copy.copy(G)
219 if self.verbosity:
220 print('hessian ', self.hessian)
222 def update_hessian_bfgs(self, pos, G):
223 n = len(self.hessian)
224 dgrad = G - self.oldG
225 dpos = pos - self.oldpos
226 dotg = np.dot(dgrad, dpos)
227 tvec = np.dot(dpos, self.hessian)
228 dott = np.dot(dpos, tvec)
229 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
230 for i in range(n):
231 for j in range(n):
232 h = dgrad[i] * dgrad[j] / dotg - tvec[i] * tvec[j] / dott
233 self.hessian[i][j] += h
235 def update_hessian_powell(self, pos, G):
236 n = len(self.hessian)
237 dgrad = G - self.oldG
238 dpos = pos - self.oldpos
239 absdpos = np.dot(dpos, dpos)
240 if absdpos < self.eps:
241 return
243 dotg = np.dot(dgrad, dpos)
244 tvec = dgrad - np.dot(dpos, self.hessian)
245 tvecdpos = np.dot(tvec, dpos)
246 ddot = tvecdpos / absdpos
248 dott = np.dot(dpos, tvec)
249 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
250 for i in range(n):
251 for j in range(n):
252 h = tvec[i] * dpos[j] + dpos[i] * \
253 tvec[j] - ddot * dpos[i] * dpos[j]
254 h *= 1. / absdpos
255 self.hessian[i][j] += h
257 def update_hessian_bofill(self, pos, G):
258 print('update Bofill')
259 n = len(self.hessian)
260 dgrad = G - self.oldG
261 dpos = pos - self.oldpos
262 absdpos = np.dot(dpos, dpos)
263 if absdpos < self.eps:
264 return
265 dotg = np.dot(dgrad, dpos)
266 tvec = dgrad - np.dot(dpos, self.hessian)
267 tvecdot = np.dot(tvec, tvec)
268 tvecdpos = np.dot(tvec, dpos)
270 coef1 = 1. - tvecdpos * tvecdpos / (absdpos * tvecdot)
271 coef2 = (1. - coef1) * absdpos / tvecdpos
272 coef3 = coef1 * tvecdpos / absdpos
274 dott = np.dot(dpos, tvec)
275 if (abs(dott) > self.eps) and (abs(dotg) > self.eps):
276 for i in range(n):
277 for j in range(n):
278 h = coef1 * (tvec[i] * dpos[j] + dpos[i] * tvec[j]) - \
279 dpos[i] * dpos[j] * coef3 + coef2 * tvec[i] * tvec[j]
280 h *= 1. / absdpos
281 self.hessian[i][j] += h
283 def step(self, forces=None):
284 """ Do one QN step
285 """
287 if forces is None:
288 forces = self.optimizable.get_forces()
290 pos = self.optimizable.get_positions().ravel()
291 G = -self.optimizable.get_forces().ravel()
293 energy = self.optimizable.get_potential_energy()
295 if hasattr(self, 'oldenergy'):
296 self.write_log('energies ' + str(energy) +
297 ' ' + str(self.oldenergy))
299 if self.forcemin:
300 de = 1e-4
301 else:
302 de = 1e-2
304 if self.transitionstate:
305 de = 0.2
307 if (energy - self.oldenergy) > de:
308 self.write_log('reject step')
309 self.optimizable.set_positions(self.oldpos.reshape((-1, 3)))
310 G = self.oldG
311 energy = self.oldenergy
312 self.radius *= 0.5
313 else:
314 self.update_hessian(pos, G)
315 de = energy - self.oldenergy
316 forces = 1.0
317 if self.forcemin:
318 self.write_log(
319 "energy change; actual: %f estimated: %f " %
320 (de, self.energy_estimate))
321 if abs(self.energy_estimate) > self.eps:
322 forces = abs((de / self.energy_estimate) - 1)
323 self.write_log('Energy prediction factor '
324 + str(forces))
325 # fg = self.get_force_prediction(G)
326 self.radius *= scale_radius_energy(forces, self.radius)
328 else:
329 self.write_log("energy change; actual: %f " % (de))
330 self.radius *= 1.5
332 fg = self.get_force_prediction(G)
333 self.write_log("Scale factors %f %f " %
334 (scale_radius_energy(forces, self.radius),
335 scale_radius_force(fg, self.radius)))
337 self.radius = max(min(self.radius, self.maxradius), 0.0001)
338 else:
339 self.update_hessian(pos, G)
341 self.write_log("new radius %f " % (self.radius))
342 self.oldenergy = energy
344 b, V = eigh(self.hessian)
345 V = V.T.copy()
346 self.V = V
348 # calculate projection of G onto eigenvectors V
349 Gbar = np.dot(G, np.transpose(V))
351 lamdas = self.get_lambdas(b, Gbar)
353 D = -Gbar / (b - lamdas)
354 n = len(D)
355 step = np.zeros(n)
356 for i in range(n):
357 step += D[i] * V[i]
359 pos = self.optimizable.get_positions().ravel()
360 pos += step
362 energy_estimate = self.get_energy_estimate(D, Gbar, b)
363 self.energy_estimate = energy_estimate
364 self.gbar_estimate = self.get_gbar_estimate(D, Gbar, b)
365 self.old_gbar = Gbar
367 self.optimizable.set_positions(pos.reshape((-1, 3)))
369 def get_energy_estimate(self, D, Gbar, b):
371 de = 0.0
372 for n in range(len(D)):
373 de += D[n] * Gbar[n] + 0.5 * D[n] * b[n] * D[n]
374 return de
376 def get_gbar_estimate(self, D, Gbar, b):
377 gbar_est = (D * b) + Gbar
378 self.write_log('Abs Gbar estimate ' + str(np.dot(gbar_est, gbar_est)))
379 return gbar_est
381 def get_lambdas(self, b, Gbar):
382 lamdas = np.zeros(len(b))
384 D = -Gbar / b
385 absD = np.sqrt(np.dot(D, D))
387 eps = 1e-12
388 nminus = self.get_hessian_inertia(b)
390 if absD < self.radius:
391 if not self.transitionstate:
392 self.write_log('Newton step')
393 return lamdas
394 else:
395 if nminus == 1:
396 self.write_log('Newton step')
397 return lamdas
398 else:
399 self.write_log(
400 "Wrong inertia of Hessian matrix: %2.2f %2.2f " %
401 (b[0], b[1]))
403 else:
404 self.write_log("Corrected Newton step: abs(D) = %2.2f " % (absD))
406 if not self.transitionstate:
407 # upper limit
408 upperlimit = min(0, b[0]) - eps
409 lamda = find_lamda(upperlimit, Gbar, b, self.radius)
410 lamdas += lamda
411 else:
412 # upperlimit
413 upperlimit = min(-b[0], b[1], 0) - eps
414 lamda = find_lamda(upperlimit, Gbar, b, self.radius)
415 lamdas += lamda
416 lamdas[0] -= 2 * lamda
418 return lamdas
420 def get_hessian_inertia(self, eigenvalues):
421 # return number of negative modes
422 self.write_log("eigenvalues {:2.2f} {:2.2f} {:2.2f} ".format(
423 eigenvalues[0], eigenvalues[1], eigenvalues[2]))
424 n = 0
425 while eigenvalues[n] < 0:
426 n += 1
427 return n
429 def get_force_prediction(self, G):
430 # return measure of how well the forces are predicted
431 Gbar = np.dot(G, np.transpose(self.V))
432 dGbar_actual = Gbar - self.old_gbar
433 dGbar_predicted = Gbar - self.gbar_estimate
435 f = np.dot(dGbar_actual, dGbar_predicted) / \
436 np.dot(dGbar_actual, dGbar_actual)
437 self.write_log('Force prediction factor ' + str(f))
438 return f