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

122 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3from typing import IO, Optional, Union 

4 

5import numpy as np 

6 

7from ase import Atoms 

8from ase.optimize.optimize import Optimizer 

9from ase.utils.linesearch import LineSearch 

10 

11 

12class LBFGS(Optimizer): 

13 """Limited memory BFGS optimizer. 

14 

15 A limited memory version of the bfgs algorithm. Unlike the bfgs algorithm 

16 used in bfgs.py, the inverse of Hessian matrix is updated. The inverse 

17 Hessian is represented only as a diagonal matrix to save memory 

18 

19 """ 

20 

21 def __init__( 

22 self, 

23 atoms: Atoms, 

24 restart: Optional[str] = None, 

25 logfile: Union[IO, str] = '-', 

26 trajectory: Optional[str] = None, 

27 maxstep: Optional[float] = None, 

28 memory: int = 100, 

29 damping: float = 1.0, 

30 alpha: float = 70.0, 

31 use_line_search: bool = False, 

32 **kwargs, 

33 ): 

34 """ 

35 

36 Parameters 

37 ---------- 

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

39 The Atoms object to relax. 

40 

41 restart: str 

42 JSON file used to store vectors for updating the inverse of 

43 Hessian matrix. If set, file with such a name will be searched 

44 and information stored will be used, if the file exists. 

45 

46 logfile: file object or str 

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

48 Use '-' for stdout. 

49 

50 trajectory: string 

51 Trajectory file used to store optimisation path. 

52 

53 maxstep: float 

54 How far is a single atom allowed to move. This is useful for DFT 

55 calculations where wavefunctions can be reused if steps are small. 

56 Default is 0.2 Angstrom. 

57 

58 memory: int 

59 Number of steps to be stored. Default value is 100. Three numpy 

60 arrays of this length containing floats are stored. 

61 

62 damping: float 

63 The calculated step is multiplied with this number before added to 

64 the positions. 

65 

66 alpha: float 

67 Initial guess for the Hessian (curvature of energy surface). A 

68 conservative value of 70.0 is the default, but number of needed 

69 steps to converge might be less if a lower value is used. However, 

70 a lower value also means risk of instability. 

71 

72 kwargs : dict, optional 

73 Extra arguments passed to 

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

75 

76 """ 

77 Optimizer.__init__(self, atoms, restart, logfile, trajectory, **kwargs) 

78 

79 if maxstep is not None: 

80 self.maxstep = maxstep 

81 else: 

82 self.maxstep = self.defaults['maxstep'] 

83 

84 if self.maxstep > 1.0: 

85 raise ValueError('You are using a much too large value for ' + 

86 'the maximum step size: %.1f Angstrom' % 

87 self.maxstep) 

88 

89 self.memory = memory 

90 # Initial approximation of inverse Hessian 1./70. is to emulate the 

91 # behaviour of BFGS. Note that this is never changed! 

92 self.H0 = 1. / alpha 

93 self.damping = damping 

94 self.use_line_search = use_line_search 

95 self.p = None 

96 self.function_calls = 0 

97 self.force_calls = 0 

98 

99 def initialize(self): 

100 """Initialize everything so no checks have to be done in step""" 

101 self.iteration = 0 

102 self.s = [] 

103 self.y = [] 

104 # Store also rho, to avoid calculating the dot product again and 

105 # again. 

106 self.rho = [] 

107 

108 self.r0 = None 

109 self.f0 = None 

110 self.e0 = None 

111 self.task = 'START' 

112 self.load_restart = False 

113 

114 def read(self): 

115 """Load saved arrays to reconstruct the Hessian""" 

116 self.iteration, self.s, self.y, self.rho, \ 

117 self.r0, self.f0, self.e0, self.task = self.load() 

118 self.load_restart = True 

119 

120 def step(self, forces=None): 

121 """Take a single step 

122 

123 Use the given forces, update the history and calculate the next step -- 

124 then take it""" 

125 

126 if forces is None: 

127 forces = self.optimizable.get_gradient().reshape(-1, 3) 

128 

129 pos = self.optimizable.get_x().reshape(-1, 3) 

130 

131 self.update(pos, forces, self.r0, self.f0) 

132 

133 s = self.s 

134 y = self.y 

135 rho = self.rho 

136 H0 = self.H0 

137 

138 loopmax = np.min([self.memory, self.iteration]) 

139 a = np.empty((loopmax,), dtype=np.float64) 

140 

141 # ## The algorithm itself: 

142 q = -forces.reshape(-1) 

143 for i in range(loopmax - 1, -1, -1): 

144 a[i] = rho[i] * np.dot(s[i], q) 

145 q -= a[i] * y[i] 

146 z = H0 * q 

147 

148 for i in range(loopmax): 

149 b = rho[i] * np.dot(y[i], z) 

150 z += s[i] * (a[i] - b) 

151 

152 self.p = - z.reshape((-1, 3)) 

153 # ## 

154 

155 g = -forces 

156 if self.use_line_search is True: 

157 e = self.func(pos) 

158 self.line_search(pos, g, e) 

159 dr = (self.alpha_k * self.p).reshape(-1, 3) 

160 else: 

161 self.force_calls += 1 

162 self.function_calls += 1 

163 dr = self.determine_step(self.p) * self.damping 

164 self.optimizable.set_x((pos + dr).ravel()) 

165 

166 self.iteration += 1 

167 self.r0 = pos 

168 self.f0 = -g 

169 self.dump((self.iteration, self.s, self.y, 

170 self.rho, self.r0, self.f0, self.e0, self.task)) 

171 

172 def determine_step(self, dr): 

173 """Determine step to take according to maxstep 

174 

175 Normalize all steps as the largest step. This way 

176 we still move along the eigendirection. 

177 """ 

178 steplengths = (dr**2).sum(1)**0.5 

179 longest_step = np.max(steplengths) 

180 if longest_step >= self.maxstep: 

181 dr *= self.maxstep / longest_step 

182 

183 return dr 

184 

185 def update(self, pos, forces, r0, f0): 

186 """Update everything that is kept in memory 

187 

188 This function is mostly here to allow for replay_trajectory. 

189 """ 

190 if self.iteration > 0: 

191 s0 = pos.reshape(-1) - r0.reshape(-1) 

192 self.s.append(s0) 

193 

194 # We use the gradient which is minus the force! 

195 y0 = f0.reshape(-1) - forces.reshape(-1) 

196 self.y.append(y0) 

197 

198 rho0 = 1.0 / np.dot(y0, s0) 

199 self.rho.append(rho0) 

200 

201 if self.iteration > self.memory: 

202 self.s.pop(0) 

203 self.y.pop(0) 

204 self.rho.pop(0) 

205 

206 def replay_trajectory(self, traj): 

207 """Initialize history from old trajectory.""" 

208 if isinstance(traj, str): 

209 from ase.io.trajectory import Trajectory 

210 traj = Trajectory(traj, 'r') 

211 r0 = None 

212 f0 = None 

213 # The last element is not added, as we get that for free when taking 

214 # the first qn-step after the replay 

215 for i in range(len(traj) - 1): 

216 pos = traj[i].get_positions() 

217 forces = traj[i].get_forces() 

218 self.update(pos, forces, r0, f0) 

219 r0 = pos.copy() 

220 f0 = forces.copy() 

221 self.iteration += 1 

222 self.r0 = r0 

223 self.f0 = f0 

224 

225 def func(self, x): 

226 """Objective function for use of the optimizers""" 

227 self.optimizable.set_x(x) 

228 self.function_calls += 1 

229 return self.optimizable.get_value() 

230 

231 def fprime(self, x): 

232 """Gradient of the objective function for use of the optimizers""" 

233 self.optimizable.set_x(x) 

234 self.force_calls += 1 

235 # Remember that forces are minus the gradient! 

236 return -self.optimizable.get_gradient() 

237 

238 def line_search(self, r, g, e): 

239 self.p = self.p.ravel() 

240 p_size = np.sqrt((self.p**2).sum()) 

241 if p_size <= np.sqrt(self.optimizable.ndofs() / 3 * 1e-10): 

242 self.p /= (p_size / np.sqrt(self.optimizable.ndofs() / 3 * 1e-10)) 

243 g = g.ravel() 

244 r = r.ravel() 

245 ls = LineSearch() 

246 self.alpha_k, e, self.e0, self.no_update = \ 

247 ls._line_search(self.func, self.fprime, r, self.p, g, e, self.e0, 

248 maxstep=self.maxstep, c1=.23, 

249 c2=.46, stpmax=50.) 

250 if self.alpha_k is None: 

251 raise RuntimeError('LineSearch failed!') 

252 

253 

254class LBFGSLineSearch(LBFGS): 

255 """This optimizer uses the LBFGS algorithm, but does a line search that 

256 fulfills the Wolff conditions. 

257 """ 

258 

259 def __init__(self, *args, **kwargs): 

260 kwargs['use_line_search'] = True 

261 LBFGS.__init__(self, *args, **kwargs) 

262 

263# """Modified version of LBFGS. 

264# 

265# This optimizer uses the LBFGS algorithm, but does a line search for the 

266# minimum along the search direction. This is done by issuing an additional 

267# force call for each step, thus doubling the number of calculations. 

268# 

269# Additionally the Hessian is reset if the new guess is not sufficiently 

270# better than the old one. 

271# """ 

272# def __init__(self, *args, **kwargs): 

273# self.dR = kwargs.pop('dR', 0.1) 

274# LBFGS.__init__(self, *args, **kwargs) 

275# 

276# def update(self, r, f, r0, f0): 

277# """Update everything that is kept in memory 

278# 

279# This function is mostly here to allow for replay_trajectory. 

280# """ 

281# if self.iteration > 0: 

282# a1 = abs(np.dot(f.reshape(-1), f0.reshape(-1))) 

283# a2 = np.dot(f0.reshape(-1), f0.reshape(-1)) 

284# if not (a1 <= 0.5 * a2 and a2 != 0): 

285# # Reset optimization 

286# self.initialize() 

287# 

288# # Note that the reset above will set self.iteration to 0 again 

289# # which is why we should check again 

290# if self.iteration > 0: 

291# s0 = r.reshape(-1) - r0.reshape(-1) 

292# self.s.append(s0) 

293# 

294# # We use the gradient which is minus the force! 

295# y0 = f0.reshape(-1) - f.reshape(-1) 

296# self.y.append(y0) 

297# 

298# rho0 = 1.0 / np.dot(y0, s0) 

299# self.rho.append(rho0) 

300# 

301# if self.iteration > self.memory: 

302# self.s.pop(0) 

303# self.y.pop(0) 

304# self.rho.pop(0) 

305# 

306# def determine_step(self, dr): 

307# f = self.atoms.get_forces() 

308# 

309# # Unit-vector along the search direction 

310# du = dr / np.sqrt(np.dot(dr.reshape(-1), dr.reshape(-1))) 

311# 

312# # We keep the old step determination before we figure 

313# # out what is the best to do. 

314# maxstep = self.maxstep * np.sqrt(3 * len(self.atoms)) 

315# 

316# # Finite difference step using temporary point 

317# self.atoms.positions += (du * self.dR) 

318# # Decide how much to move along the line du 

319# Fp1 = np.dot(f.reshape(-1), du.reshape(-1)) 

320# Fp2 = np.dot(self.atoms.get_forces().reshape(-1), du.reshape(-1)) 

321# CR = (Fp1 - Fp2) / self.dR 

322# #RdR = Fp1*0.1 

323# if CR < 0.0: 

324# #print "negcurve" 

325# RdR = maxstep 

326# #if(abs(RdR) > maxstep): 

327# # RdR = self.sign(RdR) * maxstep 

328# else: 

329# Fp = (Fp1 + Fp2) * 0.5 

330# RdR = Fp / CR 

331# if abs(RdR) > maxstep: 

332# RdR = np.sign(RdR) * maxstep 

333# else: 

334# RdR += self.dR * 0.5 

335# return du * RdR