Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import time
2import warnings
4from math import sqrt
5import numpy as np
7from ase.optimize.optimize import Optimizer
8from ase.constraints import UnitCellFilter
10from ase.utils.linesearch import LineSearch
11from ase.utils.linesearcharmijo import LineSearchArmijo
13from ase.optimize.precon.precon import make_precon
16class PreconLBFGS(Optimizer):
17 """Preconditioned version of the Limited memory BFGS optimizer, to
18 be used as a drop-in replacement for ase.optimize.lbfgs.LBFGS for systems
19 where a good preconditioner is available.
21 In the standard bfgs and lbfgs algorithms, the inverse of Hessian matrix
22 is a (usually fixed) diagonal matrix. By contrast, PreconLBFGS,
23 updates the hessian after each step with a general "preconditioner".
24 By default, the ase.optimize.precon.Exp preconditioner is applied.
25 This preconditioner is well-suited for large condensed phase structures,
26 in particular crystalline. For systems outside this category,
27 PreconLBFGS with Exp preconditioner may yield unpredictable results.
29 In time this implementation is expected to replace
30 ase.optimize.lbfgs.LBFGS.
32 See this article for full details: D. Packwood, J. R. Kermode, L. Mones,
33 N. Bernstein, J. Woolley, N. Gould, C. Ortner, and G. Csanyi, A universal
34 preconditioner for simulating condensed phase materials
35 J. Chem. Phys. 144, 164109 (2016), DOI: https://doi.org/10.1063/1.4947024
36 """
38 # CO : added parameters rigid_units and rotation_factors
39 def __init__(self, atoms, restart=None, logfile='-', trajectory=None,
40 maxstep=None, memory=100, damping=1.0, alpha=70.0,
41 master=None, precon='auto', variable_cell=False,
42 use_armijo=True, c1=0.23, c2=0.46, a_min=None,
43 rigid_units=None, rotation_factors=None, Hinv=None):
44 """Parameters:
46 atoms: Atoms object
47 The Atoms object to relax.
49 restart: string
50 Pickle file used to store vectors for updating the inverse of
51 Hessian matrix. If set, file with such a name will be searched
52 and information stored will be used, if the file exists.
54 logfile: file object or str
55 If *logfile* is a string, a file with that name will be opened.
56 Use '-' for stdout.
58 trajectory: string
59 Pickle file used to store trajectory of atomic movement.
61 maxstep: float
62 How far is a single atom allowed to move. This is useful for DFT
63 calculations where wavefunctions can be reused if steps are small.
64 Default is 0.04 Angstrom.
66 memory: int
67 Number of steps to be stored. Default value is 100. Three numpy
68 arrays of this length containing floats are stored.
70 damping: float
71 The calculated step is multiplied with this number before added to
72 the positions.
74 alpha: float
75 Initial guess for the Hessian (curvature of energy surface). A
76 conservative value of 70.0 is the default, but number of needed
77 steps to converge might be less if a lower value is used. However,
78 a lower value also means risk of instability.
80 master: boolean
81 Defaults to None, which causes only rank 0 to save files. If
82 set to true, this rank will save files.
84 precon: ase.optimize.precon.Precon instance or compatible.
85 Apply the given preconditioner during optimization. Defaults to
86 'auto', which will choose the `Exp` preconditioner unless the system
87 is too small (< 100 atoms) in which case a standard LBFGS fallback
88 is used. To enforce use of the `Exp` preconditioner, use `precon =
89 'Exp'`. Other options include 'C1', 'Pfrommer' and 'FF' - see the
90 corresponding classes in the `ase.optimize.precon` module for more
91 details. Pass precon=None or precon='ID' to disable preconditioning.
93 use_armijo: boolean
94 Enforce only the Armijo condition of sufficient decrease of
95 of the energy, and not the second Wolff condition for the forces.
96 Often significantly faster than full Wolff linesearch.
97 Defaults to True.
99 c1: float
100 c1 parameter for the line search. Default is c1=0.23.
102 c2: float
103 c2 parameter for the line search. Default is c2=0.46.
105 a_min: float
106 minimal value for the line search step parameter. Default is
107 a_min=1e-8 (use_armijo=False) or 1e-10 (use_armijo=True).
108 Higher values can be useful to avoid performing many
109 line searches for comparatively small changes in geometry.
111 variable_cell: bool
112 If True, wrap atoms an ase.constraints.UnitCellFilter to
113 relax both postions and cell. Default is False.
115 rigid_units: each I = rigid_units[i] is a list of indices, which
116 describes a subsystem of atoms that forms a (near-)rigid unit
117 If rigid_units is not None, then special search-paths are
118 are created to take the rigidness into account
120 rotation_factors: list of scalars; acceleration factors deteriming
121 the rate of rotation as opposed to the rate of stretch in the
122 rigid units
123 """
124 if variable_cell:
125 atoms = UnitCellFilter(atoms)
126 Optimizer.__init__(self, atoms, restart, logfile, trajectory, master)
128 # default preconditioner
129 # TODO: introduce a heuristic for different choices of preconditioners
130 if precon == 'auto':
131 if len(atoms) < 100:
132 precon = None
133 warnings.warn('The system is likely too small to benefit from '
134 'the standard preconditioner, hence it is '
135 'disabled. To re-enable preconditioning, call '
136 '`PreconLBFGS` by explicitly providing the '
137 'kwarg `precon`')
138 else:
139 precon = 'Exp'
141 if maxstep is not None:
142 if maxstep > 1.0:
143 raise ValueError('You are using a much too large value for ' +
144 'the maximum step size: %.1f Angstrom' %
145 maxstep)
146 self.maxstep = maxstep
147 else:
148 self.maxstep = 0.04
150 self.memory = memory
151 self.H0 = 1. / alpha # Initial approximation of inverse Hessian
152 # 1./70. is to emulate the behaviour of BFGS
153 # Note that this is never changed!
154 self.Hinv = Hinv
155 self.damping = damping
156 self.p = None
158 # construct preconditioner if passed as a string
159 self.precon = make_precon(precon)
160 self.use_armijo = use_armijo
161 self.c1 = c1
162 self.c2 = c2
163 self.a_min = a_min
164 if self.a_min is None:
165 self.a_min = 1e-10 if use_armijo else 1e-8
167 # CO
168 self.rigid_units = rigid_units
169 self.rotation_factors = rotation_factors
171 def reset_hessian(self):
172 """
173 Throw away history of the Hessian
174 """
175 self._just_reset_hessian = True
176 self.s = []
177 self.y = []
178 self.rho = [] # Store also rho, to avoid calculating the dot product
179 # again and again
181 def initialize(self):
182 """Initialize everything so no checks have to be done in step"""
183 self.iteration = 0
184 self.reset_hessian()
185 self.r0 = None
186 self.f0 = None
187 self.e0 = None
188 self.e1 = None
189 self.task = 'START'
190 self.load_restart = False
192 def read(self):
193 """Load saved arrays to reconstruct the Hessian"""
194 self.iteration, self.s, self.y, self.rho, \
195 self.r0, self.f0, self.e0, self.task = self.load()
196 self.load_restart = True
198 def step(self, f=None):
199 """Take a single step
201 Use the given forces, update the history and calculate the next step --
202 then take it"""
203 r = self.atoms.get_positions()
205 if f is None:
206 f = self.atoms.get_forces()
208 previously_reset_hessian = self._just_reset_hessian
209 self.update(r, f, self.r0, self.f0)
211 s = self.s
212 y = self.y
213 rho = self.rho
214 H0 = self.H0
216 loopmax = np.min([self.memory, len(self.y)])
217 a = np.empty((loopmax,), dtype=np.float64)
219 # The algorithm itself:
220 q = -f.reshape(-1)
221 for i in range(loopmax - 1, -1, -1):
222 a[i] = rho[i] * np.dot(s[i], q)
223 q -= a[i] * y[i]
225 if self.precon is None:
226 if self.Hinv is not None:
227 z = np.dot(self.Hinv, q)
228 else:
229 z = H0 * q
230 else:
231 self.precon.make_precon(self.atoms)
232 z = self.precon.solve(q)
234 for i in range(loopmax):
235 b = rho[i] * np.dot(y[i], z)
236 z += s[i] * (a[i] - b)
238 self.p = - z.reshape((-1, 3))
239 ###
241 g = -f
242 if self.e1 is not None:
243 e = self.e1
244 else:
245 e = self.func(r)
246 self.line_search(r, g, e, previously_reset_hessian)
247 dr = (self.alpha_k * self.p).reshape(len(self.atoms), -1)
249 if self.alpha_k != 0.0:
250 self.atoms.set_positions(r + dr)
252 self.iteration += 1
253 self.r0 = r
254 self.f0 = -g
255 self.dump((self.iteration, self.s, self.y,
256 self.rho, self.r0, self.f0, self.e0, self.task))
258 def update(self, r, f, r0, f0):
259 """Update everything that is kept in memory
261 This function is mostly here to allow for replay_trajectory.
262 """
263 if not self._just_reset_hessian:
264 s0 = r.reshape(-1) - r0.reshape(-1)
265 self.s.append(s0)
267 # We use the gradient which is minus the force!
268 y0 = f0.reshape(-1) - f.reshape(-1)
269 self.y.append(y0)
271 rho0 = 1.0 / np.dot(y0, s0)
272 self.rho.append(rho0)
273 self._just_reset_hessian = False
275 if len(self.y) > self.memory:
276 self.s.pop(0)
277 self.y.pop(0)
278 self.rho.pop(0)
280 def replay_trajectory(self, traj):
281 """Initialize history from old trajectory."""
282 if isinstance(traj, str):
283 from ase.io.trajectory import Trajectory
284 traj = Trajectory(traj, 'r')
285 r0 = None
286 f0 = None
287 # The last element is not added, as we get that for free when taking
288 # the first qn-step after the replay
289 for i in range(0, len(traj) - 1):
290 r = traj[i].get_positions()
291 f = traj[i].get_forces()
292 self.update(r, f, r0, f0)
293 r0 = r.copy()
294 f0 = f.copy()
295 self.iteration += 1
296 self.r0 = r0
297 self.f0 = f0
299 def func(self, x):
300 """Objective function for use of the optimizers"""
301 self.atoms.set_positions(x.reshape(-1, 3))
302 potl = self.atoms.get_potential_energy()
303 return potl
305 def fprime(self, x):
306 """Gradient of the objective function for use of the optimizers"""
307 self.atoms.set_positions(x.reshape(-1, 3))
308 # Remember that forces are minus the gradient!
309 return -self.atoms.get_forces().reshape(-1)
311 def line_search(self, r, g, e, previously_reset_hessian):
312 self.p = self.p.ravel()
313 p_size = np.sqrt((self.p ** 2).sum())
314 if p_size <= np.sqrt(len(self.atoms) * 1e-10):
315 self.p /= (p_size / np.sqrt(len(self.atoms) * 1e-10))
316 g = g.ravel()
317 r = r.ravel()
319 if self.use_armijo:
320 try:
321 # CO: modified call to ls.run
322 # TODO: pass also the old slope to the linesearch
323 # so that the RumPath can extract a better starting guess?
324 # alternatively: we can adjust the rotation_factors
325 # out using some extrapolation tricks?
326 ls = LineSearchArmijo(self.func, c1=self.c1, tol=1e-14)
327 step, func_val, no_update = ls.run(
328 r, self.p, a_min=self.a_min,
329 func_start=e,
330 func_prime_start=g,
331 func_old=self.e0,
332 rigid_units=self.rigid_units,
333 rotation_factors=self.rotation_factors,
334 maxstep=self.maxstep)
335 self.e0 = e
336 self.e1 = func_val
337 self.alpha_k = step
338 except (ValueError, RuntimeError):
339 if not previously_reset_hessian:
340 warnings.warn(
341 'Armijo linesearch failed, resetting Hessian and '
342 'trying again')
343 self.reset_hessian()
344 self.alpha_k = 0.0
345 else:
346 raise RuntimeError(
347 'Armijo linesearch failed after reset of Hessian, '
348 'aborting')
350 else:
351 ls = LineSearch()
352 self.alpha_k, e, self.e0, self.no_update = \
353 ls._line_search(self.func, self.fprime, r, self.p, g,
354 e, self.e0, stpmin=self.a_min,
355 maxstep=self.maxstep, c1=self.c1,
356 c2=self.c2, stpmax=50.)
357 self.e1 = e
358 if self.alpha_k is None:
359 raise RuntimeError('Wolff lineSearch failed!')
361 def run(self, fmax=0.05, steps=100000000, smax=None):
362 if smax is None:
363 smax = fmax
364 self.smax = smax
365 return Optimizer.run(self, fmax, steps)
367 def log(self, forces=None):
368 if forces is None:
369 forces = self.atoms.get_forces()
370 if isinstance(self.atoms, UnitCellFilter):
371 natoms = len(self.atoms.atoms)
372 forces, stress = forces[:natoms], self.atoms.stress
373 fmax = sqrt((forces**2).sum(axis=1).max())
374 smax = sqrt((stress**2).max())
375 else:
376 fmax = sqrt((forces**2).sum(axis=1).max())
377 if self.e1 is not None:
378 # reuse energy at end of line search to avoid extra call
379 e = self.e1
380 else:
381 e = self.atoms.get_potential_energy()
382 T = time.localtime()
383 if self.logfile is not None:
384 name = self.__class__.__name__
385 if isinstance(self.atoms, UnitCellFilter):
386 self.logfile.write(
387 '%s: %3d %02d:%02d:%02d %15.6f %12.4f %12.4f\n' %
388 (name, self.nsteps, T[3], T[4], T[5], e, fmax, smax))
390 else:
391 self.logfile.write(
392 '%s: %3d %02d:%02d:%02d %15.6f %12.4f\n' %
393 (name, self.nsteps, T[3], T[4], T[5], e, fmax))
394 self.logfile.flush()
396 def converged(self, forces=None):
397 """Did the optimization converge?"""
398 if forces is None:
399 forces = self.atoms.get_forces()
400 if isinstance(self.atoms, UnitCellFilter):
401 natoms = len(self.atoms.atoms)
402 forces, stress = forces[:natoms], self.atoms.stress
403 fmax_sq = (forces**2).sum(axis=1).max()
404 smax_sq = (stress**2).max()
405 return (fmax_sq < self.fmax**2 and smax_sq < self.smax**2)
406 else:
407 fmax_sq = (forces**2).sum(axis=1).max()
408 return fmax_sq < self.fmax**2