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 numpy as np 

2import shutil 

3import os 

4import types 

5from math import log 

6from math import exp 

7from contextlib import ExitStack 

8from pathlib import Path 

9from warnings import warn 

10 

11from ase.io import Trajectory 

12from ase.io import read 

13from ase.neb import NEB 

14from ase.optimize import BFGS 

15from ase.optimize import FIRE 

16from ase.calculators.singlepoint import SinglePointCalculator 

17import ase.parallel as mpi 

18 

19 

20class AutoNEB: 

21 """AutoNEB object. 

22 

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

24 calculations following the algorithm described in: 

25 

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

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

28 

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

30 intermediate images. 

31 

32 The stages are: 

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

34 Must at least have a relaxed starting and ending image 

35 User can supply intermediate guesses which do not need to 

36 have previously determined energies (probably from another 

37 NEB calculation with a lower level of theory) 

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

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

40 is reached 

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

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

43 are further relaxed to smooth the path 

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

45 further relaxed to smooth the path 

46 

47 Step 4 and 5-6 are optional steps! 

48 

49 Parameters: 

50 

51 attach_calculators: 

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

53 prefix: string or path 

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

55 prefix 

56 n_simul: int 

57 The number of relaxations run in parallel. 

58 n_max: int 

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

60 This number includes the two end-points. 

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

62 must be updated if the NEB is restarted. 

63 climb: boolean 

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

65 fmax: float or list of floats 

66 The maximum force along the NEB path 

67 maxsteps: int 

68 The maximum number of steps in each NEB relaxation. 

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

70 and final scan phase; 

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

72 have been inserted. 

73 k: float 

74 The spring constant along the NEB path 

75 method: str (see neb.py) 

76 Choice betweeen three method: 

77 'aseneb', standard ase NEB implementation 

78 'improvedtangent', published NEB implementation 

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

80 optimizer: object 

81 Optimizer object, defaults to FIRE 

82 Use of the valid strings 'BFGS' and 'FIRE' is deprecated. 

83 space_energy_ratio: float 

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

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

86 A space_energy_ratio set to 1 will only considder geometric gabs 

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

88 resolution. 

89 

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

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

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

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

94 n_max images have been reached. 

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

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

97 currently in the NEB. 

98 

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

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

101 """ 

102 

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

104 iter_folder='AutoNEB_iter', 

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

106 optimizer=FIRE, 

107 remove_rotation_and_translation=False, space_energy_ratio=0.5, 

108 world=None, 

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

110 self.attach_calculators = attach_calculators 

111 self.prefix = Path(prefix) 

112 self.n_simul = n_simul 

113 self.n_max = n_max 

114 self.climb = climb 

115 self.all_images = [] 

116 

117 self.parallel = parallel 

118 self.maxsteps = maxsteps 

119 self.fmax = fmax 

120 self.k = k 

121 self.method = method 

122 self.remove_rotation_and_translation = remove_rotation_and_translation 

123 self.space_energy_ratio = space_energy_ratio 

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

125 self.interpolate_method = 'idpp' 

126 print('Interpolation method not implementet.', 

127 'Using the IDPP method.') 

128 else: 

129 self.interpolate_method = interpolate_method 

130 if world is None: 

131 world = mpi.world 

132 self.world = world 

133 self.smooth_curve = smooth_curve 

134 

135 if isinstance(optimizer, str): 

136 warn('Please set optimizer as an object and not as string', 

137 FutureWarning) 

138 try: 

139 self.optimizer = { 

140 'BFGS': BFGS, 'FIRE': FIRE}[optimizer] 

141 except KeyError: 

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

143 else: 

144 self.optimizer = optimizer 

145 

146 self.iter_folder = Path(self.prefix.parent) / iter_folder 

147 self.iter_folder.mkdir(exist_ok=True) 

148 

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

150 with ExitStack() as exitstack: 

151 self._execute_one_neb(exitstack, n_cur, to_run, 

152 climb=climb, many_steps=many_steps) 

153 

154 def iter_trajpath(self, i, iiter): 

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

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

157 currently in the NEB.""" 

158 return (self.iter_folder / 

159 (self.prefix.name + f'{i:03d}iter{iiter:03d}.traj')) 

160 

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

162 climb=False, many_steps=False): 

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

164 

165 closelater = exitstack.enter_context 

166 

167 self.iteration += 1 

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

169 # neb (for reproducability purposes) 

170 if self.world.rank == 0: 

171 for i in range(n_cur): 

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

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

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

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

176 traj.write() 

177 filename_ref = self.iter_trajpath(i, self.iteration) 

178 if os.path.isfile(filename): 

179 shutil.copy2(filename, filename_ref) 

180 if self.world.rank == 0: 

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

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

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

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

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

186 method=self.method, 

187 parallel=self.parallel, 

188 remove_rotation_and_translation=self 

189 .remove_rotation_and_translation, 

190 climb=climb) 

191 

192 # Do the actual NEB calculation 

193 logpath = (self.iter_folder 

194 / f'{self.prefix.name}_log_iter{self.iteration:03d}.log') 

195 qn = closelater(self.optimizer(neb, logfile=logpath)) 

196 

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

198 if self.parallel: 

199 nneb = to_run[0] 

200 nim = len(to_run) - 2 

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

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

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

204 traj = closelater(Trajectory( 

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

206 self.all_images[j + nneb], 

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

208 )) 

209 filename_ref = self.iter_trajpath(j + nneb, self.iteration) 

210 trajhist = closelater(Trajectory( 

211 filename_ref, 'w', 

212 self.all_images[j + nneb], 

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

214 )) 

215 qn.attach(traj) 

216 qn.attach(trajhist) 

217 else: 

218 num = 1 

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

220 filename_ref = self.iter_trajpath(j, self.iteration) 

221 trajhist = closelater(Trajectory( 

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

223 )) 

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

225 

226 traj = closelater(Trajectory( 

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

228 self.all_images[j] 

229 )) 

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

231 num += 1 

232 

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

234 steps = self.maxsteps[1] 

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

236 steps = self.maxsteps[0] 

237 else: 

238 steps = self.maxsteps 

239 

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

241 fmax = self.fmax[1] 

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

243 fmax = self.fmax[0] 

244 else: 

245 fmax = self.fmax 

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

247 

248 # Remove the calculators and replace them with single 

249 # point calculators and update all the nodes for 

250 # preperration for next iteration 

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

252 neb.distribute() 

253 

254 def run(self): 

255 '''Run the AutoNEB optimization algorithm.''' 

256 n_cur = self.__initialize__() 

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

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

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

260 if self.world.rank == 0: 

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

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

263 # the largest 

264 spring_lengths = [] 

265 for j in range(n_cur - 1): 

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

267 self.all_images[j].get_positions() 

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

269 jmax = np.argmax(spring_lengths) 

270 

271 if self.world.rank == 0: 

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

273 

274 # The interpolation used to make initial guesses 

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

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

277 n_between = self.n_simul 

278 else: 

279 n_between = 1 

280 

281 toInterpolate = [self.all_images[jmax]] 

282 for i in range(n_between): 

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

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

285 

286 neb = NEB(toInterpolate) 

287 neb.interpolate(method=self.interpolate_method) 

288 

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

290 tmp += toInterpolate[1:-1] 

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

292 

293 self.all_images = tmp 

294 

295 # Expect springs to be in equilibrium 

296 k_tmp = self.k[:jmax] 

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

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

299 self.k = k_tmp 

300 

301 # Run the NEB calculation with the new image included 

302 n_cur += n_between 

303 

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

305 energies = self.get_energies() 

306 

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

308 

309 if self.world.rank == 0: 

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

311 

312 while n_non_valid_energies != 0: 

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

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

315 

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

317 to_run, climb_safe = self.which_images_to_run_on() 

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

319 

320 energies = self.get_energies() 

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

322 

323 if self.world.rank == 0: 

324 print('Finished initialisation phase.') 

325 

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

327 while n_cur < self.n_max: 

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

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

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

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

332 if self.world.rank == 0: 

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

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

335 spring_lengths = [] 

336 for j in range(n_cur - 1): 

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

338 self.all_images[j].get_positions() 

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

340 

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

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

343 tl = np.linalg.norm(total_vec) 

344 

345 fR = max(spring_lengths) / tl 

346 

347 e = self.get_energies() 

348 ed = [] 

349 emin = min(e) 

350 enorm = max(e) - emin 

351 for j in range(n_cur - 1): 

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

353 emin) / 2 / enorm 

354 ed.append(abs(delta_E)) 

355 

356 gR = max(ed) / enorm 

357 

358 if fR / gR > self.space_energy_ratio: 

359 jmax = np.argmax(spring_lengths) 

360 t = 'spring length!' 

361 else: 

362 jmax = np.argmax(ed) 

363 t = 'energy difference between neighbours!' 

364 

365 if self.world.rank == 0: 

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

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

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

369 

370 toInterpolate = [self.all_images[jmax]] 

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

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

373 

374 neb = NEB(toInterpolate) 

375 neb.interpolate(method=self.interpolate_method) 

376 

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

378 tmp += toInterpolate[1:-1] 

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

380 

381 self.all_images = tmp 

382 

383 # Expect springs to be in equilibrium 

384 k_tmp = self.k[:jmax] 

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

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

387 self.k = k_tmp 

388 

389 # Run the NEB calculation with the new image included 

390 n_cur += 1 

391 to_run, climb_safe = self.which_images_to_run_on() 

392 

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

394 

395 if self.world.rank == 0: 

396 print('n_max images has been reached') 

397 

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

399 if self.climb: 

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

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

402 if self.world.rank == 0: 

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

404 to_run, climb_safe = self.which_images_to_run_on() 

405 

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

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

408 

409 if not self.smooth_curve: 

410 return self.all_images 

411 

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

413 # gaussian distributions 

414 e = self.get_energies() 

415 peak = self.get_highest_energy_index() 

416 k_max = 10 

417 

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

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

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

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

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

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

424 

425 x1 = [] 

426 x2 = [] 

427 for i in range(peak): 

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 x1.append(np.linalg.norm(v)) 

432 

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

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

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

436 self.all_images[0].get_positions() 

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

438 k_tmp = [] 

439 for x in x1: 

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

441 for x in x2: 

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

443 

444 self.k = k_tmp 

445 # Roll back to start from the top-point 

446 if self.world.rank == 0: 

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

448 highest_energy_index = self.get_highest_energy_index() 

449 nneb = highest_energy_index - self.n_simul - 1 

450 while nneb >= 0: 

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

452 climb=False) 

453 nneb -= 1 

454 

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

456 nneb = self.get_highest_energy_index() 

457 

458 if self.world.rank == 0: 

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

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

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

462 climb=False) 

463 nneb += 1 

464 return self.all_images 

465 

466 def __initialize__(self): 

467 '''Load files from the filesystem.''' 

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

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

470 'was found. Should contain initial image') 

471 

472 # Find the images that exist 

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

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

475 

476 n_cur = index_exists[-1] + 1 

477 

478 if self.world.rank == 0: 

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

480 '(including the end-points)') 

481 if len(index_exists) == 1: 

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

483 

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

485 if i != index_exists[i]: 

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

487 'without gaps.') 

488 if self.world.rank == 0: 

489 for i in index_exists: 

490 filename_ref = self.iter_trajpath(i, 0) 

491 if os.path.isfile(filename_ref): 

492 try: 

493 os.rename(filename_ref, str(filename_ref) + '.bak') 

494 except IOError: 

495 pass 

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

497 try: 

498 shutil.copy2(filename, filename_ref) 

499 except IOError: 

500 pass 

501 # Wait for file system on all nodes is syncronized 

502 self.world.barrier() 

503 # And now lets read in the configurations 

504 for i in range(n_cur): 

505 if i in index_exists: 

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

507 newim = read(filename) 

508 self.all_images.append(newim) 

509 else: 

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

511 

512 self.iteration = 0 

513 return n_cur 

514 

515 def get_energies(self): 

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

517 invalid images.""" 

518 energies = [] 

519 for a in self.all_images: 

520 try: 

521 energies.append(a.get_potential_energy()) 

522 except RuntimeError: 

523 energies.append(np.NaN) 

524 return energies 

525 

526 def get_energies_one_image(self, image): 

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

528 if invalid.""" 

529 try: 

530 energy = image.get_potential_energy() 

531 except RuntimeError: 

532 energy = np.NaN 

533 return energy 

534 

535 def get_highest_energy_index(self): 

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

537 energies = self.get_energies() 

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

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

540 return highest_energy_index 

541 

542 def which_images_to_run_on(self): 

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

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

545 secondly include the highest energy image.""" 

546 n_cur = len(self.all_images) 

547 energies = self.get_energies() 

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

549 # which is the last one missing the energy 

550 first_missing = n_cur 

551 last_missing = 0 

552 n_missing = 0 

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

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

555 n_missing += 1 

556 first_missing = min(first_missing, i) 

557 last_missing = max(last_missing, i) 

558 

559 highest_energy_index = self.get_highest_energy_index() 

560 

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

562 nneb = max(nneb, 0) 

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

564 nneb = min(nneb, first_missing - 1) 

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

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

567 

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

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

570 to_use[0] -= 1 

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

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

573 to_use[-1] += 1 

574 

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

576 

577 

578class seriel_writer: 

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

580 self.traj = traj 

581 self.i = i 

582 self.num = num 

583 

584 def write(self): 

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

586 self.traj.write() 

587 

588 

589def store_E_and_F_in_spc(self): 

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

591 single point calculators""" 

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

593 self.get_forces() 

594 images = self.images 

595 if self.parallel: 

596 energy = np.empty(1) 

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

598 

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

600 # Determine which node is the leading for image i 

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

602 # If on this node, extract the calculated numbers 

603 if self.world.rank == root: 

604 forces = images[i].get_forces() 

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

606 # Distribute these numbers to other nodes 

607 self.world.broadcast(energy, root) 

608 self.world.broadcast(forces, root) 

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

610 # and force in single point calculator 

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

612 self.images[i], 

613 energy=energy[0], 

614 forces=forces)