Coverage for /builds/alexhroom/ase/ase/optimize/bfgs.py: 78.16%
87 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +0000
1import warnings
2from typing import IO, Optional, Union
4import numpy as np
5from numpy.linalg import eigh
7from ase import Atoms
8from ase.parallel import world
9from ase.optimize.optimize import Optimizer, UnitCellFilter
12class BFGS(Optimizer):
13 # default parameters
14 defaults = {**Optimizer.defaults, 'alpha': 70.0}
16 def __init__(
17 self,
18 atoms: Atoms,
19 restart: Optional[str] = None,
20 logfile: Optional[Union[IO, str]] = '-',
21 trajectory: Optional[str] = None,
22 append_trajectory: bool = False,
23 maxstep: Optional[float] = None,
24 master: Optional[bool] = None,
25 alpha: Optional[float] = None,
26 comm=world
27 ):
28 """BFGS optimizer.
30 Parameters:
32 atoms: Atoms object
33 The Atoms object to relax.
35 restart: string
36 JSON file used to store hessian matrix. If set, file with
37 such a name will be searched and hessian matrix stored will
38 be used, if the file exists.
40 trajectory: string
41 Trajectory file used to store optimisation path.
43 logfile: file object or str
44 If *logfile* is a string, a file with that name will be opened.
45 Use '-' for stdout.
47 maxstep: float
48 Used to set the maximum distance an atom can move per
49 iteration (default value is 0.2 Å).
51 master: boolean
52 Defaults to None, which causes only rank 0 to save files. If
53 set to true, this rank will save files.
55 comm: Communicator object
56 Defaults to world. Communicator to handle parallel file reading
57 and writing, set by ase.parallel.world.
59 alpha: float
60 Initial guess for the Hessian (curvature of energy surface). A
61 conservative value of 70.0 is the default, but number of needed
62 steps to converge might be less if a lower value is used. However,
63 a lower value also means risk of instability.
64 """
65 if maxstep is None:
66 self.maxstep = self.defaults['maxstep']
67 else:
68 self.maxstep = maxstep
70 if self.maxstep > 1.0:
71 warnings.warn('You are using a *very* large value for '
72 'the maximum step size: %.1f Å' % self.maxstep)
74 self.alpha = alpha
75 if self.alpha is None:
76 self.alpha = self.defaults['alpha']
77 Optimizer.__init__(self, atoms=atoms, restart=restart,
78 logfile=logfile, trajectory=trajectory,
79 master=master, append_trajectory=append_trajectory,
80 comm=comm)
82 def initialize(self):
83 # initial hessian
84 self.H0 = np.eye(3 * len(self.optimizable)) * self.alpha
86 self.H = None
87 self.pos0 = None
88 self.forces0 = None
90 def read(self):
91 file = self.load()
92 if len(file) == 5:
93 (self.H, self.pos0, self.forces0, self.maxstep,
94 self.atoms.orig_cell) = file
95 else:
96 self.H, self.pos0, self.forces0, self.maxstep = file
98 def step(self, forces=None):
99 optimizable = self.optimizable
101 if forces is None:
102 forces = optimizable.get_forces()
104 pos = optimizable.get_positions()
105 dpos, steplengths = self.prepare_step(pos, forces)
106 dpos = self.determine_step(dpos, steplengths)
107 optimizable.set_positions(pos + dpos)
108 if isinstance(self.atoms, UnitCellFilter):
109 self.dump((self.H, self.pos0, self.forces0, self.maxstep,
110 self.atoms.orig_cell))
111 else:
112 self.dump((self.H, self.pos0, self.forces0, self.maxstep))
114 def prepare_step(self, pos, forces):
115 forces = forces.reshape(-1)
116 self.update(pos.flat, forces, self.pos0, self.forces0)
117 omega, V = eigh(self.H)
119 # FUTURE: Log this properly
120 # # check for negative eigenvalues of the hessian
121 # if any(omega < 0):
122 # n_negative = len(omega[omega < 0])
123 # msg = '\n** BFGS Hessian has {} negative eigenvalues.'.format(
124 # n_negative
125 # )
126 # print(msg, flush=True)
127 # if self.logfile is not None:
128 # self.logfile.write(msg)
129 # self.logfile.flush()
131 dpos = np.dot(V, np.dot(forces, V) / np.fabs(omega)).reshape((-1, 3))
132 steplengths = (dpos**2).sum(1)**0.5
133 self.pos0 = pos.flat.copy()
134 self.forces0 = forces.copy()
135 return dpos, steplengths
137 def determine_step(self, dpos, steplengths):
138 """Determine step to take according to maxstep
140 Normalize all steps as the largest step. This way
141 we still move along the direction.
142 """
143 maxsteplength = np.max(steplengths)
144 if maxsteplength >= self.maxstep:
145 scale = self.maxstep / maxsteplength
146 # FUTURE: Log this properly
147 # msg = '\n** scale step by {:.3f} to be shorter than {}'.format(
148 # scale, self.maxstep
149 # )
150 # print(msg, flush=True)
152 dpos *= scale
153 return dpos
155 def update(self, pos, forces, pos0, forces0):
156 if self.H is None:
157 self.H = self.H0
158 return
159 dpos = pos - pos0
161 if np.abs(dpos).max() < 1e-7:
162 # Same configuration again (maybe a restart):
163 return
165 dforces = forces - forces0
166 a = np.dot(dpos, dforces)
167 dg = np.dot(self.H, dpos)
168 b = np.dot(dpos, dg)
169 self.H -= np.outer(dforces, dforces) / a + np.outer(dg, dg) / b
171 def replay_trajectory(self, traj):
172 """Initialize hessian from old trajectory."""
173 if isinstance(traj, str):
174 from ase.io.trajectory import Trajectory
175 traj = Trajectory(traj, 'r')
176 self.H = None
177 atoms = traj[0]
178 pos0 = atoms.get_positions().ravel()
179 forces0 = atoms.get_forces().ravel()
180 for atoms in traj:
181 pos = atoms.get_positions().ravel()
182 forces = atoms.get_forces().ravel()
183 self.update(pos, forces, pos0, forces0)
184 pos0 = pos
185 forces0 = forces
187 self.pos0 = pos0
188 self.forces0 = forces0
191class oldBFGS(BFGS):
192 def determine_step(self, dpos, steplengths):
193 """Old BFGS behaviour for scaling step lengths
195 This keeps the behaviour of truncating individual steps. Some might
196 depend of this as some absurd kind of stimulated annealing to find the
197 global minimum.
198 """
199 dpos /= np.maximum(steplengths / self.maxstep, 1.0).reshape(-1, 1)
200 return dpos