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"""Module for calculating phonons of periodic systems.""" 

2 

3from math import pi, sqrt 

4import warnings 

5from pathlib import Path 

6 

7import numpy as np 

8import numpy.linalg as la 

9import numpy.fft as fft 

10 

11import ase 

12import ase.units as units 

13from ase.parallel import world 

14from ase.dft import monkhorst_pack 

15from ase.io.trajectory import Trajectory 

16from ase.utils.filecache import MultiFileJSONCache 

17 

18 

19class Displacement: 

20 """Abstract base class for phonon and el-ph supercell calculations. 

21 

22 Both phonons and the electron-phonon interaction in periodic systems can be 

23 calculated with the so-called finite-displacement method where the 

24 derivatives of the total energy and effective potential are obtained from 

25 finite-difference approximations, i.e. by displacing the atoms. This class 

26 provides the required functionality for carrying out the calculations for 

27 the different displacements in its ``run`` member function. 

28 

29 Derived classes must overwrite the ``__call__`` member function which is 

30 called for each atomic displacement. 

31 

32 """ 

33 

34 def __init__(self, atoms, calc=None, supercell=(1, 1, 1), name=None, 

35 delta=0.01, center_refcell=False): 

36 """Init with an instance of class ``Atoms`` and a calculator. 

37 

38 Parameters: 

39 

40 atoms: Atoms object 

41 The atoms to work on. 

42 calc: Calculator 

43 Calculator for the supercell calculation. 

44 supercell: tuple 

45 Size of supercell given by the number of repetitions (l, m, n) of 

46 the small unit cell in each direction. 

47 name: str 

48 Base name to use for files. 

49 delta: float 

50 Magnitude of displacement in Ang. 

51 center_refcell: bool 

52 Reference cell in which the atoms will be displaced. If False, then 

53 corner cell in supercell is used. If True, then cell in the center 

54 of the supercell is used. 

55 

56 """ 

57 

58 # Store atoms and calculator 

59 self.atoms = atoms 

60 self.calc = calc 

61 

62 # Displace all atoms in the unit cell by default 

63 self.indices = np.arange(len(atoms)) 

64 self.name = name 

65 self.delta = delta 

66 self.center_refcell = center_refcell 

67 self.supercell = supercell 

68 

69 self.cache = MultiFileJSONCache(self.name) 

70 

71 def define_offset(self): # Reference cell offset 

72 

73 if not self.center_refcell: 

74 # Corner cell 

75 self.offset = 0 

76 else: 

77 # Center cell 

78 N_c = self.supercell 

79 self.offset = (N_c[0] // 2 * (N_c[1] * N_c[2]) + 

80 N_c[1] // 2 * N_c[2] + 

81 N_c[2] // 2) 

82 return self.offset 

83 

84 @property # type: ignore 

85 @ase.utils.deprecated('Please use phonons.supercell instead of .N_c') 

86 def N_c(self): 

87 return self._supercell 

88 

89 @property 

90 def supercell(self): 

91 return self._supercell 

92 

93 @supercell.setter 

94 def supercell(self, supercell): 

95 assert len(supercell) == 3 

96 self._supercell = tuple(supercell) 

97 self.define_offset() 

98 self._lattice_vectors_array = self.compute_lattice_vectors() 

99 

100 @ase.utils.deprecated('Please use phonons.compute_lattice_vectors()' 

101 ' instead of .lattice_vectors()') 

102 def lattice_vectors(self): 

103 return self.compute_lattice_vectors() 

104 

105 def compute_lattice_vectors(self): 

106 """Return lattice vectors for cells in the supercell.""" 

107 # Lattice vectors -- ordered as illustrated in class docstring 

108 

109 # Lattice vectors relevative to the reference cell 

110 R_cN = np.indices(self.supercell).reshape(3, -1) 

111 N_c = np.array(self.supercell)[:, np.newaxis] 

112 if self.offset == 0: 

113 R_cN += N_c // 2 

114 R_cN %= N_c 

115 R_cN -= N_c // 2 

116 return R_cN 

117 

118 def __call__(self, *args, **kwargs): 

119 """Member function called in the ``run`` function.""" 

120 

121 raise NotImplementedError("Implement in derived classes!.") 

122 

123 def set_atoms(self, atoms): 

124 """Set the atoms to vibrate. 

125 

126 Parameters: 

127 

128 atoms: list 

129 Can be either a list of strings, ints or ... 

130 

131 """ 

132 

133 assert isinstance(atoms, list) 

134 assert len(atoms) <= len(self.atoms) 

135 

136 if isinstance(atoms[0], str): 

137 assert np.all([isinstance(atom, str) for atom in atoms]) 

138 sym_a = self.atoms.get_chemical_symbols() 

139 # List for atomic indices 

140 indices = [] 

141 for type in atoms: 

142 indices.extend([a for a, atom in enumerate(sym_a) 

143 if atom == type]) 

144 else: 

145 assert np.all([isinstance(atom, int) for atom in atoms]) 

146 indices = atoms 

147 

148 self.indices = indices 

149 

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

151 from ase.vibrations.vibrations import Displacement as VDisplacement 

152 return VDisplacement(a, i, np.sign(step), abs(step), self) 

153 

154 def run(self): 

155 """Run the calculations for the required displacements. 

156 

157 This will do a calculation for 6 displacements per atom, +-x, +-y, and 

158 +-z. Only those calculations that are not already done will be 

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

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

161 job. Otherwise the calculation for that displacement will not be done. 

162 

163 """ 

164 

165 # Atoms in the supercell -- repeated in the lattice vector directions 

166 # beginning with the last 

167 atoms_N = self.atoms * self.supercell 

168 

169 # Set calculator if provided 

170 assert self.calc is not None, "Provide calculator in __init__ method" 

171 atoms_N.calc = self.calc 

172 

173 # Do calculation on equilibrium structure 

174 eq_disp = self._disp(0, 0, 0) 

175 # with self.cache.lock(f'{self.name}.eq') as handle: 

176 with self.cache.lock(eq_disp.name) as handle: 

177 if handle is not None: 

178 output = self(atoms_N) 

179 # Write output to file 

180 if world.rank == 0: 

181 handle.save(output) 

182 

183 # Positions of atoms to be displaced in the reference cell 

184 natoms = len(self.atoms) 

185 offset = natoms * self.offset 

186 pos = atoms_N.positions[offset: offset + natoms].copy() 

187 

188 # Loop over all displacements 

189 for a in self.indices: 

190 for i in range(3): 

191 for sign in [-1, 1]: 

192 disp = self._disp(a, i, sign) 

193 # key = '%s.%d%s%s' % (self.name, a, 'xyz'[i], ' +-'[sign]) 

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

195 if handle is None: 

196 continue 

197 try: 

198 atoms_N.positions[offset + a, i] = \ 

199 pos[a, i] + sign * self.delta 

200 

201 result = self.calculate(atoms_N, disp) 

202 handle.save(result) 

203 finally: 

204 # Return to initial positions 

205 atoms_N.positions[offset + a, i] = pos[a, i] 

206 

207 def clean(self): 

208 """Delete generated files.""" 

209 if world.rank != 0: 

210 return 0 

211 

212 name = Path(self.name) 

213 

214 n = 0 

215 if name.is_dir(): 

216 for fname in name.iterdir(): 

217 fname.unlink() 

218 n += 1 

219 name.rmdir() 

220 return n 

221 

222 

223class Phonons(Displacement): 

224 r"""Class for calculating phonon modes using the finite displacement method. 

225 

226 The matrix of force constants is calculated from the finite difference 

227 approximation to the first-order derivative of the atomic forces as:: 

228 

229 2 nbj nbj 

230 nbj d E F- - F+ 

231 C = ------------ ~ ------------- , 

232 mai dR dR 2 * delta 

233 mai nbj 

234 

235 where F+/F- denotes the force in direction j on atom nb when atom ma is 

236 displaced in direction +i/-i. The force constants are related by various 

237 symmetry relations. From the definition of the force constants it must 

238 be symmetric in the three indices mai:: 

239 

240 nbj mai bj ai 

241 C = C -> C (R ) = C (-R ) . 

242 mai nbj ai n bj n 

243 

244 As the force constants can only depend on the difference between the m and 

245 n indices, this symmetry is more conveniently expressed as shown on the 

246 right hand-side. 

247 

248 The acoustic sum-rule:: 

249 

250 _ _ 

251 aj \ bj 

252 C (R ) = - ) C (R ) 

253 ai 0 /__ ai m 

254 (m, b) 

255 != 

256 (0, a) 

257 

258 Ordering of the unit cells illustrated here for a 1-dimensional system (in 

259 case ``refcell=None`` in constructor!): 

260 

261 :: 

262 

263 m = 0 m = 1 m = -2 m = -1 

264 ----------------------------------------------------- 

265 | | | | | 

266 | * b | * | * | * | 

267 | | | | | 

268 | * a | * | * | * | 

269 | | | | | 

270 ----------------------------------------------------- 

271 

272 Example: 

273 

274 >>> from ase.build import bulk 

275 >>> from ase.phonons import Phonons 

276 >>> from gpaw import GPAW, FermiDirac 

277 >>> atoms = bulk('Si', 'diamond', a=5.4) 

278 >>> calc = GPAW(kpts=(5, 5, 5), 

279 h=0.2, 

280 occupations=FermiDirac(0.)) 

281 >>> ph = Phonons(atoms, calc, supercell=(5, 5, 5)) 

282 >>> ph.run() 

283 >>> ph.read(method='frederiksen', acoustic=True) 

284 

285 """ 

286 

287 def __init__(self, *args, **kwargs): 

288 """Initialize with base class args and kwargs.""" 

289 

290 if 'name' not in kwargs: 

291 kwargs['name'] = "phonon" 

292 

293 self.deprecate_refcell(kwargs) 

294 

295 Displacement.__init__(self, *args, **kwargs) 

296 

297 # Attributes for force constants and dynamical matrix in real space 

298 self.C_N = None # in units of eV / Ang**2 

299 self.D_N = None # in units of eV / Ang**2 / amu 

300 

301 # Attributes for born charges and static dielectric tensor 

302 self.Z_avv = None 

303 self.eps_vv = None 

304 

305 @staticmethod 

306 def deprecate_refcell(kwargs: dict): 

307 if 'refcell' in kwargs: 

308 warnings.warn('Keyword refcell of Phonons is deprecated.' 

309 'Please use center_refcell (bool)', FutureWarning) 

310 kwargs['center_refcell'] = bool(kwargs['refcell']) 

311 kwargs.pop('refcell') 

312 

313 return kwargs 

314 

315 def __call__(self, atoms_N): 

316 """Calculate forces on atoms in supercell.""" 

317 return atoms_N.get_forces() 

318 

319 def calculate(self, atoms_N, disp): 

320 forces = self(atoms_N) 

321 return {'forces': forces} 

322 

323 def check_eq_forces(self): 

324 """Check maximum size of forces in the equilibrium structure.""" 

325 

326 name = f'{self.name}.eq' 

327 feq_av = self.cache[name]['forces'] 

328 

329 fmin = feq_av.max() 

330 fmax = feq_av.min() 

331 i_min = np.where(feq_av == fmin) 

332 i_max = np.where(feq_av == fmax) 

333 

334 return fmin, fmax, i_min, i_max 

335 

336 def read_born_charges(self, name=None, neutrality=True): 

337 r"""Read Born charges and dieletric tensor from JSON file. 

338 

339 The charge neutrality sum-rule:: 

340 

341 _ _ 

342 \ a 

343 ) Z = 0 

344 /__ ij 

345 a 

346 

347 Parameters: 

348 

349 neutrality: bool 

350 Restore charge neutrality condition on calculated Born effective 

351 charges. 

352 

353 """ 

354 

355 # Load file with Born charges and dielectric tensor for atoms in the 

356 # unit cell 

357 if name is None: 

358 key = '%s.born' % self.name 

359 else: 

360 key = name 

361 

362 Z_avv, eps_vv = self.cache[key] 

363 

364 # Neutrality sum-rule 

365 if neutrality: 

366 Z_mean = Z_avv.sum(0) / len(Z_avv) 

367 Z_avv -= Z_mean 

368 

369 self.Z_avv = Z_avv[self.indices] 

370 self.eps_vv = eps_vv 

371 

372 def read(self, method='Frederiksen', symmetrize=3, acoustic=True, 

373 cutoff=None, born=False, **kwargs): 

374 """Read forces from json files and calculate force constants. 

375 

376 Extra keyword arguments will be passed to ``read_born_charges``. 

377 

378 Parameters: 

379 

380 method: str 

381 Specify method for evaluating the atomic forces. 

382 symmetrize: int 

383 Symmetrize force constants (see doc string at top) when 

384 ``symmetrize != 0`` (default: 3). Since restoring the acoustic sum 

385 rule breaks the symmetry, the symmetrization must be repeated a few 

386 times until the changes a insignificant. The integer gives the 

387 number of iterations that will be carried out. 

388 acoustic: bool 

389 Restore the acoustic sum rule on the force constants. 

390 cutoff: None or float 

391 Zero elements in the dynamical matrix between atoms with an 

392 interatomic distance larger than the cutoff. 

393 born: bool 

394 Read in Born effective charge tensor and high-frequency static 

395 dielelctric tensor from file. 

396 

397 """ 

398 

399 method = method.lower() 

400 assert method in ['standard', 'frederiksen'] 

401 if cutoff is not None: 

402 cutoff = float(cutoff) 

403 

404 # Read Born effective charges and optical dielectric tensor 

405 if born: 

406 self.read_born_charges(**kwargs) 

407 

408 # Number of atoms 

409 natoms = len(self.indices) 

410 # Number of unit cells 

411 N = np.prod(self.supercell) 

412 # Matrix of force constants as a function of unit cell index in units 

413 # of eV / Ang**2 

414 C_xNav = np.empty((natoms * 3, N, natoms, 3), dtype=float) 

415 

416 # Loop over all atomic displacements and calculate force constants 

417 for i, a in enumerate(self.indices): 

418 for j, v in enumerate('xyz'): 

419 # Atomic forces for a displacement of atom a in direction v 

420 # basename = '%s.%d%s' % (self.name, a, v) 

421 basename = '%d%s' % (a, v) 

422 fminus_av = self.cache[basename + '-']['forces'] 

423 fplus_av = self.cache[basename + '+']['forces'] 

424 

425 if method == 'frederiksen': 

426 fminus_av[a] -= fminus_av.sum(0) 

427 fplus_av[a] -= fplus_av.sum(0) 

428 

429 # Finite difference derivative 

430 C_av = fminus_av - fplus_av 

431 C_av /= 2 * self.delta 

432 

433 # Slice out included atoms 

434 C_Nav = C_av.reshape((N, len(self.atoms), 3))[:, self.indices] 

435 index = 3 * i + j 

436 C_xNav[index] = C_Nav 

437 

438 # Make unitcell index the first and reshape 

439 C_N = C_xNav.swapaxes(0, 1).reshape((N,) + (3 * natoms, 3 * natoms)) 

440 

441 # Cut off before symmetry and acoustic sum rule are imposed 

442 if cutoff is not None: 

443 self.apply_cutoff(C_N, cutoff) 

444 

445 # Symmetrize force constants 

446 if symmetrize: 

447 for i in range(symmetrize): 

448 # Symmetrize 

449 C_N = self.symmetrize(C_N) 

450 # Restore acoustic sum-rule 

451 if acoustic: 

452 self.acoustic(C_N) 

453 else: 

454 break 

455 

456 # Store force constants and dynamical matrix 

457 self.C_N = C_N 

458 self.D_N = C_N.copy() 

459 

460 # Add mass prefactor 

461 m_a = self.atoms.get_masses() 

462 self.m_inv_x = np.repeat(m_a[self.indices]**-0.5, 3) 

463 M_inv = np.outer(self.m_inv_x, self.m_inv_x) 

464 for D in self.D_N: 

465 D *= M_inv 

466 

467 def symmetrize(self, C_N): 

468 """Symmetrize force constant matrix.""" 

469 

470 # Number of atoms 

471 natoms = len(self.indices) 

472 # Number of unit cells 

473 N = np.prod(self.supercell) 

474 

475 # Reshape force constants to (l, m, n) cell indices 

476 C_lmn = C_N.reshape(self.supercell + (3 * natoms, 3 * natoms)) 

477 

478 # Shift reference cell to center index 

479 if self.offset == 0: 

480 C_lmn = fft.fftshift(C_lmn, axes=(0, 1, 2)).copy() 

481 # Make force constants symmetric in indices -- in case of an even 

482 # number of unit cells don't include the first cell 

483 i, j, k = 1 - np.asarray(self.supercell) % 2 

484 C_lmn[i:, j:, k:] *= 0.5 

485 C_lmn[i:, j:, k:] += \ 

486 C_lmn[i:, j:, k:][::-1, ::-1, ::-1].transpose(0, 1, 2, 4, 3).copy() 

487 if self.offset == 0: 

488 C_lmn = fft.ifftshift(C_lmn, axes=(0, 1, 2)).copy() 

489 

490 # Change to single unit cell index shape 

491 C_N = C_lmn.reshape((N, 3 * natoms, 3 * natoms)) 

492 

493 return C_N 

494 

495 def acoustic(self, C_N): 

496 """Restore acoustic sumrule on force constants.""" 

497 

498 # Number of atoms 

499 natoms = len(self.indices) 

500 # Copy force constants 

501 C_N_temp = C_N.copy() 

502 

503 # Correct atomic diagonals of R_m = (0, 0, 0) matrix 

504 for C in C_N_temp: 

505 for a in range(natoms): 

506 for a_ in range(natoms): 

507 C_N[self.offset, 

508 3 * a: 3 * a + 3, 

509 3 * a: 3 * a + 3] -= C[3 * a: 3 * a + 3, 

510 3 * a_: 3 * a_ + 3] 

511 

512 def apply_cutoff(self, D_N, r_c): 

513 """Zero elements for interatomic distances larger than the cutoff. 

514 

515 Parameters: 

516 

517 D_N: ndarray 

518 Dynamical/force constant matrix. 

519 r_c: float 

520 Cutoff in Angstrom. 

521 

522 """ 

523 

524 # Number of atoms and primitive cells 

525 natoms = len(self.indices) 

526 N = np.prod(self.supercell) 

527 # Lattice vectors 

528 R_cN = self._lattice_vectors_array 

529 # Reshape matrix to individual atomic and cartesian dimensions 

530 D_Navav = D_N.reshape((N, natoms, 3, natoms, 3)) 

531 

532 # Cell vectors 

533 cell_vc = self.atoms.cell.transpose() 

534 # Atomic positions in reference cell 

535 pos_av = self.atoms.get_positions() 

536 

537 # Zero elements with a distance to atoms in the reference cell 

538 # larger than the cutoff 

539 for n in range(N): 

540 # Lattice vector to cell 

541 R_v = np.dot(cell_vc, R_cN[:, n]) 

542 # Atomic positions in cell 

543 posn_av = pos_av + R_v 

544 # Loop over atoms and zero elements 

545 for i, a in enumerate(self.indices): 

546 dist_a = np.sqrt(np.sum((pos_av[a] - posn_av)**2, axis=-1)) 

547 # Atoms where the distance is larger than the cufoff 

548 i_a = dist_a > r_c # np.where(dist_a > r_c) 

549 # Zero elements 

550 D_Navav[n, i, :, i_a, :] = 0.0 

551 

552 def get_force_constant(self): 

553 """Return matrix of force constants.""" 

554 

555 assert self.C_N is not None 

556 return self.C_N 

557 

558 def get_band_structure(self, path, modes=False, born=False, verbose=True): 

559 omega_kl = self.band_structure(path.kpts, modes, born, verbose) 

560 if modes: 

561 assert 0 

562 omega_kl, modes = omega_kl 

563 

564 from ase.spectrum.band_structure import BandStructure 

565 bs = BandStructure(path, energies=omega_kl[None]) 

566 return bs 

567 

568 def compute_dynamical_matrix(self, q_scaled: np.ndarray, D_N: np.ndarray): 

569 """ Computation of the dynamical matrix in momentum space D_ab(q). 

570 This is a Fourier transform from real-space dynamical matrix D_N 

571 for a given momentum vector q. 

572 

573 q_scaled: q vector in scaled coordinates. 

574 

575 D_N: the dynamical matrix in real-space. It is necessary, at least 

576 currently, to provide this matrix explicitly (rather than use 

577 self.D_N) because this matrix is modified by the Born charges 

578 contributions and these modifications are momentum (q) dependent. 

579 

580 Result: 

581 D(q): two-dimensional, complex-valued array of 

582 shape=(3 * natoms, 3 * natoms). 

583 """ 

584 # Evaluate fourier sum 

585 R_cN = self._lattice_vectors_array 

586 phase_N = np.exp(-2.j * pi * np.dot(q_scaled, R_cN)) 

587 D_q = np.sum(phase_N[:, np.newaxis, np.newaxis] * D_N, axis=0) 

588 return D_q 

589 

590 def band_structure(self, path_kc, modes=False, born=False, verbose=True): 

591 """Calculate phonon dispersion along a path in the Brillouin zone. 

592 

593 The dynamical matrix at arbitrary q-vectors is obtained by Fourier 

594 transforming the real-space force constants. In case of negative 

595 eigenvalues (squared frequency), the corresponding negative frequency 

596 is returned. 

597 

598 Frequencies and modes are in units of eV and Ang/sqrt(amu), 

599 respectively. 

600 

601 Parameters: 

602 

603 path_kc: ndarray 

604 List of k-point coordinates (in units of the reciprocal lattice 

605 vectors) specifying the path in the Brillouin zone for which the 

606 dynamical matrix will be calculated. 

607 modes: bool 

608 Returns both frequencies and modes when True. 

609 born: bool 

610 Include non-analytic part given by the Born effective charges and 

611 the static part of the high-frequency dielectric tensor. This 

612 contribution to the force constant accounts for the splitting 

613 between the LO and TO branches for q -> 0. 

614 verbose: bool 

615 Print warnings when imaginary frequncies are detected. 

616 

617 """ 

618 

619 assert self.D_N is not None 

620 if born: 

621 assert self.Z_avv is not None 

622 assert self.eps_vv is not None 

623 

624 # Dynamical matrix in real-space 

625 D_N = self.D_N 

626 

627 # Lists for frequencies and modes along path 

628 omega_kl = [] 

629 u_kl = [] 

630 

631 # Reciprocal basis vectors for use in non-analytic contribution 

632 reci_vc = 2 * pi * la.inv(self.atoms.cell) 

633 # Unit cell volume in Bohr^3 

634 vol = abs(la.det(self.atoms.cell)) / units.Bohr**3 

635 

636 for q_c in path_kc: 

637 

638 # Add non-analytic part 

639 if born: 

640 # q-vector in cartesian coordinates 

641 q_v = np.dot(reci_vc, q_c) 

642 # Non-analytic contribution to force constants in atomic units 

643 qdotZ_av = np.dot(q_v, self.Z_avv).ravel() 

644 C_na = (4 * pi * np.outer(qdotZ_av, qdotZ_av) / 

645 np.dot(q_v, np.dot(self.eps_vv, q_v)) / vol) 

646 self.C_na = C_na / units.Bohr**2 * units.Hartree 

647 # Add mass prefactor and convert to eV / (Ang^2 * amu) 

648 M_inv = np.outer(self.m_inv_x, self.m_inv_x) 

649 D_na = C_na * M_inv / units.Bohr**2 * units.Hartree 

650 self.D_na = D_na 

651 D_N = self.D_N + D_na / np.prod(self.supercell) 

652 

653 # if np.prod(self.N_c) == 1: 

654 # 

655 # q_av = np.tile(q_v, len(self.indices)) 

656 # q_xx = np.vstack([q_av]*len(self.indices)*3) 

657 # D_m += q_xx 

658 

659 # Evaluate fourier sum 

660 D_q = self.compute_dynamical_matrix(q_c, D_N) 

661 

662 if modes: 

663 omega2_l, u_xl = la.eigh(D_q, UPLO='U') 

664 # Sort eigenmodes according to eigenvalues (see below) and 

665 # multiply with mass prefactor 

666 u_lx = (self.m_inv_x[:, np.newaxis] * 

667 u_xl[:, omega2_l.argsort()]).T.copy() 

668 u_kl.append(u_lx.reshape((-1, len(self.indices), 3))) 

669 else: 

670 omega2_l = la.eigvalsh(D_q, UPLO='U') 

671 

672 # Sort eigenvalues in increasing order 

673 omega2_l.sort() 

674 # Use dtype=complex to handle negative eigenvalues 

675 omega_l = np.sqrt(omega2_l.astype(complex)) 

676 

677 # Take care of imaginary frequencies 

678 if not np.all(omega2_l >= 0.): 

679 indices = np.where(omega2_l < 0)[0] 

680 

681 if verbose: 

682 print('WARNING, %i imaginary frequencies at ' 

683 'q = (% 5.2f, % 5.2f, % 5.2f) ; (omega_q =% 5.3e*i)' 

684 % (len(indices), q_c[0], q_c[1], q_c[2], 

685 omega_l[indices][0].imag)) 

686 

687 omega_l[indices] = -1 * np.sqrt(np.abs(omega2_l[indices].real)) 

688 

689 omega_kl.append(omega_l.real) 

690 

691 # Conversion factor: sqrt(eV / Ang^2 / amu) -> eV 

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

693 omega_kl = s * np.asarray(omega_kl) 

694 

695 if modes: 

696 return omega_kl, np.asarray(u_kl) 

697 

698 return omega_kl 

699 

700 def get_dos(self, kpts=(10, 10, 10), npts=1000, delta=1e-3, indices=None): 

701 from ase.spectrum.dosdata import RawDOSData 

702 # dos = self.dos(kpts, npts, delta, indices) 

703 kpts_kc = monkhorst_pack(kpts) 

704 omega_w = self.band_structure(kpts_kc).ravel() 

705 dos = RawDOSData(omega_w, np.ones_like(omega_w)) 

706 return dos 

707 

708 def dos(self, kpts=(10, 10, 10), npts=1000, delta=1e-3, indices=None): 

709 """Calculate phonon dos as a function of energy. 

710 

711 Parameters: 

712 

713 qpts: tuple 

714 Shape of Monkhorst-Pack grid for sampling the Brillouin zone. 

715 npts: int 

716 Number of energy points. 

717 delta: float 

718 Broadening of Lorentzian line-shape in eV. 

719 indices: list 

720 If indices is not None, the atomic-partial dos for the specified 

721 atoms will be calculated. 

722 

723 """ 

724 

725 # Monkhorst-Pack grid 

726 kpts_kc = monkhorst_pack(kpts) 

727 N = np.prod(kpts) 

728 # Get frequencies 

729 omega_kl = self.band_structure(kpts_kc) 

730 # Energy axis and dos 

731 omega_e = np.linspace(0., np.amax(omega_kl) + 5e-3, num=npts) 

732 dos_e = np.zeros_like(omega_e) 

733 

734 # Sum up contribution from all q-points and branches 

735 for omega_l in omega_kl: 

736 diff_el = (omega_e[:, np.newaxis] - omega_l[np.newaxis, :])**2 

737 dos_el = 1. / (diff_el + (0.5 * delta)**2) 

738 dos_e += dos_el.sum(axis=1) 

739 

740 dos_e *= 1. / (N * pi) * 0.5 * delta 

741 

742 return omega_e, dos_e 

743 

744 def write_modes(self, q_c, branches=0, kT=units.kB * 300, born=False, 

745 repeat=(1, 1, 1), nimages=30, center=False): 

746 """Write modes to trajectory file. 

747 

748 Parameters: 

749 

750 q_c: ndarray 

751 q-vector of the modes. 

752 branches: int or list 

753 Branch index of modes. 

754 kT: float 

755 Temperature in units of eV. Determines the amplitude of the atomic 

756 displacements in the modes. 

757 born: bool 

758 Include non-analytic contribution to the force constants at q -> 0. 

759 repeat: tuple 

760 Repeat atoms (l, m, n) times in the directions of the lattice 

761 vectors. Displacements of atoms in repeated cells carry a Bloch 

762 phase factor given by the q-vector and the cell lattice vector R_m. 

763 nimages: int 

764 Number of images in an oscillation. 

765 center: bool 

766 Center atoms in unit cell if True (default: False). 

767 

768 """ 

769 

770 if isinstance(branches, int): 

771 branch_l = [branches] 

772 else: 

773 branch_l = list(branches) 

774 

775 # Calculate modes 

776 omega_l, u_l = self.band_structure([q_c], modes=True, born=born) 

777 # Repeat atoms 

778 atoms = self.atoms * repeat 

779 # Center 

780 if center: 

781 atoms.center() 

782 

783 # Here ``Na`` refers to a composite unit cell/atom dimension 

784 pos_Nav = atoms.get_positions() 

785 # Total number of unit cells 

786 N = np.prod(repeat) 

787 

788 # Corresponding lattice vectors R_m 

789 R_cN = np.indices(repeat).reshape(3, -1) 

790 # Bloch phase 

791 phase_N = np.exp(2.j * pi * np.dot(q_c, R_cN)) 

792 phase_Na = phase_N.repeat(len(self.atoms)) 

793 

794 for l in branch_l: 

795 

796 omega = omega_l[0, l] 

797 u_av = u_l[0, l] 

798 

799 # Mean displacement of a classical oscillator at temperature T 

800 u_av *= sqrt(kT) / abs(omega) 

801 

802 mode_av = np.zeros((len(self.atoms), 3), dtype=complex) 

803 # Insert slice with atomic displacements for the included atoms 

804 mode_av[self.indices] = u_av 

805 # Repeat and multiply by Bloch phase factor 

806 mode_Nav = np.vstack(N * [mode_av]) * phase_Na[:, np.newaxis] 

807 

808 with Trajectory('%s.mode.%d.traj' % (self.name, l), 'w') as traj: 

809 for x in np.linspace(0, 2 * pi, nimages, endpoint=False): 

810 atoms.set_positions((pos_Nav + np.exp(1.j * x) * 

811 mode_Nav).real) 

812 traj.write(atoms)