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

1from ase.io import Trajectory 

2from ase.io import read 

3from ase.neb import NEB 

4from ase.optimize import BFGS 

5from ase.optimize import FIRE 

6from ase.calculators.singlepoint import SinglePointCalculator 

7import ase.parallel as mpi 

8import numpy as np 

9import shutil 

10import os 

11import types 

12from math import log 

13from math import exp 

14from contextlib import ExitStack 

15 

16 

17class AutoNEB: 

18 """AutoNEB object. 

19 

20 The AutoNEB algorithm streamlines the execution of NEB and CI-NEB 

21 calculations following the algorithm described in: 

22 

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

24 145, 094107, 2016. (doi: 10.1063/1.4961868) 

25 

26 The user supplies at minimum the two end-points and possibly also some 

27 intermediate images. 

28 

29 The stages are: 

30 1) Define a set of images and name them sequentially. 

31 Must at least have a relaxed starting and ending image 

32 User can supply intermediate guesses which do not need to 

33 have previously determined energies (probably from another 

34 NEB calculation with a lower level of theory) 

35 2) AutoNEB will first evaluate the user provided intermediate images 

36 3) AutoNEB will then add additional images dynamically until n_max 

37 is reached 

38 4) A climbing image will attempt to locate the saddle point 

39 5) All the images between the highest point and the starting point 

40 are further relaxed to smooth the path 

41 6) All the images between the highest point and the ending point are 

42 further relaxed to smooth the path 

43 

44 Step 4 and 5-6 are optional steps! 

45 

46 Parameters: 

47 

48 attach_calculators: 

49 Function which adds valid calculators to the list of images supplied. 

50 prefix: string 

51 All files that the AutoNEB method reads and writes are prefixed with 

52 this string 

53 n_simul: int 

54 The number of relaxations run in parallel. 

55 n_max: int 

56 The number of images along the NEB path when done. 

57 This number includes the two end-points. 

58 Important: due to the dynamic adding of images around the peak n_max 

59 must be updated if the NEB is restarted. 

60 climb: boolean 

61 Should a CI-NEB calculation be done at the top-point 

62 fmax: float or list of floats 

63 The maximum force along the NEB path 

64 maxsteps: int 

65 The maximum number of steps in each NEB relaxation. 

66 If a list is given the first number of steps is used in the build-up 

67 and final scan phase; 

68 the second number of steps is used in the CI step after all images 

69 have been inserted. 

70 k: float 

71 The spring constant along the NEB path 

72 method: str (see neb.py) 

73 Choice betweeen three method: 

74 'aseneb', standard ase NEB implementation 

75 'improvedtangent', published NEB implementation 

76 'eb', full spring force implementation (default) 

77 optimizer: str 

78 Which optimizer to use in the relaxation. Valid values are 'BFGS' 

79 and 'FIRE' (default) 

80 space_energy_ratio: float 

81 The preference for new images to be added in a big energy gab 

82 with a preference around the peak or in the biggest geometric gab. 

83 A space_energy_ratio set to 1 will only considder geometric gabs 

84 while one set to 0 will result in only images for energy 

85 resolution. 

86 

87 The AutoNEB method uses a fixed file-naming convention. 

88 The initial images should have the naming prefix000.traj, prefix001.traj, 

89 ... up until the final image in prefix00N.traj 

90 Images are dynamically added in between the first and last image until 

91 n_max images have been reached. 

92 When doing the i'th NEB optimization a set of files 

93 prefixXXXiter00i.traj exists with XXX ranging from 000 to the N images 

94 currently in the NEB. 

95 

96 The most recent NEB path can always be monitored by: 

97 $ ase-gui -n -1 neb???.traj 

98 """ 

99 

100 def __init__(self, attach_calculators, prefix, n_simul, n_max, 

101 iter_folder='AutoNEB_iter', 

102 fmax=0.025, maxsteps=10000, k=0.1, climb=True, method='eb', 

103 optimizer='FIRE', 

104 remove_rotation_and_translation=False, space_energy_ratio=0.5, 

105 world=None, 

106 parallel=True, smooth_curve=False, interpolate_method='idpp'): 

107 self.attach_calculators = attach_calculators 

108 self.prefix = prefix 

109 self.n_simul = n_simul 

110 self.n_max = n_max 

111 self.climb = climb 

112 self.all_images = [] 

113 

114 self.parallel = parallel 

115 self.maxsteps = maxsteps 

116 self.fmax = fmax 

117 self.k = k 

118 self.method = method 

119 self.remove_rotation_and_translation = remove_rotation_and_translation 

120 self.space_energy_ratio = space_energy_ratio 

121 if interpolate_method not in ['idpp', 'linear']: 

122 self.interpolate_method = 'idpp' 

123 print('Interpolation method not implementet.', 

124 'Using the IDPP method.') 

125 else: 

126 self.interpolate_method = interpolate_method 

127 if world is None: 

128 world = mpi.world 

129 self.world = world 

130 self.smooth_curve = smooth_curve 

131 

132 if optimizer == 'BFGS': 

133 self.optimizer = BFGS 

134 elif optimizer == 'FIRE': 

135 self.optimizer = FIRE 

136 else: 

137 raise Exception('Optimizer needs to be BFGS or FIRE') 

138 self.iter_folder = iter_folder 

139 if not os.path.exists(self.iter_folder) and self.world.rank == 0: 

140 os.makedirs(self.iter_folder) 

141 

142 def execute_one_neb(self, n_cur, to_run, climb=False, many_steps=False): 

143 with ExitStack() as exitstack: 

144 self._execute_one_neb(exitstack, n_cur, to_run, 

145 climb=climb, many_steps=many_steps) 

146 

147 def _execute_one_neb(self, exitstack, n_cur, to_run, 

148 climb=False, many_steps=False): 

149 '''Internal method which executes one NEB optimization.''' 

150 

151 closelater = exitstack.enter_context 

152 

153 self.iteration += 1 

154 # First we copy around all the images we are not using in this 

155 # neb (for reproducability purposes) 

156 if self.world.rank == 0: 

157 for i in range(n_cur): 

158 if i not in to_run[1: -1]: 

159 filename = '%s%03d.traj' % (self.prefix, i) 

160 with Trajectory(filename, mode='w', 

161 atoms=self.all_images[i]) as traj: 

162 traj.write() 

163 filename_ref = self.iter_folder + \ 

164 '/%s%03diter%03d.traj' % (self.prefix, i, 

165 self.iteration) 

166 if os.path.isfile(filename): 

167 shutil.copy2(filename, filename_ref) 

168 if self.world.rank == 0: 

169 print('Now starting iteration %d on ' % self.iteration, to_run) 

170 # Attach calculators to all the images we will include in the NEB 

171 self.attach_calculators([self.all_images[i] for i in to_run[1: -1]]) 

172 neb = NEB([self.all_images[i] for i in to_run], 

173 k=[self.k[i] for i in to_run[0:-1]], 

174 method=self.method, 

175 parallel=self.parallel, 

176 remove_rotation_and_translation=self 

177 .remove_rotation_and_translation, 

178 climb=climb) 

179 

180 # Do the actual NEB calculation 

181 qn = closelater( 

182 self.optimizer(neb, 

183 logfile=self.iter_folder + 

184 '/%s_log_iter%03d.log' % (self.prefix, 

185 self.iteration)) 

186 ) 

187 

188 # Find the ranks which are masters for each their calculation 

189 if self.parallel: 

190 nneb = to_run[0] 

191 nim = len(to_run) - 2 

192 n = self.world.size // nim # number of cpu's per image 

193 j = 1 + self.world.rank // n # my image number 

194 assert nim * n == self.world.size 

195 traj = closelater(Trajectory( 

196 '%s%03d.traj' % (self.prefix, j + nneb), 'w', 

197 self.all_images[j + nneb], 

198 master=(self.world.rank % n == 0) 

199 )) 

200 filename_ref = self.iter_folder + \ 

201 '/%s%03diter%03d.traj' % (self.prefix, 

202 j + nneb, self.iteration) 

203 trajhist = closelater(Trajectory( 

204 filename_ref, 'w', 

205 self.all_images[j + nneb], 

206 master=(self.world.rank % n == 0) 

207 )) 

208 qn.attach(traj) 

209 qn.attach(trajhist) 

210 else: 

211 num = 1 

212 for i, j in enumerate(to_run[1: -1]): 

213 filename_ref = self.iter_folder + \ 

214 '/%s%03diter%03d.traj' % (self.prefix, j, self.iteration) 

215 trajhist = closelater(Trajectory( 

216 filename_ref, 'w', self.all_images[j] 

217 )) 

218 qn.attach(seriel_writer(trajhist, i, num).write) 

219 

220 traj = closelater(Trajectory( 

221 '%s%03d.traj' % (self.prefix, j), 'w', 

222 self.all_images[j] 

223 )) 

224 qn.attach(seriel_writer(traj, i, num).write) 

225 num += 1 

226 

227 if isinstance(self.maxsteps, (list, tuple)) and many_steps: 

228 steps = self.maxsteps[1] 

229 elif isinstance(self.maxsteps, (list, tuple)) and not many_steps: 

230 steps = self.maxsteps[0] 

231 else: 

232 steps = self.maxsteps 

233 

234 if isinstance(self.fmax, (list, tuple)) and many_steps: 

235 fmax = self.fmax[1] 

236 elif isinstance(self.fmax, (list, tuple)) and not many_steps: 

237 fmax = self.fmax[0] 

238 else: 

239 fmax = self.fmax 

240 qn.run(fmax=fmax, steps=steps) 

241 

242 # Remove the calculators and replace them with single 

243 # point calculators and update all the nodes for 

244 # preperration for next iteration 

245 neb.distribute = types.MethodType(store_E_and_F_in_spc, neb) 

246 neb.distribute() 

247 

248 def run(self): 

249 '''Run the AutoNEB optimization algorithm.''' 

250 n_cur = self.__initialize__() 

251 while len(self.all_images) < self.n_simul + 2: 

252 if isinstance(self.k, (float, int)): 

253 self.k = [self.k] * (len(self.all_images) - 1) 

254 if self.world.rank == 0: 

255 print('Now adding images for initial run') 

256 # Insert a new image where the distance between two images is 

257 # the largest 

258 spring_lengths = [] 

259 for j in range(n_cur - 1): 

260 spring_vec = self.all_images[j + 1].get_positions() - \ 

261 self.all_images[j].get_positions() 

262 spring_lengths.append(np.linalg.norm(spring_vec)) 

263 jmax = np.argmax(spring_lengths) 

264 

265 if self.world.rank == 0: 

266 print('Max length between images is at ', jmax) 

267 

268 # The interpolation used to make initial guesses 

269 # If only start and end images supplied make all img at ones 

270 if len(self.all_images) == 2: 

271 n_between = self.n_simul 

272 else: 

273 n_between = 1 

274 

275 toInterpolate = [self.all_images[jmax]] 

276 for i in range(n_between): 

277 toInterpolate += [toInterpolate[0].copy()] 

278 toInterpolate += [self.all_images[jmax + 1]] 

279 

280 neb = NEB(toInterpolate) 

281 neb.interpolate(method=self.interpolate_method) 

282 

283 tmp = self.all_images[:jmax + 1] 

284 tmp += toInterpolate[1:-1] 

285 tmp.extend(self.all_images[jmax + 1:]) 

286 

287 self.all_images = tmp 

288 

289 # Expect springs to be in equilibrium 

290 k_tmp = self.k[:jmax] 

291 k_tmp += [self.k[jmax] * (n_between + 1)] * (n_between + 1) 

292 k_tmp.extend(self.k[jmax + 1:]) 

293 self.k = k_tmp 

294 

295 # Run the NEB calculation with the new image included 

296 n_cur += n_between 

297 

298 # Determine if any images do not have a valid energy yet 

299 energies = self.get_energies() 

300 

301 n_non_valid_energies = len([e for e in energies if e != e]) 

302 

303 if self.world.rank == 0: 

304 print('Start of evaluation of the initial images') 

305 

306 while n_non_valid_energies != 0: 

307 if isinstance(self.k, (float, int)): 

308 self.k = [self.k] * (len(self.all_images) - 1) 

309 

310 # First do one run since some energie are non-determined 

311 to_run, climb_safe = self.which_images_to_run_on() 

312 self.execute_one_neb(n_cur, to_run, climb=False) 

313 

314 energies = self.get_energies() 

315 n_non_valid_energies = len([e for e in energies if e != e]) 

316 

317 if self.world.rank == 0: 

318 print('Finished initialisation phase.') 

319 

320 # Then add one image at a time until we have n_max images 

321 while n_cur < self.n_max: 

322 if isinstance(self.k, (float, int)): 

323 self.k = [self.k] * (len(self.all_images) - 1) 

324 # Insert a new image where the distance between two images 

325 # is the largest OR where a higher energy reselution is needed 

326 if self.world.rank == 0: 

327 print('****Now adding another image until n_max is reached', 

328 '({0}/{1})****'.format(n_cur, self.n_max)) 

329 spring_lengths = [] 

330 for j in range(n_cur - 1): 

331 spring_vec = self.all_images[j + 1].get_positions() - \ 

332 self.all_images[j].get_positions() 

333 spring_lengths.append(np.linalg.norm(spring_vec)) 

334 

335 total_vec = self.all_images[0].get_positions() - \ 

336 self.all_images[-1].get_positions() 

337 tl = np.linalg.norm(total_vec) 

338 

339 fR = max(spring_lengths) / tl 

340 

341 e = self.get_energies() 

342 ed = [] 

343 emin = min(e) 

344 enorm = max(e) - emin 

345 for j in range(n_cur - 1): 

346 delta_E = (e[j + 1] - e[j]) * (e[j + 1] + e[j] - 2 * 

347 emin) / 2 / enorm 

348 ed.append(abs(delta_E)) 

349 

350 gR = max(ed) / enorm 

351 

352 if fR / gR > self.space_energy_ratio: 

353 jmax = np.argmax(spring_lengths) 

354 t = 'spring length!' 

355 else: 

356 jmax = np.argmax(ed) 

357 t = 'energy difference between neighbours!' 

358 

359 if self.world.rank == 0: 

360 print('Adding image between {0} and'.format(jmax), 

361 '{0}. New image point is selected'.format(jmax + 1), 

362 'on the basis of the biggest ' + t) 

363 

364 toInterpolate = [self.all_images[jmax]] 

365 toInterpolate += [toInterpolate[0].copy()] 

366 toInterpolate += [self.all_images[jmax + 1]] 

367 

368 neb = NEB(toInterpolate) 

369 neb.interpolate(method=self.interpolate_method) 

370 

371 tmp = self.all_images[:jmax + 1] 

372 tmp += toInterpolate[1:-1] 

373 tmp.extend(self.all_images[jmax + 1:]) 

374 

375 self.all_images = tmp 

376 

377 # Expect springs to be in equilibrium 

378 k_tmp = self.k[:jmax] 

379 k_tmp += [self.k[jmax] * 2] * 2 

380 k_tmp.extend(self.k[jmax + 1:]) 

381 self.k = k_tmp 

382 

383 # Run the NEB calculation with the new image included 

384 n_cur += 1 

385 to_run, climb_safe = self.which_images_to_run_on() 

386 

387 self.execute_one_neb(n_cur, to_run, climb=False) 

388 

389 if self.world.rank == 0: 

390 print('n_max images has been reached') 

391 

392 # Do a single climb around the top-point if requested 

393 if self.climb: 

394 if isinstance(self.k, (float, int)): 

395 self.k = [self.k] * (len(self.all_images) - 1) 

396 if self.world.rank == 0: 

397 print('****Now doing the CI-NEB calculation****') 

398 to_run, climb_safe = self.which_images_to_run_on() 

399 

400 assert climb_safe, 'climb_safe should be true at this point!' 

401 self.execute_one_neb(n_cur, to_run, climb=True, many_steps=True) 

402 

403 if not self.smooth_curve: 

404 return self.all_images 

405 

406 # If a smooth_curve is requsted ajust the springs to follow two 

407 # gaussian distributions 

408 e = self.get_energies() 

409 peak = self.get_highest_energy_index() 

410 k_max = 10 

411 

412 d1 = np.linalg.norm(self.all_images[peak].get_positions() - 

413 self.all_images[0].get_positions()) 

414 d2 = np.linalg.norm(self.all_images[peak].get_positions() - 

415 self.all_images[-1].get_positions()) 

416 l1 = -d1 ** 2 / log(0.2) 

417 l2 = -d2 ** 2 / log(0.2) 

418 

419 x1 = [] 

420 x2 = [] 

421 for i in range(peak): 

422 v = (self.all_images[i].get_positions() + 

423 self.all_images[i + 1].get_positions()) / 2 - \ 

424 self.all_images[0].get_positions() 

425 x1.append(np.linalg.norm(v)) 

426 

427 for i in range(peak, len(self.all_images) - 1): 

428 v = (self.all_images[i].get_positions() + 

429 self.all_images[i + 1].get_positions()) / 2 - \ 

430 self.all_images[0].get_positions() 

431 x2.append(np.linalg.norm(v)) 

432 k_tmp = [] 

433 for x in x1: 

434 k_tmp.append(k_max * exp(-((x - d1) ** 2) / l1)) 

435 for x in x2: 

436 k_tmp.append(k_max * exp(-((x - d1) ** 2) / l2)) 

437 

438 self.k = k_tmp 

439 # Roll back to start from the top-point 

440 if self.world.rank == 0: 

441 print('Now moving from top to start') 

442 highest_energy_index = self.get_highest_energy_index() 

443 nneb = highest_energy_index - self.n_simul - 1 

444 while nneb >= 0: 

445 self.execute_one_neb(n_cur, range(nneb, nneb + self.n_simul + 2), 

446 climb=False) 

447 nneb -= 1 

448 

449 # Roll forward from the top-point until the end 

450 nneb = self.get_highest_energy_index() 

451 

452 if self.world.rank == 0: 

453 print('Now moving from top to end') 

454 while nneb <= self.n_max - self.n_simul - 2: 

455 self.execute_one_neb(n_cur, range(nneb, nneb + self.n_simul + 2), 

456 climb=False) 

457 nneb += 1 

458 return self.all_images 

459 

460 def __initialize__(self): 

461 '''Load files from the filesystem.''' 

462 if not os.path.isfile('%s000.traj' % self.prefix): 

463 raise IOError('No file with name %s000.traj' % self.prefix, 

464 'was found. Should contain initial image') 

465 

466 # Find the images that exist 

467 index_exists = [i for i in range(self.n_max) if 

468 os.path.isfile('%s%03d.traj' % (self.prefix, i))] 

469 

470 n_cur = index_exists[-1] + 1 

471 

472 if self.world.rank == 0: 

473 print('The NEB initially has %d images ' % len(index_exists), 

474 '(including the end-points)') 

475 if len(index_exists) == 1: 

476 raise Exception('Only a start point exists') 

477 

478 for i in range(len(index_exists)): 

479 if i != index_exists[i]: 

480 raise Exception('Files must be ordered sequentially', 

481 'without gaps.') 

482 if self.world.rank == 0: 

483 for i in index_exists: 

484 filename_ref = self.iter_folder + \ 

485 '/%s%03diter000.traj' % (self.prefix, i) 

486 if os.path.isfile(filename_ref): 

487 try: 

488 os.rename(filename_ref, filename_ref + '.bak') 

489 except IOError: 

490 pass 

491 filename = '%s%03d.traj' % (self.prefix, i) 

492 try: 

493 shutil.copy2(filename, filename_ref) 

494 except IOError: 

495 pass 

496 # Wait for file system on all nodes is syncronized 

497 self.world.barrier() 

498 # And now lets read in the configurations 

499 for i in range(n_cur): 

500 if i in index_exists: 

501 filename = '%s%03d.traj' % (self.prefix, i) 

502 newim = read(filename) 

503 self.all_images.append(newim) 

504 else: 

505 self.all_images.append(self.all_images[0].copy()) 

506 

507 self.iteration = 0 

508 return n_cur 

509 

510 def get_energies(self): 

511 """Utility method to extract all energies and insert np.NaN at 

512 invalid images.""" 

513 energies = [] 

514 for a in self.all_images: 

515 try: 

516 energies.append(a.get_potential_energy()) 

517 except RuntimeError: 

518 energies.append(np.NaN) 

519 return energies 

520 

521 def get_energies_one_image(self, image): 

522 """Utility method to extract energy of an image and return np.NaN 

523 if invalid.""" 

524 try: 

525 energy = image.get_potential_energy() 

526 except RuntimeError: 

527 energy = np.NaN 

528 return energy 

529 

530 def get_highest_energy_index(self): 

531 """Find the index of the image with the highest energy.""" 

532 energies = self.get_energies() 

533 valid_entries = [(i, e) for i, e in enumerate(energies) if e == e] 

534 highest_energy_index = max(valid_entries, key=lambda x: x[1])[0] 

535 return highest_energy_index 

536 

537 def which_images_to_run_on(self): 

538 """Determine which set of images to do a NEB at. 

539 The priority is to first include all images without valid energies, 

540 secondly include the highest energy image.""" 

541 n_cur = len(self.all_images) 

542 energies = self.get_energies() 

543 # Find out which image is the first one missing the energy and 

544 # which is the last one missing the energy 

545 first_missing = n_cur 

546 last_missing = 0 

547 n_missing = 0 

548 for i in range(1, n_cur - 1): 

549 if energies[i] != energies[i]: 

550 n_missing += 1 

551 first_missing = min(first_missing, i) 

552 last_missing = max(last_missing, i) 

553 

554 highest_energy_index = self.get_highest_energy_index() 

555 

556 nneb = highest_energy_index - 1 - self.n_simul // 2 

557 nneb = max(nneb, 0) 

558 nneb = min(nneb, n_cur - self.n_simul - 2) 

559 nneb = min(nneb, first_missing - 1) 

560 nneb = max(nneb + self.n_simul, last_missing) - self.n_simul 

561 to_use = range(nneb, nneb + self.n_simul + 2) 

562 

563 while self.get_energies_one_image(self.all_images[to_use[0]]) != \ 

564 self.get_energies_one_image(self.all_images[to_use[0]]): 

565 to_use[0] -= 1 

566 while self.get_energies_one_image(self.all_images[to_use[-1]]) != \ 

567 self.get_energies_one_image(self.all_images[to_use[-1]]): 

568 to_use[-1] += 1 

569 

570 return to_use, (highest_energy_index in to_use[1: -1]) 

571 

572 

573class seriel_writer: 

574 def __init__(self, traj, i, num): 

575 self.traj = traj 

576 self.i = i 

577 self.num = num 

578 

579 def write(self): 

580 if self.num % (self.i + 1) == 0: 

581 self.traj.write() 

582 

583 

584def store_E_and_F_in_spc(self): 

585 """Collect the energies and forces on all nodes and store as 

586 single point calculators""" 

587 # Make sure energies and forces are known on all nodes 

588 self.get_forces() 

589 images = self.images 

590 if self.parallel: 

591 energy = np.empty(1) 

592 forces = np.empty((self.natoms, 3)) 

593 

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

595 # Determine which node is the leading for image i 

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

597 # If on this node, extract the calculated numbers 

598 if self.world.rank == root: 

599 energy[0] = images[i].get_potential_energy() 

600 forces = images[i].get_forces() 

601 # Distribute these numbers to other nodes 

602 self.world.broadcast(energy, root) 

603 self.world.broadcast(forces, root) 

604 # On all nodes, remove the calculator, keep only energy 

605 # and force in single point calculator 

606 self.images[i].calc = SinglePointCalculator( 

607 self.images[i], 

608 energy=energy[0], 

609 forces=forces)