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
2from ase.optimize.optimize import Dynamics
5def subtract_projection(a, b):
6 '''returns new vector that removes vector a's projection vector b. Is
7 also equivalent to the vector rejection.'''
8 aout = a - np.vdot(a, b) / np.vdot(b, b) * b
9 return aout
12def normalize(a):
13 '''Makes a unit vector out of a vector'''
14 return a / np.linalg.norm(a)
17class ContourExploration(Dynamics):
19 def __init__(self, atoms,
20 maxstep=0.5,
21 parallel_drift=0.1,
22 energy_target=None,
23 angle_limit=20,
24 potentiostat_step_scale=None,
25 remove_translation=False,
26 use_frenet_serret=True,
27 initialization_step_scale=1e-2,
28 use_target_shift=True, target_shift_previous_steps=10,
29 use_tangent_curvature=False,
30 rng=np.random,
31 force_consistent=None,
32 trajectory=None, logfile=None,
33 append_trajectory=False, loginterval=1):
34 """Contour Exploration object.
36 Parameters:
38 atoms: Atoms object
39 The Atoms object to operate on. Atomic velocities are required for
40 the method. If the atoms object does not contain velocities,
41 random ones will be applied.
43 maxstep: float
44 Used to set the maximum distance an atom can move per
45 iteration (default value is 0.5 Å).
47 parallel_drift: float
48 The fraction of the update step that is parallel to the contour but
49 in a random direction. Used to break symmetries.
51 energy_target: float
52 The total system potential energy for that the potentiostat attepts
53 to maintain. (defaults the initial potential energy)
55 angle_limit: float or None
56 Limits the stepsize to a maximum change of direction angle using the
57 curvature. Gives a scale-free means of tuning the stepsize on the
58 fly. Typically less than 30 degrees gives reasonable results but
59 lower angle limits result in higher potentiostatic accuracy. Units
60 of degrees. (default 20°)
62 potentiostat_step_scale: float or None
63 Scales the size of the potentiostat step. The potentiostat step is
64 determined by linear extrapolation from the current potential energy
65 to the target_energy with the current forces. A
66 potentiostat_step_scale > 1.0 overcorrects and < 1.0
67 undercorrects. By default, a simple heuristic is used to selected
68 the valued based on the parallel_drift. (default None)
70 remove_translation: boolean
71 When True, the net momentum is removed at each step. Improves
72 potentiostatic accuracy slightly for bulk systems but should not be
73 used with constraints. (default False)
75 use_frenet_serret: Bool
76 Controls whether or not the Taylor expansion of the Frenet-Serret
77 formulas for curved path extrapolation are used. Required for using
78 angle_limit based step scalling. (default True)
80 initialization_step_scale: float
81 Controls the scale of the initial step as a multiple of maxstep.
82 (default 1e-2)
84 use_target_shift: boolean
85 Enables shifting of the potentiostat target to compensate for
86 systematic undercorrection or overcorrection by the potentiostat.
87 Uses the average of the *target_shift_previous_steps* to prevent
88 coupled occilations. (default True)
90 target_shift_previous_steps: int
91 The number of pevious steps to average when using use_target_shift.
92 (default 10)
94 use_tangent_curvature: boolean
95 Use the velocity unit tangent rather than the contour normals from
96 forces to compute the curvature. Usually not as accurate.
97 (default False)
99 rng: a random number generator
100 Lets users control the random number generator for the
101 parallel_drift vector. (default numpy.random)
103 force_consistent: boolean
104 (default None)
106 trajectory: Trajectory object or str (optional)
107 Attach trajectory object. If *trajectory* is a string a
108 Trajectory will be constructed. Default: None.
110 logfile: file object or str (optional)
111 If *logfile* is a string, a file with that name will be opened.
112 Use '-' for stdout. Default: None.
114 loginterval: int (optional)
115 Only write a log line for every *loginterval* time steps.
116 Default: 1
118 append_trajectory: boolean
119 Defaults to False, which causes the trajectory file to be
120 overwriten each time the dynamics is restarted from scratch.
121 If True, the new structures are appended to the trajectory
122 file instead.
123 """
125 if potentiostat_step_scale is None:
126 # a heuristic guess since most systems will overshoot when there is
127 # drift
128 self.potentiostat_step_scale = 1.1 + 0.6 * parallel_drift
129 else:
130 self.potentiostat_step_scale = potentiostat_step_scale
132 self.rng = rng
133 self.remove_translation = remove_translation
134 self.use_frenet_serret = use_frenet_serret
135 self.force_consistent = force_consistent
136 self.use_tangent_curvature = use_tangent_curvature
137 self.initialization_step_scale = initialization_step_scale
138 self.maxstep = maxstep
139 self.angle_limit = angle_limit
140 self.parallel_drift = parallel_drift
141 self.use_target_shift = use_target_shift
143 # These will be populated once self.step() is called, but could be set
144 # after instantiating with ce = ContourExploration(...) like so:
145 # ce.Nold = Nold
146 # ce.r_old = atoms_old.get_positions()
147 # ce.Told = Told
148 # to resume a previous contour trajectory.
150 self.T = None
151 self.Told = None
152 self.N = None
153 self.Nold = None
154 self.r_old = None
155 self.r = None
157 if energy_target is None:
158 self.energy_target = atoms.get_potential_energy(
159 force_consistent=self.force_consistent)
160 else:
161 self.energy_target = energy_target
163 # Initizing the previous steps at the target energy slows
164 # target_shifting untill the system has had
165 # 'target_shift_previous_steps' steps to equilibrate and should prevent
166 # occilations. These need to be initialized before the initialize_old
167 # step to prevent a crash
168 self.previous_energies = np.full(target_shift_previous_steps,
169 self.energy_target)
171 # these first two are purely for logging,
172 # auto scaling will still occur
173 # and curvature will still be found if use_frenet_serret == True
174 self.step_size = 0.0
175 self.curvature = 0
177 # loginterval exists for the MolecularDynamics class but not for
178 # the more general Dynamics class
179 Dynamics.__init__(self, atoms,
180 logfile, trajectory, # loginterval,
181 append_trajectory=append_trajectory,
182 )
184 # we need velocities or NaNs will be produced,
185 # if none are provided we make random ones
186 velocities = self.atoms.get_velocities()
187 if np.linalg.norm(velocities) < 1e-6:
188 # we have to pass dimension since atoms are not yet stored
189 atoms.set_velocities(self.rand_vect())
191 # Required stuff for Dynamics
192 def todict(self):
193 return {'type': 'contour-exploration',
194 'dyn-type': self.__class__.__name__,
195 'stepsize': self.step_size}
197 def run(self, steps=50):
198 """ Call Dynamics.run and adjust max_steps """
199 self.max_steps = steps + self.nsteps
200 return Dynamics.run(self)
202 def log(self):
203 if self.logfile is not None:
204 # name = self.__class__.__name__
205 if self.nsteps == 0:
206 args = (
207 "Step",
208 "Energy_Target",
209 "Energy",
210 "Curvature",
211 "Step_Size",
212 "Energy_Deviation_per_atom")
213 msg = "# %4s %15s %15s %12s %12s %15s\n" % args
214 self.logfile.write(msg)
215 e = self.atoms.get_potential_energy(
216 force_consistent=self.force_consistent)
217 dev_per_atom = (e - self.energy_target) / len(self.atoms)
218 args = (
219 self.nsteps,
220 self.energy_target,
221 e,
222 self.curvature,
223 self.step_size,
224 dev_per_atom)
225 msg = "%6d %15.6f %15.6f %12.6f %12.6f %24.9f\n" % args
226 self.logfile.write(msg)
228 self.logfile.flush()
230 def rand_vect(self):
231 '''Returns a random (Natoms,3) vector'''
232 vect = self.rng.rand(len(self.atoms), 3) - 0.5
233 return vect
235 def create_drift_unit_vector(self, N, T):
236 '''Creates a random drift unit vector with no projection on N or T and
237 with out a net translation so systems don't wander'''
238 drift = self.rand_vect()
239 drift = subtract_projection(drift, N)
240 drift = subtract_projection(drift, T)
241 # removes net translation, so systems don't wander
242 drift = drift - drift.sum(axis=0) / len(self.atoms)
243 D = normalize(drift)
244 return D
246 def compute_step_contributions(self, potentiostat_step_size):
247 '''Computes the orthogonal component sizes of the step so that the net
248 step obeys the smaller of step_size or maxstep.'''
249 if abs(potentiostat_step_size) < self.step_size:
250 delta_s_perpendicular = potentiostat_step_size
251 contour_step_size = np.sqrt(
252 self.step_size**2 - potentiostat_step_size**2)
253 delta_s_parallel = np.sqrt(
254 1 - self.parallel_drift**2) * contour_step_size
255 delta_s_drift = contour_step_size * self.parallel_drift
257 else:
258 # in this case all priority goes to potentiostat terms
259 delta_s_parallel = 0.0
260 delta_s_drift = 0.0
261 delta_s_perpendicular = np.sign(
262 potentiostat_step_size) * self.step_size
264 return delta_s_perpendicular, delta_s_parallel, delta_s_drift
266 def _compute_update_without_fs(self, potentiostat_step_size, scale=1.0):
267 '''Only uses the forces to compute an orthogonal update vector'''
269 # Without the use of curvature there is no way to estimate the
270 # limiting step size
271 self.step_size = self.maxstep * scale
273 delta_s_perpendicular, delta_s_parallel, delta_s_drift = \
274 self.compute_step_contributions(
275 potentiostat_step_size)
277 dr_perpendicular = self.N * delta_s_perpendicular
278 dr_parallel = delta_s_parallel * self.T
280 D = self.create_drift_unit_vector(self.N, self.T)
281 dr_drift = D * delta_s_drift
283 dr = dr_parallel + dr_drift + dr_perpendicular
284 dr = self.step_size * normalize(dr)
285 return dr
287 def _compute_update_with_fs(self, potentiostat_step_size):
288 '''Uses the Frenet–Serret formulas to perform curvature based
289 extrapolation to compute the update vector'''
290 # this should keep the dr clear of the constraints
291 # by using the actual change, not a velocity vector
292 delta_r = self.r - self.rold
293 delta_s = np.linalg.norm(delta_r)
294 # approximation of delta_s we use this incase an adaptive step_size
295 # algo get used
297 delta_T = self.T - self.Told
298 delta_N = self.N - self.Nold
299 dTds = delta_T / delta_s
300 dNds = delta_N / delta_s
301 if self.use_tangent_curvature:
302 curvature = np.linalg.norm(dTds)
303 # on a perfect trajectory, the normal can be computed this way,
304 # But the normal should always be tied to forces
305 # N = dTds / curvature
306 else:
307 # normals are better since they are fixed to the reality of
308 # forces. I see smaller forces and energy errors in bulk systems
309 # using the normals for curvature
310 curvature = np.linalg.norm(dNds)
311 self.curvature = curvature
313 if self.angle_limit is not None:
314 phi = np.pi / 180 * self.angle_limit
315 self.step_size = np.sqrt(2 - 2 * np.cos(phi)) / curvature
316 self.step_size = min(self.step_size, self.maxstep)
318 # now we can compute a safe step
319 delta_s_perpendicular, delta_s_parallel, delta_s_drift = \
320 self.compute_step_contributions(
321 potentiostat_step_size)
323 N_guess = self.N + dNds * delta_s_parallel
324 T_guess = self.T + dTds * delta_s_parallel
325 # the extrapolation is good at keeping N_guess and T_guess
326 # orthogonal but not normalized:
327 N_guess = normalize(N_guess)
328 T_guess = normalize(T_guess)
330 dr_perpendicular = delta_s_perpendicular * (N_guess)
332 dr_parallel = delta_s_parallel * self.T * \
333 (1 - (delta_s_parallel * curvature)**2 / 6.0) \
334 + self.N * (curvature / 2.0) * delta_s_parallel**2
336 D = self.create_drift_unit_vector(N_guess, T_guess)
337 dr_drift = D * delta_s_drift
339 # combine the components
340 dr = dr_perpendicular + dr_parallel + dr_drift
341 dr = self.step_size * normalize(dr)
342 # because we guess our orthonormalization directions,
343 # we renormalize to ensure a correct step size
344 return dr
346 def update_previous_energies(self, energy):
347 '''Updates the energy history in self.previous_energies to include the
348 current energy.'''
349 # np.roll shifts the values to keep nice sequential ordering.
350 self.previous_energies = np.roll(self.previous_energies, 1)
351 self.previous_energies[0] = energy
353 def compute_potentiostat_step_size(self, forces, energy):
354 '''Computes the potentiostat step size by linear extrapolation of the
355 potential energy using the forces. The step size can be positive or
356 negative depending on whether or not the energy is too high or too low.
357 '''
358 if self.use_target_shift:
359 target_shift = self.energy_target - np.mean(self.previous_energies)
360 else:
361 target_shift = 0.0
363 # deltaU is the potential error that will be corrected for
364 deltaU = energy - (self.energy_target + target_shift)
366 f_norm = np.linalg.norm(forces)
367 # can be positive or negative
368 potentiostat_step_size = (deltaU / f_norm) * \
369 self.potentiostat_step_scale
370 return potentiostat_step_size
372 def step(self, f=None):
373 atoms = self.atoms
375 if f is None:
376 f = atoms.get_forces()
378 # get the velocity vector and old kinetic energy for momentum rescaling
379 velocities = atoms.get_velocities()
380 KEold = atoms.get_kinetic_energy()
382 energy = atoms.get_potential_energy(
383 force_consistent=self.force_consistent)
384 self.update_previous_energies(energy)
385 potentiostat_step_size = self.compute_potentiostat_step_size(f, energy)
387 self.N = normalize(f)
388 self.r = atoms.get_positions()
389 # remove velocity projection on forces
390 v_parallel = subtract_projection(velocities, self.N)
391 self.T = normalize(v_parallel)
393 if self.use_frenet_serret:
394 if self.Nold is not None and self.Told is not None:
395 dr = self._compute_update_with_fs(potentiostat_step_size)
396 else:
397 # we must have the old positions and vectors for an FS step
398 # if we don't, we can only do a small step
399 dr = self._compute_update_without_fs(
400 potentiostat_step_size,
401 scale=self.initialization_step_scale)
402 else: # of course we can run less accuratly without FS.
403 dr = self._compute_update_without_fs(potentiostat_step_size)
405 # now that dr is done, we check if there is translation
406 if self.remove_translation:
407 net_motion = dr.sum(axis=0) / len(atoms)
408 # print(net_motion)
409 dr = dr - net_motion
410 dr_unit = dr / np.linalg.norm(dr)
411 dr = dr_unit * self.step_size
413 # save old positions before update
414 self.Nold = self.N
415 self.rold = self.r
416 self.Told = self.T
418 # if we have constraints then this will do the first part of the
419 # RATTLE algorithm:
420 # If we can avoid using momenta, this will be simpler.
421 masses = atoms.get_masses()[:, np.newaxis]
422 atoms.set_positions(self.r + dr)
423 new_momenta = (atoms.get_positions() - self.r) * masses # / self.dt
425 # We need to store the momenta on the atoms before calculating
426 # the forces, as in a parallel Asap calculation atoms may
427 # migrate during force calculations, and the momenta need to
428 # migrate along with the atoms.
429 atoms.set_momenta(new_momenta, apply_constraint=False)
431 # Now we get the new forces!
432 f = atoms.get_forces(md=True)
434 # I don't really know if removing md=True from above will break
435 # compatibility with RATTLE, leaving it alone for now.
436 f_constrained = atoms.get_forces()
437 # but this projection needs the forces to be consistent with the
438 # constraints. We have to set the new velocities perpendicular so they
439 # get logged properly in the trajectory files.
440 vnew = subtract_projection(atoms.get_velocities(), f_constrained)
441 # using the md = True forces like this:
442 # vnew = subtract_projection(atoms.get_velocities(), f)
443 # will not work with constraints
444 atoms.set_velocities(vnew)
446 # rescaling momentum to maintain constant kinetic energy.
447 KEnew = atoms.get_kinetic_energy()
448 Ms = np.sqrt(KEold / KEnew) # Ms = Momentum_scale
449 atoms.set_momenta(Ms * atoms.get_momenta())
451 # Normally this would be the second part of RATTLE
452 # will be done here like this:
453 # atoms.set_momenta(atoms.get_momenta() + 0.5 * self.dt * f)
454 return f