Coverage for /builds/ase/ase/ase/optimize/lbfgs.py : 80.00%

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 numpy as np
3from ase.optimize.optimize import Optimizer
4from ase.utils.linesearch import LineSearch
7class LBFGS(Optimizer):
8 """Limited memory BFGS optimizer.
10 A limited memory version of the bfgs algorithm. Unlike the bfgs algorithm
11 used in bfgs.py, the inverse of Hessian matrix is updated. The inverse
12 Hessian is represented only as a diagonal matrix to save memory
14 """
16 def __init__(self, atoms, restart=None, logfile='-', trajectory=None,
17 maxstep=None, memory=100, damping=1.0, alpha=70.0,
18 use_line_search=False, master=None,
19 force_consistent=None):
20 """Parameters:
22 atoms: Atoms object
23 The Atoms object to relax.
25 restart: string
26 Pickle file used to store vectors for updating the inverse of
27 Hessian matrix. If set, file with such a name will be searched
28 and information stored will be used, if the file exists.
30 logfile: file object or str
31 If *logfile* is a string, a file with that name will be opened.
32 Use '-' for stdout.
34 trajectory: string
35 Pickle file used to store trajectory of atomic movement.
37 maxstep: float
38 How far is a single atom allowed to move. This is useful for DFT
39 calculations where wavefunctions can be reused if steps are small.
40 Default is 0.2 Angstrom.
42 memory: int
43 Number of steps to be stored. Default value is 100. Three numpy
44 arrays of this length containing floats are stored.
46 damping: float
47 The calculated step is multiplied with this number before added to
48 the positions.
50 alpha: float
51 Initial guess for the Hessian (curvature of energy surface). A
52 conservative value of 70.0 is the default, but number of needed
53 steps to converge might be less if a lower value is used. However,
54 a lower value also means risk of instability.
56 master: boolean
57 Defaults to None, which causes only rank 0 to save files. If
58 set to true, this rank will save files.
60 force_consistent: boolean or None
61 Use force-consistent energy calls (as opposed to the energy
62 extrapolated to 0 K). By default (force_consistent=None) uses
63 force-consistent energies if available in the calculator, but
64 falls back to force_consistent=False if not.
65 """
66 Optimizer.__init__(self, atoms, restart, logfile, trajectory, master,
67 force_consistent=force_consistent)
69 if maxstep is not None:
70 self.maxstep = maxstep
71 else:
72 self.maxstep = self.defaults['maxstep']
74 if self.maxstep > 1.0:
75 raise ValueError('You are using a much too large value for ' +
76 'the maximum step size: %.1f Angstrom' %
77 maxstep)
79 self.memory = memory
80 # Initial approximation of inverse Hessian 1./70. is to emulate the
81 # behaviour of BFGS. Note that this is never changed!
82 self.H0 = 1. / alpha
83 self.damping = damping
84 self.use_line_search = use_line_search
85 self.p = None
86 self.function_calls = 0
87 self.force_calls = 0
89 def initialize(self):
90 """Initialize everything so no checks have to be done in step"""
91 self.iteration = 0
92 self.s = []
93 self.y = []
94 # Store also rho, to avoid calculating the dot product again and
95 # again.
96 self.rho = []
98 self.r0 = None
99 self.f0 = None
100 self.e0 = None
101 self.task = 'START'
102 self.load_restart = False
104 def read(self):
105 """Load saved arrays to reconstruct the Hessian"""
106 self.iteration, self.s, self.y, self.rho, \
107 self.r0, self.f0, self.e0, self.task = self.load()
108 self.load_restart = True
110 def step(self, forces=None):
111 """Take a single step
113 Use the given forces, update the history and calculate the next step --
114 then take it"""
116 if forces is None:
117 forces = self.atoms.get_forces()
119 pos = self.atoms.get_positions()
121 self.update(pos, forces, self.r0, self.f0)
123 s = self.s
124 y = self.y
125 rho = self.rho
126 H0 = self.H0
128 loopmax = np.min([self.memory, self.iteration])
129 a = np.empty((loopmax,), dtype=np.float64)
131 # ## The algorithm itself:
132 q = -forces.reshape(-1)
133 for i in range(loopmax - 1, -1, -1):
134 a[i] = rho[i] * np.dot(s[i], q)
135 q -= a[i] * y[i]
136 z = H0 * q
138 for i in range(loopmax):
139 b = rho[i] * np.dot(y[i], z)
140 z += s[i] * (a[i] - b)
142 self.p = - z.reshape((-1, 3))
143 # ##
145 g = -forces
146 if self.use_line_search is True:
147 e = self.func(pos)
148 self.line_search(pos, g, e)
149 dr = (self.alpha_k * self.p).reshape(len(self.atoms), -1)
150 else:
151 self.force_calls += 1
152 self.function_calls += 1
153 dr = self.determine_step(self.p) * self.damping
154 self.atoms.set_positions(pos + dr)
156 self.iteration += 1
157 self.r0 = pos
158 self.f0 = -g
159 self.dump((self.iteration, self.s, self.y,
160 self.rho, self.r0, self.f0, self.e0, self.task))
162 def determine_step(self, dr):
163 """Determine step to take according to maxstep
165 Normalize all steps as the largest step. This way
166 we still move along the eigendirection.
167 """
168 steplengths = (dr**2).sum(1)**0.5
169 longest_step = np.max(steplengths)
170 if longest_step >= self.maxstep:
171 dr *= self.maxstep / longest_step
173 return dr
175 def update(self, pos, forces, r0, f0):
176 """Update everything that is kept in memory
178 This function is mostly here to allow for replay_trajectory.
179 """
180 if self.iteration > 0:
181 s0 = pos.reshape(-1) - r0.reshape(-1)
182 self.s.append(s0)
184 # We use the gradient which is minus the force!
185 y0 = f0.reshape(-1) - forces.reshape(-1)
186 self.y.append(y0)
188 rho0 = 1.0 / np.dot(y0, s0)
189 self.rho.append(rho0)
191 if self.iteration > self.memory:
192 self.s.pop(0)
193 self.y.pop(0)
194 self.rho.pop(0)
196 def replay_trajectory(self, traj):
197 """Initialize history from old trajectory."""
198 if isinstance(traj, str):
199 from ase.io.trajectory import Trajectory
200 traj = Trajectory(traj, 'r')
201 r0 = None
202 f0 = None
203 # The last element is not added, as we get that for free when taking
204 # the first qn-step after the replay
205 for i in range(0, len(traj) - 1):
206 pos = traj[i].get_positions()
207 forces = traj[i].get_forces()
208 self.update(pos, forces, r0, f0)
209 r0 = pos.copy()
210 f0 = forces.copy()
211 self.iteration += 1
212 self.r0 = r0
213 self.f0 = f0
215 def func(self, x):
216 """Objective function for use of the optimizers"""
217 self.atoms.set_positions(x.reshape(-1, 3))
218 self.function_calls += 1
219 return self.atoms.get_potential_energy(
220 force_consistent=self.force_consistent)
222 def fprime(self, x):
223 """Gradient of the objective function for use of the optimizers"""
224 self.atoms.set_positions(x.reshape(-1, 3))
225 self.force_calls += 1
226 # Remember that forces are minus the gradient!
227 return - self.atoms.get_forces().reshape(-1)
229 def line_search(self, r, g, e):
230 self.p = self.p.ravel()
231 p_size = np.sqrt((self.p**2).sum())
232 if p_size <= np.sqrt(len(self.atoms) * 1e-10):
233 self.p /= (p_size / np.sqrt(len(self.atoms) * 1e-10))
234 g = g.ravel()
235 r = r.ravel()
236 ls = LineSearch()
237 self.alpha_k, e, self.e0, self.no_update = \
238 ls._line_search(self.func, self.fprime, r, self.p, g, e, self.e0,
239 maxstep=self.maxstep, c1=.23,
240 c2=.46, stpmax=50.)
241 if self.alpha_k is None:
242 raise RuntimeError('LineSearch failed!')
245class LBFGSLineSearch(LBFGS):
246 """This optimizer uses the LBFGS algorithm, but does a line search that
247 fulfills the Wolff conditions.
248 """
250 def __init__(self, *args, **kwargs):
251 kwargs['use_line_search'] = True
252 LBFGS.__init__(self, *args, **kwargs)
254# """Modified version of LBFGS.
255#
256# This optimizer uses the LBFGS algorithm, but does a line search for the
257# minimum along the search direction. This is done by issuing an additional
258# force call for each step, thus doubling the number of calculations.
259#
260# Additionally the Hessian is reset if the new guess is not sufficiently
261# better than the old one.
262# """
263# def __init__(self, *args, **kwargs):
264# self.dR = kwargs.pop('dR', 0.1)
265# LBFGS.__init__(self, *args, **kwargs)
266#
267# def update(self, r, f, r0, f0):
268# """Update everything that is kept in memory
269#
270# This function is mostly here to allow for replay_trajectory.
271# """
272# if self.iteration > 0:
273# a1 = abs(np.dot(f.reshape(-1), f0.reshape(-1)))
274# a2 = np.dot(f0.reshape(-1), f0.reshape(-1))
275# if not (a1 <= 0.5 * a2 and a2 != 0):
276# # Reset optimization
277# self.initialize()
278#
279# # Note that the reset above will set self.iteration to 0 again
280# # which is why we should check again
281# if self.iteration > 0:
282# s0 = r.reshape(-1) - r0.reshape(-1)
283# self.s.append(s0)
284#
285# # We use the gradient which is minus the force!
286# y0 = f0.reshape(-1) - f.reshape(-1)
287# self.y.append(y0)
288#
289# rho0 = 1.0 / np.dot(y0, s0)
290# self.rho.append(rho0)
291#
292# if self.iteration > self.memory:
293# self.s.pop(0)
294# self.y.pop(0)
295# self.rho.pop(0)
296#
297# def determine_step(self, dr):
298# f = self.atoms.get_forces()
299#
300# # Unit-vector along the search direction
301# du = dr / np.sqrt(np.dot(dr.reshape(-1), dr.reshape(-1)))
302#
303# # We keep the old step determination before we figure
304# # out what is the best to do.
305# maxstep = self.maxstep * np.sqrt(3 * len(self.atoms))
306#
307# # Finite difference step using temporary point
308# self.atoms.positions += (du * self.dR)
309# # Decide how much to move along the line du
310# Fp1 = np.dot(f.reshape(-1), du.reshape(-1))
311# Fp2 = np.dot(self.atoms.get_forces().reshape(-1), du.reshape(-1))
312# CR = (Fp1 - Fp2) / self.dR
313# #RdR = Fp1*0.1
314# if CR < 0.0:
315# #print "negcurve"
316# RdR = maxstep
317# #if(abs(RdR) > maxstep):
318# # RdR = self.sign(RdR) * maxstep
319# else:
320# Fp = (Fp1 + Fp2) * 0.5
321# RdR = Fp / CR
322# if abs(RdR) > maxstep:
323# RdR = np.sign(RdR) * maxstep
324# else:
325# RdR += self.dR * 0.5
326# return du * RdR