Hide keyboard shortcuts

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 sys 

2import threading 

3import warnings 

4from abc import ABC, abstractmethod 

5import time 

6 

7import numpy as np 

8 

9from scipy.interpolate import CubicSpline 

10from scipy.integrate import cumtrapz 

11 

12import ase.parallel 

13from ase.build import minimize_rotation_and_translation 

14from ase.calculators.calculator import Calculator 

15from ase.calculators.singlepoint import SinglePointCalculator 

16from ase.optimize import MDMin 

17from ase.optimize.optimize import Optimizer 

18from ase.optimize.sciopt import OptimizerConvergenceError 

19from ase.geometry import find_mic 

20from ase.utils import lazyproperty, deprecated 

21from ase.utils.forcecurve import fit_images 

22from ase.optimize.precon import Precon, PreconImages 

23from ase.optimize.ode import ode12r 

24 

25 

26class Spring: 

27 def __init__(self, atoms1, atoms2, energy1, energy2, k): 

28 self.atoms1 = atoms1 

29 self.atoms2 = atoms2 

30 self.energy1 = energy1 

31 self.energy2 = energy2 

32 self.k = k 

33 

34 def _find_mic(self): 

35 pos1 = self.atoms1.get_positions() 

36 pos2 = self.atoms2.get_positions() 

37 # XXX If we want variable cells we will need to edit this. 

38 mic, _ = find_mic(pos2 - pos1, self.atoms1.cell, self.atoms1.pbc) 

39 return mic 

40 

41 @lazyproperty 

42 def t(self): 

43 return self._find_mic() 

44 

45 @lazyproperty 

46 def nt(self): 

47 return np.linalg.norm(self.t) 

48 

49 

50class NEBState: 

51 def __init__(self, neb, images, energies): 

52 self.neb = neb 

53 self.images = images 

54 self.energies = energies 

55 

56 def spring(self, i): 

57 return Spring(self.images[i], self.images[i + 1], 

58 self.energies[i], self.energies[i + 1], 

59 self.neb.k[i]) 

60 

61 @lazyproperty 

62 def imax(self): 

63 return 1 + np.argsort(self.energies[1:-1])[-1] 

64 

65 @property 

66 def emax(self): 

67 return self.energies[self.imax] 

68 

69 @lazyproperty 

70 def eqlength(self): 

71 images = self.images 

72 beeline = (images[self.neb.nimages - 1].get_positions() - 

73 images[0].get_positions()) 

74 beelinelength = np.linalg.norm(beeline) 

75 return beelinelength / (self.neb.nimages - 1) 

76 

77 @lazyproperty 

78 def nimages(self): 

79 return len(self.images) 

80 

81 @property 

82 def precon(self): 

83 return self.neb.precon 

84 

85 

86class NEBMethod(ABC): 

87 def __init__(self, neb): 

88 self.neb = neb 

89 

90 @abstractmethod 

91 def get_tangent(self, state, spring1, spring2, i): 

92 ... 

93 

94 @abstractmethod 

95 def add_image_force(self, state, tangential_force, tangent, imgforce, 

96 spring1, spring2, i): 

97 ... 

98 

99 def adjust_positions(self, positions): 

100 return positions 

101 

102 

103class ImprovedTangentMethod(NEBMethod): 

104 """ 

105 Tangent estimates are improved according to Eqs. 8-11 in paper I. 

106 Tangents are weighted at extrema to ensure smooth transitions between 

107 the positive and negative tangents. 

108 """ 

109 

110 def get_tangent(self, state, spring1, spring2, i): 

111 energies = state.energies 

112 if energies[i + 1] > energies[i] > energies[i - 1]: 

113 tangent = spring2.t.copy() 

114 elif energies[i + 1] < energies[i] < energies[i - 1]: 

115 tangent = spring1.t.copy() 

116 else: 

117 deltavmax = max(abs(energies[i + 1] - energies[i]), 

118 abs(energies[i - 1] - energies[i])) 

119 deltavmin = min(abs(energies[i + 1] - energies[i]), 

120 abs(energies[i - 1] - energies[i])) 

121 if energies[i + 1] > energies[i - 1]: 

122 tangent = spring2.t * deltavmax + spring1.t * deltavmin 

123 else: 

124 tangent = spring2.t * deltavmin + spring1.t * deltavmax 

125 # Normalize the tangent vector 

126 tangent /= np.linalg.norm(tangent) 

127 return tangent 

128 

129 def add_image_force(self, state, tangential_force, tangent, imgforce, 

130 spring1, spring2, i): 

131 imgforce -= tangential_force * tangent 

132 # Improved parallel spring force (formula 12 of paper I) 

133 imgforce += (spring2.nt * spring2.k - spring1.nt * spring1.k) * tangent 

134 

135 

136class ASENEBMethod(NEBMethod): 

137 """ 

138 Standard NEB implementation in ASE. The tangent of each image is 

139 estimated from the spring closest to the saddle point in each 

140 spring pair. 

141 """ 

142 

143 def get_tangent(self, state, spring1, spring2, i): 

144 imax = self.neb.imax 

145 if i < imax: 

146 tangent = spring2.t 

147 elif i > imax: 

148 tangent = spring1.t 

149 else: 

150 tangent = spring1.t + spring2.t 

151 return tangent 

152 

153 def add_image_force(self, state, tangential_force, tangent, imgforce, 

154 spring1, spring2, i): 

155 # Magnitude for normalizing. Ensure it is not 0 

156 tangent_mag = np.vdot(tangent, tangent) or 1 

157 factor = tangent / tangent_mag 

158 imgforce -= tangential_force * factor 

159 imgforce -= np.vdot( 

160 spring1.t * spring1.k - 

161 spring2.t * spring2.k, tangent) * factor 

162 

163 

164class FullSpringMethod(NEBMethod): 

165 """ 

166 Elastic band method. The full spring force is included. 

167 """ 

168 

169 def get_tangent(self, state, spring1, spring2, i): 

170 # Tangents are bisections of spring-directions 

171 # (formula C8 of paper III) 

172 tangent = spring1.t / spring1.nt + spring2.t / spring2.nt 

173 tangent /= np.linalg.norm(tangent) 

174 return tangent 

175 

176 def add_image_force(self, state, tangential_force, tangent, imgforce, 

177 spring1, spring2, i): 

178 imgforce -= tangential_force * tangent 

179 energies = state.energies 

180 # Spring forces 

181 # Eqs. C1, C5, C6 and C7 in paper III) 

182 f1 = -(spring1.nt - 

183 state.eqlength) * spring1.t / spring1.nt * spring1.k 

184 f2 = (spring2.nt - state.eqlength) * spring2.t / spring2.nt * spring2.k 

185 if self.neb.climb and abs(i - self.neb.imax) == 1: 

186 deltavmax = max(abs(energies[i + 1] - energies[i]), 

187 abs(energies[i - 1] - energies[i])) 

188 deltavmin = min(abs(energies[i + 1] - energies[i]), 

189 abs(energies[i - 1] - energies[i])) 

190 imgforce += (f1 + f2) * deltavmin / deltavmax 

191 else: 

192 imgforce += f1 + f2 

193 

194 

195class BaseSplineMethod(NEBMethod): 

196 """ 

197 Base class for SplineNEB and String methods 

198 

199 Can optionally be preconditioned, as described in the following article: 

200 

201 S. Makri, C. Ortner and J. R. Kermode, J. Chem. Phys. 

202 150, 094109 (2019) 

203 https://dx.doi.org/10.1063/1.5064465 

204 """ 

205 

206 def __init__(self, neb): 

207 NEBMethod.__init__(self, neb) 

208 

209 def get_tangent(self, state, spring1, spring2, i): 

210 return state.precon.get_tangent(i) 

211 

212 def add_image_force(self, state, tangential_force, tangent, imgforce, 

213 spring1, spring2, i): 

214 # project out tangential component (Eqs 6 and 7 in Paper IV) 

215 imgforce -= tangential_force * tangent 

216 

217 

218class SplineMethod(BaseSplineMethod): 

219 """ 

220 NEB using spline interpolation, plus optional preconditioning 

221 """ 

222 

223 def add_image_force(self, state, tangential_force, tangent, imgforce, 

224 spring1, spring2, i): 

225 super().add_image_force(state, tangential_force, 

226 tangent, imgforce, spring1, spring2, i) 

227 eta = state.precon.get_spring_force(i, spring1.k, spring2.k, tangent) 

228 imgforce += eta 

229 

230 

231class StringMethod(BaseSplineMethod): 

232 """ 

233 String method using spline interpolation, plus optional preconditioning 

234 """ 

235 

236 def adjust_positions(self, positions): 

237 # fit cubic spline to positions, reinterpolate to equispace images 

238 # note this uses the preconditioned distance metric. 

239 fit = self.neb.spline_fit(positions) 

240 new_s = np.linspace(0.0, 1.0, self.neb.nimages) 

241 new_positions = fit.x(new_s[1:-1]).reshape(-1, 3) 

242 return new_positions 

243 

244 

245def get_neb_method(neb, method): 

246 if method == 'eb': 

247 return FullSpringMethod(neb) 

248 elif method == 'aseneb': 

249 return ASENEBMethod(neb) 

250 elif method == 'improvedtangent': 

251 return ImprovedTangentMethod(neb) 

252 elif method == 'spline': 

253 return SplineMethod(neb) 

254 elif method == 'string': 

255 return StringMethod(neb) 

256 else: 

257 raise ValueError(f'Bad method: {method}') 

258 

259 

260class BaseNEB: 

261 def __init__(self, images, k=0.1, climb=False, parallel=False, 

262 remove_rotation_and_translation=False, world=None, 

263 method='aseneb', allow_shared_calculator=False, precon=None): 

264 

265 self.images = images 

266 self.climb = climb 

267 self.parallel = parallel 

268 self.allow_shared_calculator = allow_shared_calculator 

269 

270 for img in images: 

271 if len(img) != self.natoms: 

272 raise ValueError('Images have different numbers of atoms') 

273 if np.any(img.pbc != images[0].pbc): 

274 raise ValueError('Images have different boundary conditions') 

275 if np.any(img.get_atomic_numbers() != 

276 images[0].get_atomic_numbers()): 

277 raise ValueError('Images have atoms in different orders') 

278 # check periodic cell directions 

279 cell_ok = True 

280 for pbc, vc, vc0 in zip(img.pbc, img.cell, images[0].cell): 

281 if pbc and np.any(np.abs(vc - vc0) > 1e-8): 

282 cell_ok = False 

283 if not cell_ok: 

284 raise NotImplementedError( 

285 "Variable cell in periodic directions " 

286 "is not implemented yet for NEB") 

287 

288 self.emax = np.nan 

289 

290 self.remove_rotation_and_translation = remove_rotation_and_translation 

291 

292 if method in ['aseneb', 'eb', 'improvedtangent', 'spline', 'string']: 

293 self.method = method 

294 else: 

295 raise NotImplementedError(method) 

296 

297 if precon is not None and method not in ['spline', 'string']: 

298 raise NotImplementedError(f'no precon implemented: {method}') 

299 self.precon = precon 

300 

301 self.neb_method = get_neb_method(self, method) 

302 if isinstance(k, (float, int)): 

303 k = [k] * (self.nimages - 1) 

304 self.k = list(k) 

305 

306 if world is None: 

307 world = ase.parallel.world 

308 self.world = world 

309 

310 if parallel: 

311 if self.allow_shared_calculator: 

312 raise RuntimeError( 

313 "Cannot use shared calculators in parallel in NEB.") 

314 self.real_forces = None # ndarray of shape (nimages, natom, 3) 

315 self.energies = None # ndarray of shape (nimages,) 

316 self.residuals = None # ndarray of shape (nimages,) 

317 

318 @property 

319 def natoms(self): 

320 return len(self.images[0]) 

321 

322 @property 

323 def nimages(self): 

324 return len(self.images) 

325 

326 @staticmethod 

327 def freeze_results_on_image(atoms: ase.Atoms, 

328 **results_to_include): 

329 atoms.calc = SinglePointCalculator(atoms=atoms, **results_to_include) 

330 

331 def interpolate(self, method='linear', mic=False, apply_constraint=None): 

332 """Interpolate the positions of the interior images between the 

333 initial state (image 0) and final state (image -1). 

334 

335 method: str 

336 Method by which to interpolate: 'linear' or 'idpp'. 

337 linear provides a standard straight-line interpolation, while 

338 idpp uses an image-dependent pair potential. 

339 mic: bool 

340 Use the minimum-image convention when interpolating. 

341 apply_constraint: bool 

342 Controls if the constraints attached to the images 

343 are ignored or applied when setting the interpolated positions. 

344 Default value is None, in this case the resulting constrained 

345 positions (apply_constraint=True) are compared with unconstrained 

346 positions (apply_constraint=False), 

347 if the positions are not the same 

348 the user is required to specify the desired behaviour 

349 by setting up apply_constraint keyword argument to False or True. 

350 """ 

351 if self.remove_rotation_and_translation: 

352 minimize_rotation_and_translation(self.images[0], self.images[-1]) 

353 

354 interpolate(self.images, mic, apply_constraint=apply_constraint) 

355 

356 if method == 'idpp': 

357 idpp_interpolate(images=self, traj=None, log=None, mic=mic) 

358 

359 @deprecated("Please use NEB's interpolate(method='idpp') method or " 

360 "directly call the idpp_interpolate function from ase.neb") 

361 def idpp_interpolate(self, traj='idpp.traj', log='idpp.log', fmax=0.1, 

362 optimizer=MDMin, mic=False, steps=100): 

363 idpp_interpolate(self, traj=traj, log=log, fmax=fmax, 

364 optimizer=optimizer, mic=mic, steps=steps) 

365 

366 def get_positions(self): 

367 positions = np.empty(((self.nimages - 2) * self.natoms, 3)) 

368 n1 = 0 

369 for image in self.images[1:-1]: 

370 n2 = n1 + self.natoms 

371 positions[n1:n2] = image.get_positions() 

372 n1 = n2 

373 return positions 

374 

375 def set_positions(self, positions, adjust_positions=True): 

376 if adjust_positions: 

377 # optional reparameterisation step: some NEB methods need to adjust 

378 # positions e.g. string method does this to equispace the images) 

379 positions = self.neb_method.adjust_positions(positions) 

380 n1 = 0 

381 for image in self.images[1:-1]: 

382 n2 = n1 + self.natoms 

383 image.set_positions(positions[n1:n2]) 

384 n1 = n2 

385 

386 def get_forces(self): 

387 """Evaluate and return the forces.""" 

388 images = self.images 

389 

390 if not self.allow_shared_calculator: 

391 calculators = [image.calc for image in images 

392 if image.calc is not None] 

393 if len(set(calculators)) != len(calculators): 

394 msg = ('One or more NEB images share the same calculator. ' 

395 'Each image must have its own calculator. ' 

396 'You may wish to use the ase.neb.SingleCalculatorNEB ' 

397 'class instead, although using separate calculators ' 

398 'is recommended.') 

399 raise ValueError(msg) 

400 

401 forces = np.empty(((self.nimages - 2), self.natoms, 3)) 

402 energies = np.empty(self.nimages) 

403 

404 if self.remove_rotation_and_translation: 

405 for i in range(1, self.nimages): 

406 minimize_rotation_and_translation(images[i - 1], images[i]) 

407 

408 if self.method != 'aseneb': 

409 energies[0] = images[0].get_potential_energy() 

410 energies[-1] = images[-1].get_potential_energy() 

411 

412 if not self.parallel: 

413 # Do all images - one at a time: 

414 for i in range(1, self.nimages - 1): 

415 forces[i - 1] = images[i].get_forces() 

416 energies[i] = images[i].get_potential_energy() 

417 

418 elif self.world.size == 1: 

419 def run(image, energies, forces): 

420 forces[:] = image.get_forces() 

421 energies[:] = image.get_potential_energy() 

422 

423 threads = [threading.Thread(target=run, 

424 args=(images[i], 

425 energies[i:i + 1], 

426 forces[i - 1:i])) 

427 for i in range(1, self.nimages - 1)] 

428 for thread in threads: 

429 thread.start() 

430 for thread in threads: 

431 thread.join() 

432 else: 

433 # Parallelize over images: 

434 i = self.world.rank * (self.nimages - 2) // self.world.size + 1 

435 try: 

436 forces[i - 1] = images[i].get_forces() 

437 energies[i] = images[i].get_potential_energy() 

438 except Exception: 

439 # Make sure other images also fail: 

440 error = self.world.sum(1.0) 

441 raise 

442 else: 

443 error = self.world.sum(0.0) 

444 if error: 

445 raise RuntimeError('Parallel NEB failed!') 

446 

447 for i in range(1, self.nimages - 1): 

448 root = (i - 1) * self.world.size // (self.nimages - 2) 

449 self.world.broadcast(energies[i:i + 1], root) 

450 self.world.broadcast(forces[i - 1], root) 

451 

452 # if this is the first force call, we need to build the preconditioners 

453 if (self.precon is None or isinstance(self.precon, str) or 

454 isinstance(self.precon, Precon) or 

455 isinstance(self.precon, list)): 

456 self.precon = PreconImages(self.precon, images) 

457 

458 # apply preconditioners to transform forces 

459 # for the default IdentityPrecon this does not change their values 

460 precon_forces = self.precon.apply(forces, index=slice(1, -1)) 

461 

462 # Save for later use in iterimages: 

463 self.energies = energies 

464 self.real_forces = np.zeros((self.nimages, self.natoms, 3)) 

465 self.real_forces[1:-1] = forces 

466 

467 state = NEBState(self, images, energies) 

468 

469 # Can we get rid of self.energies, self.imax, self.emax etc.? 

470 self.imax = state.imax 

471 self.emax = state.emax 

472 

473 spring1 = state.spring(0) 

474 

475 self.residuals = [] 

476 for i in range(1, self.nimages - 1): 

477 spring2 = state.spring(i) 

478 tangent = self.neb_method.get_tangent(state, spring1, spring2, i) 

479 

480 # Get overlap between full PES-derived force and tangent 

481 tangential_force = np.vdot(forces[i - 1], tangent) 

482 

483 # from now on we use the preconditioned forces (equal for precon=ID) 

484 imgforce = precon_forces[i - 1] 

485 

486 if i == self.imax and self.climb: 

487 """The climbing image, imax, is not affected by the spring 

488 forces. This image feels the full PES-derived force, 

489 but the tangential component is inverted: 

490 see Eq. 5 in paper II.""" 

491 if self.method == 'aseneb': 

492 tangent_mag = np.vdot(tangent, tangent) # For normalizing 

493 imgforce -= 2 * tangential_force / tangent_mag * tangent 

494 else: 

495 imgforce -= 2 * tangential_force * tangent 

496 else: 

497 self.neb_method.add_image_force(state, tangential_force, 

498 tangent, imgforce, spring1, 

499 spring2, i) 

500 # compute the residual - with ID precon, this is just max force 

501 residual = self.precon.get_residual(i, imgforce) 

502 self.residuals.append(residual) 

503 

504 spring1 = spring2 

505 

506 return precon_forces.reshape((-1, 3)) 

507 

508 def get_residual(self): 

509 """Return residual force along the band. 

510 

511 Typically this the maximum force component on any image. For 

512 non-trivial preconditioners, the appropriate preconditioned norm 

513 is used to compute the residual. 

514 """ 

515 if self.residuals is None: 

516 raise RuntimeError("get_residual() called before get_forces()") 

517 return np.max(self.residuals) 

518 

519 def get_potential_energy(self, force_consistent=False): 

520 """Return the maximum potential energy along the band. 

521 Note that the force_consistent keyword is ignored and is only 

522 present for compatibility with ase.Atoms.get_potential_energy.""" 

523 return self.emax 

524 

525 def set_calculators(self, calculators): 

526 """Set new calculators to the images. 

527 

528 Parameters 

529 ---------- 

530 calculators : Calculator / list(Calculator) 

531 calculator(s) to attach to images 

532 - single calculator, only if allow_shared_calculator=True 

533 list of calculators if length: 

534 - length nimages, set to all images 

535 - length nimages-2, set to non-end images only 

536 """ 

537 

538 if not isinstance(calculators, list): 

539 if self.allow_shared_calculator: 

540 calculators = [calculators] * self.nimages 

541 else: 

542 raise RuntimeError("Cannot set shared calculator to NEB " 

543 "with allow_shared_calculator=False") 

544 

545 n = len(calculators) 

546 if n == self.nimages: 

547 for i in range(self.nimages): 

548 self.images[i].calc = calculators[i] 

549 elif n == self.nimages - 2: 

550 for i in range(1, self.nimages - 1): 

551 self.images[i].calc = calculators[i - 1] 

552 else: 

553 raise RuntimeError( 

554 'len(calculators)=%d does not fit to len(images)=%d' 

555 % (n, self.nimages)) 

556 

557 def __len__(self): 

558 # Corresponds to number of optimizable degrees of freedom, i.e. 

559 # virtual atom count for the optimization algorithm. 

560 return (self.nimages - 2) * self.natoms 

561 

562 def iterimages(self): 

563 # Allows trajectory to convert NEB into several images 

564 for i, atoms in enumerate(self.images): 

565 if i == 0 or i == self.nimages - 1: 

566 yield atoms 

567 else: 

568 atoms = atoms.copy() 

569 self.freeze_results_on_image( 

570 atoms, energy=self.energies[i], 

571 forces=self.real_forces[i]) 

572 

573 yield atoms 

574 

575 def spline_fit(self, positions=None, norm='precon'): 

576 """ 

577 Fit a cubic spline to this NEB 

578 

579 Args: 

580 norm (str, optional): Norm to use: 'precon' (default) or 'euclidean' 

581 

582 Returns: 

583 fit: ase.precon.precon.SplineFit instance 

584 """ 

585 if norm == 'precon': 

586 if self.precon is None or isinstance(self.precon, str): 

587 self.precon = PreconImages(self.precon, self.images) 

588 precon = self.precon 

589 # if this is the first call, we need to build the preconditioners 

590 elif norm == 'euclidean': 

591 precon = PreconImages('ID', self.images) 

592 else: 

593 raise ValueError(f'unsupported norm {norm}') 

594 return precon.spline_fit(positions) 

595 

596 def integrate_forces(self, spline_points=1000, bc_type='not-a-knot'): 

597 """Use spline fit to integrate forces along MEP to approximate 

598 energy differences using the virtual work approach. 

599 

600 Args: 

601 spline_points (int, optional): Number of points. Defaults to 1000. 

602 bc_type (str, optional): Boundary conditions, default 'not-a-knot'. 

603 

604 Returns: 

605 s: reaction coordinate in range [0, 1], with `spline_points` entries 

606 E: result of integrating forces, on the same grid as `s`. 

607 F: projected forces along MEP 

608 """ 

609 # note we use standard Euclidean rather than preconditioned norm 

610 # to compute the virtual work 

611 fit = self.spline_fit(norm='euclidean') 

612 forces = np.array([image.get_forces().reshape(-1) 

613 for image in self.images]) 

614 f = CubicSpline(fit.s, forces, bc_type=bc_type) 

615 

616 s = np.linspace(0.0, 1.0, spline_points, endpoint=True) 

617 dE = f(s) * fit.dx_ds(s) 

618 F = dE.sum(axis=1) 

619 E = -cumtrapz(F, s, initial=0.0) 

620 return s, E, F 

621 

622 

623class DyNEB(BaseNEB): 

624 def __init__(self, images, k=0.1, fmax=0.05, climb=False, parallel=False, 

625 remove_rotation_and_translation=False, world=None, 

626 dynamic_relaxation=True, scale_fmax=0., method='aseneb', 

627 allow_shared_calculator=False, precon=None): 

628 """ 

629 Subclass of NEB that allows for scaled and dynamic optimizations of 

630 images. This method, which only works in series, does not perform 

631 force calls on images that are below the convergence criterion. 

632 The convergence criteria can be scaled with a displacement metric 

633 to focus the optimization on the saddle point region. 

634 

635 'Scaled and Dynamic Optimizations of Nudged Elastic Bands', 

636 P. Lindgren, G. Kastlunger and A. A. Peterson, 

637 J. Chem. Theory Comput. 15, 11, 5787-5793 (2019). 

638 

639 dynamic_relaxation: bool 

640 True skips images with forces below the convergence criterion. 

641 This is updated after each force call; if a previously converged 

642 image goes out of tolerance (due to spring adjustments between 

643 the image and its neighbors), it will be optimized again. 

644 False reverts to the default NEB implementation. 

645 

646 fmax: float 

647 Must be identical to the fmax of the optimizer. 

648 

649 scale_fmax: float 

650 Scale convergence criteria along band based on the distance between 

651 an image and the image with the highest potential energy. This 

652 keyword determines how rapidly the convergence criteria are scaled. 

653 """ 

654 super().__init__( 

655 images, k=k, climb=climb, parallel=parallel, 

656 remove_rotation_and_translation=remove_rotation_and_translation, 

657 world=world, method=method, 

658 allow_shared_calculator=allow_shared_calculator, precon=precon) 

659 self.fmax = fmax 

660 self.dynamic_relaxation = dynamic_relaxation 

661 self.scale_fmax = scale_fmax 

662 

663 if not self.dynamic_relaxation and self.scale_fmax: 

664 msg = ('Scaled convergence criteria only implemented in series ' 

665 'with dynamic relaxation.') 

666 raise ValueError(msg) 

667 

668 def set_positions(self, positions): 

669 if not self.dynamic_relaxation: 

670 return super().set_positions(positions) 

671 

672 n1 = 0 

673 for i, image in enumerate(self.images[1:-1]): 

674 if self.parallel: 

675 msg = ('Dynamic relaxation does not work efficiently ' 

676 'when parallelizing over images. Try AutoNEB ' 

677 'routine for freezing images in parallel.') 

678 raise ValueError(msg) 

679 else: 

680 forces_dyn = self._fmax_all(self.images) 

681 if forces_dyn[i] < self.fmax: 

682 n1 += self.natoms 

683 else: 

684 n2 = n1 + self.natoms 

685 image.set_positions(positions[n1:n2]) 

686 n1 = n2 

687 

688 def _fmax_all(self, images): 

689 """Store maximum force acting on each image in list. This is used in 

690 the dynamic optimization routine in the set_positions() function.""" 

691 n = self.natoms 

692 forces = self.get_forces() 

693 fmax_images = [ 

694 np.sqrt((forces[n * i:n + n * i] ** 2).sum(axis=1)).max() 

695 for i in range(self.nimages - 2)] 

696 return fmax_images 

697 

698 def get_forces(self): 

699 forces = super().get_forces() 

700 if not self.dynamic_relaxation: 

701 return forces 

702 

703 """Get NEB forces and scale the convergence criteria to focus 

704 optimization on saddle point region. The keyword scale_fmax 

705 determines the rate of convergence scaling.""" 

706 n = self.natoms 

707 for i in range(self.nimages - 2): 

708 n1 = n * i 

709 n2 = n1 + n 

710 force = np.sqrt((forces[n1:n2] ** 2.).sum(axis=1)).max() 

711 n_imax = (self.imax - 1) * n # Image with highest energy. 

712 

713 positions = self.get_positions() 

714 pos_imax = positions[n_imax:n_imax + n] 

715 

716 """Scale convergence criteria based on distance between an 

717 image and the image with the highest potential energy.""" 

718 rel_pos = np.sqrt(((positions[n1:n2] - pos_imax) ** 2).sum()) 

719 if force < self.fmax * (1 + rel_pos * self.scale_fmax): 

720 if i == self.imax - 1: 

721 # Keep forces at saddle point for the log file. 

722 pass 

723 else: 

724 # Set forces to zero before they are sent to optimizer. 

725 forces[n1:n2, :] = 0 

726 return forces 

727 

728 

729def _check_deprecation(keyword, kwargs): 

730 if keyword in kwargs: 

731 warnings.warn(f'Keyword {keyword} of NEB is deprecated. ' 

732 'Please use the DyNEB class instead for dynamic ' 

733 'relaxation', FutureWarning) 

734 

735 

736class NEB(DyNEB): 

737 def __init__(self, images, k=0.1, climb=False, parallel=False, 

738 remove_rotation_and_translation=False, world=None, 

739 method='aseneb', allow_shared_calculator=False, 

740 precon=None, **kwargs): 

741 """Nudged elastic band. 

742 

743 Paper I: 

744 

745 G. Henkelman and H. Jonsson, Chem. Phys, 113, 9978 (2000). 

746 :doi:`10.1063/1.1323224` 

747 

748 Paper II: 

749 

750 G. Henkelman, B. P. Uberuaga, and H. Jonsson, Chem. Phys, 

751 113, 9901 (2000). 

752 :doi:`10.1063/1.1329672` 

753 

754 Paper III: 

755 

756 E. L. Kolsbjerg, M. N. Groves, and B. Hammer, J. Chem. Phys, 

757 145, 094107 (2016) 

758 :doi:`10.1063/1.4961868` 

759 

760 Paper IV: 

761 

762 S. Makri, C. Ortner and J. R. Kermode, J. Chem. Phys. 

763 150, 094109 (2019) 

764 https://dx.doi.org/10.1063/1.5064465 

765 

766 images: list of Atoms objects 

767 Images defining path from initial to final state. 

768 k: float or list of floats 

769 Spring constant(s) in eV/Ang. One number or one for each spring. 

770 climb: bool 

771 Use a climbing image (default is no climbing image). 

772 parallel: bool 

773 Distribute images over processors. 

774 remove_rotation_and_translation: bool 

775 TRUE actives NEB-TR for removing translation and 

776 rotation during NEB. By default applied non-periodic 

777 systems 

778 method: string of method 

779 Choice betweeen five methods: 

780 

781 * aseneb: standard ase NEB implementation 

782 * improvedtangent: Paper I NEB implementation 

783 * eb: Paper III full spring force implementation 

784 * spline: Paper IV spline interpolation (supports precon) 

785 * string: Paper IV string method (supports precon) 

786 allow_shared_calculator: bool 

787 Allow images to share the same calculator between them. 

788 Incompatible with parallelisation over images. 

789 precon: string, :class:`ase.optimize.precon.Precon` instance or list of 

790 instances. If present, enable preconditioing as in Paper IV. This is 

791 possible using the 'spline' or 'string' methods only. 

792 Default is no preconditioning (precon=None), which is converted to 

793 a list of :class:`ase.precon.precon.IdentityPrecon` instances. 

794 """ 

795 for keyword in 'dynamic_relaxation', 'fmax', 'scale_fmax': 

796 _check_deprecation(keyword, kwargs) 

797 defaults = dict(dynamic_relaxation=False, 

798 fmax=0.05, 

799 scale_fmax=0.0) 

800 defaults.update(kwargs) 

801 # Only reason for separating BaseNEB/NEB is that we are 

802 # deprecating dynamic_relaxation. 

803 # 

804 # We can turn BaseNEB into NEB once we get rid of the 

805 # deprecated variables. 

806 # 

807 # Then we can also move DyNEB into ase.dyneb without cyclic imports. 

808 # We can do that in ase-3.22 or 3.23. 

809 super().__init__( 

810 images, k=k, climb=climb, parallel=parallel, 

811 remove_rotation_and_translation=remove_rotation_and_translation, 

812 world=world, method=method, 

813 allow_shared_calculator=allow_shared_calculator, 

814 precon=precon, 

815 **defaults) 

816 

817 

818class NEBOptimizer(Optimizer): 

819 """ 

820 This optimizer applies an adaptive ODE solver to a NEB 

821 

822 Details of the adaptive ODE solver are described in paper IV 

823 """ 

824 

825 def __init__(self, 

826 neb, 

827 restart=None, logfile='-', trajectory=None, 

828 master=None, 

829 append_trajectory=False, 

830 method='ODE', 

831 alpha=0.01, 

832 verbose=0, 

833 rtol=0.1, 

834 C1=1e-2, 

835 C2=2.0): 

836 

837 super().__init__(atoms=neb, restart=restart, 

838 logfile=logfile, trajectory=trajectory, 

839 master=master, 

840 append_trajectory=append_trajectory, 

841 force_consistent=False) 

842 self.neb = neb 

843 

844 method = method.lower() 

845 methods = ['ode', 'static', 'krylov'] 

846 if method not in methods: 

847 raise ValueError(f'method must be one of {methods}') 

848 self.method = method 

849 

850 self.alpha = alpha 

851 self.verbose = verbose 

852 self.rtol = rtol 

853 self.C1 = C1 

854 self.C2 = C2 

855 

856 def force_function(self, X): 

857 positions = X.reshape((self.neb.nimages - 2) * 

858 self.neb.natoms, 3) 

859 self.neb.set_positions(positions) 

860 forces = self.neb.get_forces().reshape(-1) 

861 return forces 

862 

863 def get_residual(self, F=None, X=None): 

864 return self.neb.get_residual() 

865 

866 def log(self): 

867 fmax = self.get_residual() 

868 T = time.localtime() 

869 if self.logfile is not None: 

870 name = f'{self.__class__.__name__}[{self.method}]' 

871 if self.nsteps == 0: 

872 args = (" " * len(name), "Step", "Time", "fmax") 

873 msg = "%s %4s %8s %12s\n" % args 

874 self.logfile.write(msg) 

875 

876 args = (name, self.nsteps, T[3], T[4], T[5], fmax) 

877 msg = "%s: %3d %02d:%02d:%02d %12.4f\n" % args 

878 self.logfile.write(msg) 

879 self.logfile.flush() 

880 

881 def callback(self, X, F=None): 

882 self.log() 

883 self.call_observers() 

884 self.nsteps += 1 

885 

886 def run_ode(self, fmax): 

887 try: 

888 ode12r(self.force_function, 

889 self.neb.get_positions().reshape(-1), 

890 fmax=fmax, 

891 rtol=self.rtol, 

892 C1=self.C1, 

893 C2=self.C2, 

894 steps=self.max_steps, 

895 verbose=self.verbose, 

896 callback=self.callback, 

897 residual=self.get_residual) 

898 return True 

899 except OptimizerConvergenceError: 

900 return False 

901 

902 def run_static(self, fmax): 

903 X = self.neb.get_positions().reshape(-1) 

904 for step in range(self.max_steps): 

905 F = self.force_function(X) 

906 if self.neb.get_residual() <= fmax: 

907 return True 

908 X += self.alpha * F 

909 self.callback(X) 

910 return False 

911 

912 def run(self, fmax=0.05, steps=None, method=None): 

913 """ 

914 Optimize images to obtain the minimum energy path 

915 

916 Parameters 

917 ---------- 

918 fmax - desired force tolerance 

919 steps - maximum number of steps 

920 """ 

921 if steps: 

922 self.max_steps = steps 

923 if method is None: 

924 method = self.method 

925 if method == 'ode': 

926 return self.run_ode(fmax) 

927 elif method == 'static': 

928 return self.run_static(fmax) 

929 else: 

930 raise ValueError(f'unknown method: {self.method}') 

931 

932 

933class IDPP(Calculator): 

934 """Image dependent pair potential. 

935 

936 See: 

937 Improved initial guess for minimum energy path calculations. 

938 Søren Smidstrup, Andreas Pedersen, Kurt Stokbro and Hannes Jónsson 

939 Chem. Phys. 140, 214106 (2014) 

940 """ 

941 

942 implemented_properties = ['energy', 'forces'] 

943 

944 def __init__(self, target, mic): 

945 Calculator.__init__(self) 

946 self.target = target 

947 self.mic = mic 

948 

949 def calculate(self, atoms, properties, system_changes): 

950 Calculator.calculate(self, atoms, properties, system_changes) 

951 

952 P = atoms.get_positions() 

953 d = [] 

954 D = [] 

955 for p in P: 

956 Di = P - p 

957 if self.mic: 

958 Di, di = find_mic(Di, atoms.get_cell(), atoms.get_pbc()) 

959 else: 

960 di = np.sqrt((Di ** 2).sum(1)) 

961 d.append(di) 

962 D.append(Di) 

963 d = np.array(d) 

964 D = np.array(D) 

965 

966 dd = d - self.target 

967 d.ravel()[::len(d) + 1] = 1 # avoid dividing by zero 

968 d4 = d ** 4 

969 e = 0.5 * (dd ** 2 / d4).sum() 

970 f = -2 * ((dd * (1 - 2 * dd / d) / d ** 5)[..., np.newaxis] * D).sum( 

971 0) 

972 self.results = {'energy': e, 'forces': f} 

973 

974 

975@deprecated("SingleCalculatorNEB is deprecated. " 

976 "Please use NEB(allow_shared_calculator=True) instead.") 

977class SingleCalculatorNEB(NEB): 

978 def __init__(self, images, *args, **kwargs): 

979 kwargs["allow_shared_calculator"] = True 

980 super().__init__(images, *args, **kwargs) 

981 

982 

983def interpolate(images, mic=False, interpolate_cell=False, 

984 use_scaled_coord=False, apply_constraint=None): 

985 """Given a list of images, linearly interpolate the positions of the 

986 interior images. 

987 

988 mic: bool 

989 Map movement into the unit cell by using the minimum image convention. 

990 interpolate_cell: bool 

991 Interpolate the three cell vectors linearly just like the atomic 

992 positions. Not implemented for NEB calculations! 

993 use_scaled_coord: bool 

994 Use scaled/internal/fractional coordinates instead of real ones for the 

995 interpolation. Not implemented for NEB calculations! 

996 apply_constraint: bool 

997 Controls if the constraints attached to the images 

998 are ignored or applied when setting the interpolated positions. 

999 Default value is None, in this case the resulting constrained positions 

1000 (apply_constraint=True) are compared with unconstrained positions 

1001 (apply_constraint=False), if the positions are not the same 

1002 the user is required to specify the desired behaviour 

1003 by setting up apply_constraint keyword argument to False or True. 

1004 """ 

1005 if use_scaled_coord: 

1006 pos1 = images[0].get_scaled_positions(wrap=mic) 

1007 pos2 = images[-1].get_scaled_positions(wrap=mic) 

1008 else: 

1009 pos1 = images[0].get_positions() 

1010 pos2 = images[-1].get_positions() 

1011 d = pos2 - pos1 

1012 if not use_scaled_coord and mic: 

1013 d = find_mic(d, images[0].get_cell(), images[0].pbc)[0] 

1014 d /= (len(images) - 1.0) 

1015 if interpolate_cell: 

1016 cell1 = images[0].get_cell() 

1017 cell2 = images[-1].get_cell() 

1018 cell_diff = cell2 - cell1 

1019 cell_diff /= (len(images) - 1.0) 

1020 for i in range(1, len(images) - 1): 

1021 # first the new cell, otherwise scaled positions are wrong 

1022 if interpolate_cell: 

1023 images[i].set_cell(cell1 + i * cell_diff) 

1024 new_pos = pos1 + i * d 

1025 if use_scaled_coord: 

1026 images[i].set_scaled_positions(new_pos) 

1027 else: 

1028 if apply_constraint is None: 

1029 unconstrained_image = images[i].copy() 

1030 unconstrained_image.set_positions(new_pos, 

1031 apply_constraint=False) 

1032 images[i].set_positions(new_pos, apply_constraint=True) 

1033 try: 

1034 np.testing.assert_allclose(unconstrained_image.positions, 

1035 images[i].positions) 

1036 except AssertionError: 

1037 raise RuntimeError(f"Constraint(s) in image number {i} \n" 

1038 f"affect the interpolation results.\n" 

1039 "Please specify if you want to \n" 

1040 "apply or ignore the constraints \n" 

1041 "during the interpolation \n" 

1042 "with apply_constraint argument.") 

1043 else: 

1044 images[i].set_positions(new_pos, 

1045 apply_constraint=apply_constraint) 

1046 

1047 

1048def idpp_interpolate(images, traj='idpp.traj', log='idpp.log', fmax=0.1, 

1049 optimizer=MDMin, mic=False, steps=100): 

1050 """Interpolate using the IDPP method. 'images' can either be a plain 

1051 list of images or an NEB object (containing a list of images).""" 

1052 if hasattr(images, 'interpolate'): 

1053 neb = images 

1054 else: 

1055 neb = NEB(images) 

1056 

1057 d1 = neb.images[0].get_all_distances(mic=mic) 

1058 d2 = neb.images[-1].get_all_distances(mic=mic) 

1059 d = (d2 - d1) / (neb.nimages - 1) 

1060 real_calcs = [] 

1061 for i, image in enumerate(neb.images): 

1062 real_calcs.append(image.calc) 

1063 image.calc = IDPP(d1 + i * d, mic=mic) 

1064 

1065 with optimizer(neb, trajectory=traj, logfile=log) as opt: 

1066 opt.run(fmax=fmax, steps=steps) 

1067 

1068 for image, calc in zip(neb.images, real_calcs): 

1069 image.calc = calc 

1070 

1071 

1072class NEBTools: 

1073 """Class to make many of the common tools for NEB analysis available to 

1074 the user. Useful for scripting the output of many jobs. Initialize with 

1075 list of images which make up one or more band of the NEB relaxation.""" 

1076 

1077 def __init__(self, images): 

1078 self.images = images 

1079 

1080 @deprecated('NEBTools.get_fit() is deprecated. ' 

1081 'Please use ase.utils.forcecurve.fit_images(images).') 

1082 def get_fit(self): 

1083 return fit_images(self.images) 

1084 

1085 def get_barrier(self, fit=True, raw=False): 

1086 """Returns the barrier estimate from the NEB, along with the 

1087 Delta E of the elementary reaction. If fit=True, the barrier is 

1088 estimated based on the interpolated fit to the images; if 

1089 fit=False, the barrier is taken as the maximum-energy image 

1090 without interpolation. Set raw=True to get the raw energy of the 

1091 transition state instead of the forward barrier.""" 

1092 forcefit = fit_images(self.images) 

1093 energies = forcefit.energies 

1094 fit_energies = forcefit.fit_energies 

1095 dE = energies[-1] - energies[0] 

1096 if fit: 

1097 barrier = max(fit_energies) 

1098 else: 

1099 barrier = max(energies) 

1100 if raw: 

1101 barrier += self.images[0].get_potential_energy() 

1102 return barrier, dE 

1103 

1104 def get_fmax(self, **kwargs): 

1105 """Returns fmax, as used by optimizers with NEB.""" 

1106 neb = NEB(self.images, **kwargs) 

1107 forces = neb.get_forces() 

1108 return np.sqrt((forces ** 2).sum(axis=1).max()) 

1109 

1110 def plot_band(self, ax=None): 

1111 """Plots the NEB band on matplotlib axes object 'ax'. If ax=None 

1112 returns a new figure object.""" 

1113 forcefit = fit_images(self.images) 

1114 ax = forcefit.plot(ax=ax) 

1115 return ax.figure 

1116 

1117 def plot_bands(self, constant_x=False, constant_y=False, 

1118 nimages=None, label='nebplots'): 

1119 """Given a trajectory containing many steps of a NEB, makes 

1120 plots of each band in the series in a single PDF. 

1121 

1122 constant_x: bool 

1123 Use the same x limits on all plots. 

1124 constant_y: bool 

1125 Use the same y limits on all plots. 

1126 nimages: int 

1127 Number of images per band. Guessed if not supplied. 

1128 label: str 

1129 Name for the output file. .pdf will be appended. 

1130 """ 

1131 from matplotlib import pyplot 

1132 from matplotlib.backends.backend_pdf import PdfPages 

1133 if nimages is None: 

1134 nimages = self._guess_nimages() 

1135 nebsteps = len(self.images) // nimages 

1136 if constant_x or constant_y: 

1137 sys.stdout.write('Scaling axes.\n') 

1138 sys.stdout.flush() 

1139 # Plot all to one plot, then pull its x and y range. 

1140 fig, ax = pyplot.subplots() 

1141 for index in range(nebsteps): 

1142 images = self.images[index * nimages:(index + 1) * nimages] 

1143 NEBTools(images).plot_band(ax=ax) 

1144 xlim = ax.get_xlim() 

1145 ylim = ax.get_ylim() 

1146 pyplot.close(fig) # Reference counting "bug" in pyplot. 

1147 with PdfPages(label + '.pdf') as pdf: 

1148 for index in range(nebsteps): 

1149 sys.stdout.write('\rProcessing band {:10d} / {:10d}' 

1150 .format(index, nebsteps)) 

1151 sys.stdout.flush() 

1152 fig, ax = pyplot.subplots() 

1153 images = self.images[index * nimages:(index + 1) * nimages] 

1154 NEBTools(images).plot_band(ax=ax) 

1155 if constant_x: 

1156 ax.set_xlim(xlim) 

1157 if constant_y: 

1158 ax.set_ylim(ylim) 

1159 pdf.savefig(fig) 

1160 pyplot.close(fig) # Reference counting "bug" in pyplot. 

1161 sys.stdout.write('\n') 

1162 

1163 def _guess_nimages(self): 

1164 """Attempts to guess the number of images per band from 

1165 a trajectory, based solely on the repetition of the 

1166 potential energy of images. This should also work for symmetric 

1167 cases.""" 

1168 e_first = self.images[0].get_potential_energy() 

1169 nimages = None 

1170 for index, image in enumerate(self.images[1:], start=1): 

1171 e = image.get_potential_energy() 

1172 if e == e_first: 

1173 # Need to check for symmetric case when e_first = e_last. 

1174 try: 

1175 e_next = self.images[index + 1].get_potential_energy() 

1176 except IndexError: 

1177 pass 

1178 else: 

1179 if e_next == e_first: 

1180 nimages = index + 1 # Symmetric 

1181 break 

1182 nimages = index # Normal 

1183 break 

1184 if nimages is None: 

1185 sys.stdout.write('Appears to be only one band in the images.\n') 

1186 return len(self.images) 

1187 # Sanity check that the energies of the last images line up too. 

1188 e_last = self.images[nimages - 1].get_potential_energy() 

1189 e_nextlast = self.images[2 * nimages - 1].get_potential_energy() 

1190 if not (e_last == e_nextlast): 

1191 raise RuntimeError('Could not guess number of images per band.') 

1192 sys.stdout.write('Number of images per band guessed to be {:d}.\n' 

1193 .format(nimages)) 

1194 return nimages 

1195 

1196 

1197class NEBtools(NEBTools): 

1198 @deprecated('NEBtools has been renamed; please use NEBTools.') 

1199 def __init__(self, images): 

1200 NEBTools.__init__(self, images) 

1201 

1202 

1203@deprecated('Please use NEBTools.plot_band_from_fit.') 

1204def plot_band_from_fit(s, E, Sfit, Efit, lines, ax=None): 

1205 NEBTools.plot_band_from_fit(s, E, Sfit, Efit, lines, ax=None) 

1206 

1207 

1208def fit0(*args, **kwargs): 

1209 raise DeprecationWarning('fit0 is deprecated. Use `fit_raw` from ' 

1210 '`ase.utils.forcecurve` instead.')