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

1"""A class for computing vibrational modes""" 

2 

3from math import pi, sqrt, log 

4import sys 

5 

6import numpy as np 

7from pathlib import Path 

8 

9import ase.units as units 

10import ase.io 

11from ase.parallel import world, paropen 

12 

13from ase.utils.filecache import get_json_cache 

14from .data import VibrationsData 

15 

16from collections import namedtuple 

17 

18 

19class AtomicDisplacements: 

20 def _disp(self, a, i, step): 

21 if isinstance(i, str): # XXX Simplify by removing this. 

22 i = 'xyz'.index(i) 

23 return Displacement(a, i, np.sign(step), abs(step), self) 

24 

25 def _eq_disp(self): 

26 return self._disp(0, 0, 0) 

27 

28 @property 

29 def ndof(self): 

30 return 3 * len(self.indices) 

31 

32 

33class Displacement(namedtuple('Displacement', ['a', 'i', 'sign', 'ndisp', 

34 'vib'])): 

35 @property 

36 def name(self): 

37 if self.sign == 0: 

38 return 'eq' 

39 

40 axisname = 'xyz'[self.i] 

41 dispname = self.ndisp * ' +-'[self.sign] 

42 return f'{self.a}{axisname}{dispname}' 

43 

44 @property 

45 def _cached(self): 

46 return self.vib.cache[self.name] 

47 

48 def forces(self): 

49 return self._cached['forces'].copy() 

50 

51 @property 

52 def step(self): 

53 return self.ndisp * self.sign * self.vib.delta 

54 

55 # XXX dipole only valid for infrared 

56 def dipole(self): 

57 return self._cached['dipole'].copy() 

58 

59 # XXX below stuff only valid for TDDFT excitation stuff 

60 def save_ov_nn(self, ov_nn): 

61 np.save(self.name + '.ov', ov_nn) 

62 

63 def load_ov_nn(self): 

64 return np.load(self.name + '.ov.npy') 

65 

66 @property 

67 def _exname(self): 

68 return Path(self.vib.exname) / f'ex.{self.name}{self.vib.exext}' 

69 

70 def calculate_and_save_static_polarizability(self, atoms): 

71 exobj = self.vib._new_exobj() 

72 excitation_data = exobj.calculate(atoms) 

73 np.savetxt(self._exname, excitation_data) 

74 

75 def load_static_polarizability(self): 

76 return np.loadtxt(self._exname) 

77 

78 def read_exobj(self): 

79 # XXX each exobj should allow for self._exname as Path 

80 return self.vib.read_exobj(str(self._exname)) 

81 

82 def calculate_and_save_exlist(self, atoms): 

83 # exo = self.vib._new_exobj() 

84 excalc = self.vib._new_exobj() 

85 exlist = excalc.calculate(atoms) 

86 # XXX each exobj should allow for self._exname as Path 

87 exlist.write(str(self._exname)) 

88 

89 

90class Vibrations(AtomicDisplacements): 

91 """Class for calculating vibrational modes using finite difference. 

92 

93 The vibrational modes are calculated from a finite difference 

94 approximation of the Hessian matrix. 

95 

96 The *summary()*, *get_energies()* and *get_frequencies()* methods all take 

97 an optional *method* keyword. Use method='Frederiksen' to use the method 

98 described in: 

99 

100 T. Frederiksen, M. Paulsson, M. Brandbyge, A. P. Jauho: 

101 "Inelastic transport theory from first-principles: methodology and 

102 applications for nanoscale devices", Phys. Rev. B 75, 205413 (2007) 

103 

104 atoms: Atoms object 

105 The atoms to work on. 

106 indices: list of int 

107 List of indices of atoms to vibrate. Default behavior is 

108 to vibrate all atoms. 

109 name: str 

110 Name to use for files. 

111 delta: float 

112 Magnitude of displacements. 

113 nfree: int 

114 Number of displacements per atom and cartesian coordinate, 2 and 4 are 

115 supported. Default is 2 which will displace each atom +delta and 

116 -delta for each cartesian coordinate. 

117 

118 Example: 

119 

120 >>> from ase import Atoms 

121 >>> from ase.calculators.emt import EMT 

122 >>> from ase.optimize import BFGS 

123 >>> from ase.vibrations import Vibrations 

124 >>> n2 = Atoms('N2', [(0, 0, 0), (0, 0, 1.1)], 

125 ... calculator=EMT()) 

126 >>> BFGS(n2).run(fmax=0.01) 

127 BFGS: 0 16:01:21 0.440339 3.2518 

128 BFGS: 1 16:01:21 0.271928 0.8211 

129 BFGS: 2 16:01:21 0.263278 0.1994 

130 BFGS: 3 16:01:21 0.262777 0.0088 

131 >>> vib = Vibrations(n2) 

132 >>> vib.run() 

133 >>> vib.summary() 

134 --------------------- 

135 # meV cm^-1 

136 --------------------- 

137 0 0.0 0.0 

138 1 0.0 0.0 

139 2 0.0 0.0 

140 3 1.4 11.5 

141 4 1.4 11.5 

142 5 152.7 1231.3 

143 --------------------- 

144 Zero-point energy: 0.078 eV 

145 >>> vib.write_mode(-1) # write last mode to trajectory file 

146 

147 """ 

148 

149 def __init__(self, atoms, indices=None, name='vib', delta=0.01, nfree=2): 

150 assert nfree in [2, 4] 

151 self.atoms = atoms 

152 self.calc = atoms.calc 

153 if indices is None: 

154 indices = range(len(atoms)) 

155 if len(indices) != len(set(indices)): 

156 raise ValueError( 

157 'one (or more) indices included more than once') 

158 self.indices = np.asarray(indices) 

159 

160 self.delta = delta 

161 self.nfree = nfree 

162 self.H = None 

163 self.ir = None 

164 self._vibrations = None 

165 

166 self.cache = get_json_cache(name) 

167 

168 @property 

169 def name(self): 

170 return str(self.cache.directory) 

171 

172 def run(self): 

173 """Run the vibration calculations. 

174 

175 This will calculate the forces for 6 displacements per atom +/-x, 

176 +/-y, +/-z. Only those calculations that are not already done will be 

177 started. Be aware that an interrupted calculation may produce an empty 

178 file (ending with .json), which must be deleted before restarting the 

179 job. Otherwise the forces will not be calculated for that 

180 displacement. 

181 

182 Note that the calculations for the different displacements can be done 

183 simultaneously by several independent processes. This feature relies 

184 on the existence of files and the subsequent creation of the file in 

185 case it is not found. 

186 

187 If the program you want to use does not have a calculator in ASE, use 

188 ``iterdisplace`` to get all displaced structures and calculate the 

189 forces on your own. 

190 """ 

191 

192 if not self.cache.writable: 

193 raise RuntimeError( 

194 'Cannot run calculation. ' 

195 'Cache must be removed or split in order ' 

196 'to have only one sort of data structure at a time.') 

197 

198 self._check_old_pickles() 

199 

200 for disp, atoms in self.iterdisplace(inplace=True): 

201 with self.cache.lock(disp.name) as handle: 

202 if handle is None: 

203 continue 

204 

205 result = self.calculate(atoms, disp) 

206 

207 if world.rank == 0: 

208 handle.save(result) 

209 

210 def _check_old_pickles(self): 

211 from pathlib import Path 

212 eq_pickle_path = Path(f'{self.name}.eq.pckl') 

213 pickle2json_instructions = f"""\ 

214Found old pickle files such as {eq_pickle_path}. \ 

215Please remove them and recalculate or run \ 

216"python -m ase.vibrations.pickle2json --help".""" 

217 if len(self.cache) == 0 and eq_pickle_path.exists(): 

218 raise RuntimeError(pickle2json_instructions) 

219 

220 def iterdisplace(self, inplace=False): 

221 """Yield name and atoms object for initial and displaced structures. 

222 

223 Use this to export the structures for each single-point calculation 

224 to an external program instead of using ``run()``. Then save the 

225 calculated gradients to <name>.json and continue using this instance. 

226 """ 

227 # XXX change of type of disp 

228 atoms = self.atoms if inplace else self.atoms.copy() 

229 displacements = self.displacements() 

230 eq_disp = next(displacements) 

231 assert eq_disp.name == 'eq' 

232 yield eq_disp, atoms 

233 

234 for disp in displacements: 

235 if not inplace: 

236 atoms = self.atoms.copy() 

237 pos0 = atoms.positions[disp.a, disp.i] 

238 atoms.positions[disp.a, disp.i] += disp.step 

239 yield disp, atoms 

240 

241 if inplace: 

242 atoms.positions[disp.a, disp.i] = pos0 

243 

244 def iterimages(self): 

245 """Yield initial and displaced structures.""" 

246 for name, atoms in self.iterdisplace(): 

247 yield atoms 

248 

249 def _iter_ai(self): 

250 for a in self.indices: 

251 for i in range(3): 

252 yield a, i 

253 

254 def displacements(self): 

255 yield self._eq_disp() 

256 

257 for a, i in self._iter_ai(): 

258 for sign in [-1, 1]: 

259 for ndisp in range(1, self.nfree // 2 + 1): 

260 yield self._disp(a, i, sign * ndisp) 

261 

262 def calculate(self, atoms, disp): 

263 results = {} 

264 results['forces'] = self.calc.get_forces(atoms) 

265 

266 if self.ir: 

267 results['dipole'] = self.calc.get_dipole_moment(atoms) 

268 

269 return results 

270 

271 def clean(self, empty_files=False, combined=True): 

272 """Remove json-files. 

273 

274 Use empty_files=True to remove only empty files and 

275 combined=False to not remove the combined file. 

276 

277 """ 

278 

279 if world.rank != 0: 

280 return 0 

281 

282 if empty_files: 

283 return self.cache.strip_empties() # XXX Fails on combined cache 

284 

285 nfiles = self.cache.filecount() 

286 self.cache.clear() 

287 return nfiles 

288 

289 def combine(self): 

290 """Combine json-files to one file ending with '.all.json'. 

291 

292 The other json-files will be removed in order to have only one sort 

293 of data structure at a time. 

294 

295 """ 

296 nelements_before = self.cache.filecount() 

297 self.cache = self.cache.combine() 

298 return nelements_before 

299 

300 def split(self): 

301 """Split combined json-file. 

302 

303 The combined json-file will be removed in order to have only one 

304 sort of data structure at a time. 

305 

306 """ 

307 count = self.cache.filecount() 

308 self.cache = self.cache.split() 

309 return count 

310 

311 def read(self, method='standard', direction='central'): 

312 self.method = method.lower() 

313 self.direction = direction.lower() 

314 assert self.method in ['standard', 'frederiksen'] 

315 assert self.direction in ['central', 'forward', 'backward'] 

316 

317 n = 3 * len(self.indices) 

318 H = np.empty((n, n)) 

319 r = 0 

320 

321 eq_disp = self._eq_disp() 

322 

323 if direction != 'central': 

324 feq = eq_disp.forces() 

325 

326 for a, i in self._iter_ai(): 

327 disp_minus = self._disp(a, i, -1) 

328 disp_plus = self._disp(a, i, 1) 

329 

330 fminus = disp_minus.forces() 

331 fplus = disp_plus.forces() 

332 if self.method == 'frederiksen': 

333 fminus[a] -= fminus.sum(0) 

334 fplus[a] -= fplus.sum(0) 

335 if self.nfree == 4: 

336 fminusminus = self._disp(a, i, -2).forces() 

337 fplusplus = self._disp(a, i, 2).forces() 

338 if self.method == 'frederiksen': 

339 fminusminus[a] -= fminusminus.sum(0) 

340 fplusplus[a] -= fplusplus.sum(0) 

341 if self.direction == 'central': 

342 if self.nfree == 2: 

343 H[r] = .5 * (fminus - fplus)[self.indices].ravel() 

344 else: 

345 assert self.nfree == 4 

346 H[r] = H[r] = (-fminusminus + 

347 8 * fminus - 

348 8 * fplus + 

349 fplusplus)[self.indices].ravel() / 12.0 

350 elif self.direction == 'forward': 

351 H[r] = (feq - fplus)[self.indices].ravel() 

352 else: 

353 assert self.direction == 'backward' 

354 H[r] = (fminus - feq)[self.indices].ravel() 

355 H[r] /= 2 * self.delta 

356 r += 1 

357 H += H.copy().T 

358 self.H = H 

359 masses = self.atoms.get_masses() 

360 if any(masses[self.indices] == 0): 

361 raise RuntimeError('Zero mass encountered in one or more of ' 

362 'the vibrated atoms. Use Atoms.set_masses()' 

363 ' to set all masses to non-zero values.') 

364 

365 self.im = np.repeat(masses[self.indices]**-0.5, 3) 

366 self._vibrations = self.get_vibrations(read_cache=False) 

367 

368 omega2, modes = np.linalg.eigh(self.im[:, None] * H * self.im) 

369 self.modes = modes.T.copy() 

370 

371 # Conversion factor: 

372 s = units._hbar * 1e10 / sqrt(units._e * units._amu) 

373 self.hnu = s * omega2.astype(complex)**0.5 

374 

375 def get_vibrations(self, method='standard', direction='central', 

376 read_cache=True, **kw): 

377 """Get vibrations as VibrationsData object 

378 

379 If read() has not yet been called, this will be called to assemble data 

380 from the outputs of run(). Most of the arguments to this function are 

381 options to be passed to read() in this case. 

382 

383 Args: 

384 method (str): Calculation method passed to read() 

385 direction (str): Finite-difference scheme passed to read() 

386 read_cache (bool): The VibrationsData object will be cached for 

387 quick access. Set False to force regeneration of the cache with 

388 the current atoms/Hessian/indices data. 

389 **kw: Any remaining keyword arguments are passed to read() 

390 

391 Returns: 

392 VibrationsData 

393 

394 """ 

395 if read_cache and (self._vibrations is not None): 

396 return self._vibrations 

397 

398 else: 

399 if (self.H is None or method.lower() != self.method or 

400 direction.lower() != self.direction): 

401 self.read(method, direction, **kw) 

402 

403 return VibrationsData.from_2d(self.atoms, self.H, 

404 indices=self.indices) 

405 

406 def get_energies(self, method='standard', direction='central', **kw): 

407 """Get vibration energies in eV.""" 

408 return self.get_vibrations(method=method, 

409 direction=direction, **kw).get_energies() 

410 

411 def get_frequencies(self, method='standard', direction='central'): 

412 """Get vibration frequencies in cm^-1.""" 

413 return self.get_vibrations(method=method, 

414 direction=direction).get_frequencies() 

415 

416 def summary(self, method='standard', direction='central', freq=None, 

417 log=sys.stdout): 

418 if freq is not None: 

419 energies = freq * units.invcm 

420 else: 

421 energies = self.get_energies(method=method, direction=direction) 

422 

423 summary_lines = VibrationsData._tabulate_from_energies(energies) 

424 log_text = '\n'.join(summary_lines) + '\n' 

425 

426 if isinstance(log, str): 

427 with paropen(log, 'a') as log_file: 

428 log_file.write(log_text) 

429 else: 

430 log.write(log_text) 

431 

432 def get_zero_point_energy(self, freq=None): 

433 if freq: 

434 raise NotImplementedError() 

435 return self.get_vibrations().get_zero_point_energy() 

436 

437 def get_mode(self, n): 

438 """Get mode number .""" 

439 return self.get_vibrations().get_modes(all_atoms=True)[n] 

440 

441 def write_mode(self, n=None, kT=units.kB * 300, nimages=30): 

442 """Write mode number n to trajectory file. If n is not specified, 

443 writes all non-zero modes.""" 

444 if n is None: 

445 for index, energy in enumerate(self.get_energies()): 

446 if abs(energy) > 1e-5: 

447 self.write_mode(n=index, kT=kT, nimages=nimages) 

448 return 

449 

450 else: 

451 n %= len(self.get_energies()) 

452 

453 with ase.io.Trajectory('%s.%d.traj' % (self.name, n), 'w') as traj: 

454 for image in (self.get_vibrations() 

455 .iter_animated_mode(n, 

456 temperature=kT, frames=nimages)): 

457 traj.write(image) 

458 

459 def show_as_force(self, n, scale=0.2, show=True): 

460 return self.get_vibrations().show_as_force(n, scale=scale, show=show) 

461 

462 def write_jmol(self): 

463 """Writes file for viewing of the modes with jmol.""" 

464 

465 with open(self.name + '.xyz', 'w') as fd: 

466 self._write_jmol(fd) 

467 

468 def _write_jmol(self, fd): 

469 symbols = self.atoms.get_chemical_symbols() 

470 freq = self.get_frequencies() 

471 for n in range(3 * len(self.indices)): 

472 fd.write('%6d\n' % len(self.atoms)) 

473 

474 if freq[n].imag != 0: 

475 c = 'i' 

476 freq[n] = freq[n].imag 

477 

478 else: 

479 freq[n] = freq[n].real 

480 c = ' ' 

481 

482 fd.write('Mode #%d, f = %.1f%s cm^-1' 

483 % (n, float(freq[n].real), c)) 

484 

485 if self.ir: 

486 fd.write(', I = %.4f (D/Å)^2 amu^-1.\n' % self.intensities[n]) 

487 else: 

488 fd.write('.\n') 

489 

490 mode = self.get_mode(n) 

491 for i, pos in enumerate(self.atoms.positions): 

492 fd.write('%2s %12.5f %12.5f %12.5f %12.5f %12.5f %12.5f\n' % 

493 (symbols[i], pos[0], pos[1], pos[2], 

494 mode[i, 0], mode[i, 1], mode[i, 2])) 

495 

496 def fold(self, frequencies, intensities, 

497 start=800.0, end=4000.0, npts=None, width=4.0, 

498 type='Gaussian', normalize=False): 

499 """Fold frequencies and intensities within the given range 

500 and folding method (Gaussian/Lorentzian). 

501 The energy unit is cm^-1. 

502 normalize=True ensures the integral over the peaks to give the 

503 intensity. 

504 """ 

505 

506 lctype = type.lower() 

507 assert lctype in ['gaussian', 'lorentzian'] 

508 if not npts: 

509 npts = int((end - start) / width * 10 + 1) 

510 prefactor = 1 

511 if lctype == 'lorentzian': 

512 intensities = intensities * width * pi / 2. 

513 if normalize: 

514 prefactor = 2. / width / pi 

515 else: 

516 sigma = width / 2. / sqrt(2. * log(2.)) 

517 if normalize: 

518 prefactor = 1. / sigma / sqrt(2 * pi) 

519 

520 # Make array with spectrum data 

521 spectrum = np.empty(npts) 

522 energies = np.linspace(start, end, npts) 

523 for i, energy in enumerate(energies): 

524 energies[i] = energy 

525 if lctype == 'lorentzian': 

526 spectrum[i] = (intensities * 0.5 * width / pi / 

527 ((frequencies - energy)**2 + 

528 0.25 * width**2)).sum() 

529 else: 

530 spectrum[i] = (intensities * 

531 np.exp(-(frequencies - energy)**2 / 

532 2. / sigma**2)).sum() 

533 return [energies, prefactor * spectrum] 

534 

535 def write_dos(self, out='vib-dos.dat', start=800, end=4000, 

536 npts=None, width=10, 

537 type='Gaussian', method='standard', direction='central'): 

538 """Write out the vibrational density of states to file. 

539 

540 First column is the wavenumber in cm^-1, the second column the 

541 folded vibrational density of states. 

542 Start and end points, and width of the Gaussian/Lorentzian 

543 should be given in cm^-1.""" 

544 frequencies = self.get_frequencies(method, direction).real 

545 intensities = np.ones(len(frequencies)) 

546 energies, spectrum = self.fold(frequencies, intensities, 

547 start, end, npts, width, type) 

548 

549 # Write out spectrum in file. 

550 outdata = np.empty([len(energies), 2]) 

551 outdata.T[0] = energies 

552 outdata.T[1] = spectrum 

553 

554 with open(out, 'w') as fd: 

555 fd.write('# %s folded, width=%g cm^-1\n' % (type.title(), width)) 

556 fd.write('# [cm^-1] arbitrary\n') 

557 for row in outdata: 

558 fd.write('%.3f %15.5e\n' % 

559 (row[0], row[1]))