Coverage for /builds/ase/ase/ase/phonons.py: 83.04%
336 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3"""Module for calculating phonons of periodic systems."""
5import warnings
6from math import pi, sqrt
7from pathlib import Path
9import numpy as np
10import numpy.fft as fft
11import numpy.linalg as la
13import ase
14import ase.units as units
15from ase.dft import monkhorst_pack
16from ase.io.trajectory import Trajectory
17from ase.parallel import world
18from ase.utils import deprecated
19from ase.utils.filecache import MultiFileJSONCache
22class Displacement:
23 """Abstract base class for phonon and el-ph supercell calculations.
25 Both phonons and the electron-phonon interaction in periodic systems can be
26 calculated with the so-called finite-displacement method where the
27 derivatives of the total energy and effective potential are obtained from
28 finite-difference approximations, i.e. by displacing the atoms. This class
29 provides the required functionality for carrying out the calculations for
30 the different displacements in its ``run`` member function.
32 Derived classes must overwrite the ``__call__`` member function which is
33 called for each atomic displacement.
35 """
37 def __init__(self, atoms, calc=None, supercell=(1, 1, 1), name=None,
38 delta=0.01, center_refcell=False, comm=None):
39 """Init with an instance of class ``Atoms`` and a calculator.
41 Parameters:
43 atoms: Atoms object
44 The atoms to work on.
45 calc: Calculator
46 Calculator for the supercell calculation.
47 supercell: tuple
48 Size of supercell given by the number of repetitions (l, m, n) of
49 the small unit cell in each direction.
50 name: str
51 Base name to use for files.
52 delta: float
53 Magnitude of displacement in Ang.
54 center_refcell: bool
55 Reference cell in which the atoms will be displaced. If False, then
56 corner cell in supercell is used. If True, then cell in the center
57 of the supercell is used.
58 comm: communicator
59 MPI communicator for the phonon calculation.
60 Default is to use world.
61 """
63 # Store atoms and calculator
64 self.atoms = atoms
65 self.calc = calc
67 # Displace all atoms in the unit cell by default
68 self.indices = np.arange(len(atoms))
69 self.name = name
70 self.delta = delta
71 self.center_refcell = center_refcell
72 self.supercell = supercell
74 if comm is None:
75 comm = world
76 self.comm = comm
78 self.cache = MultiFileJSONCache(self.name)
80 def define_offset(self): # Reference cell offset
82 if not self.center_refcell:
83 # Corner cell
84 self.offset = 0
85 else:
86 # Center cell
87 N_c = self.supercell
88 self.offset = (N_c[0] // 2 * (N_c[1] * N_c[2]) +
89 N_c[1] // 2 * N_c[2] +
90 N_c[2] // 2)
91 return self.offset
93 @property
94 @ase.utils.deprecated('Please use phonons.supercell instead of .N_c')
95 def N_c(self):
96 return self._supercell
98 @property
99 def supercell(self):
100 return self._supercell
102 @supercell.setter
103 def supercell(self, supercell):
104 assert len(supercell) == 3
105 self._supercell = tuple(supercell)
106 self.define_offset()
107 self._lattice_vectors_array = self.compute_lattice_vectors()
109 @ase.utils.deprecated('Please use phonons.compute_lattice_vectors()'
110 ' instead of .lattice_vectors()')
111 def lattice_vectors(self):
112 return self.compute_lattice_vectors()
114 def compute_lattice_vectors(self):
115 """Return lattice vectors for cells in the supercell."""
116 # Lattice vectors -- ordered as illustrated in class docstring
118 # Lattice vectors relevative to the reference cell
119 R_cN = np.indices(self.supercell).reshape(3, -1)
120 N_c = np.array(self.supercell)[:, np.newaxis]
121 if self.offset == 0:
122 R_cN += N_c // 2
123 R_cN %= N_c
124 R_cN -= N_c // 2
125 return R_cN
127 def __call__(self, *args, **kwargs):
128 """Member function called in the ``run`` function."""
130 raise NotImplementedError("Implement in derived classes!.")
132 def set_atoms(self, atoms):
133 """Set the atoms to vibrate.
135 Parameters:
137 atoms: list
138 Can be either a list of strings, ints or ...
140 """
142 assert isinstance(atoms, list)
143 assert len(atoms) <= len(self.atoms)
145 if isinstance(atoms[0], str):
146 assert np.all([isinstance(atom, str) for atom in atoms])
147 sym_a = self.atoms.get_chemical_symbols()
148 # List for atomic indices
149 indices = []
150 for type in atoms:
151 indices.extend([a for a, atom in enumerate(sym_a)
152 if atom == type])
153 else:
154 assert np.all([isinstance(atom, int) for atom in atoms])
155 indices = atoms
157 self.indices = indices
159 def _eq_disp(self):
160 return self._disp(0, 0, 0)
162 def _disp(self, a, i, step):
163 from ase.vibrations.vibrations import Displacement as VDisplacement
164 return VDisplacement(a, i, np.sign(step), abs(step), self)
166 def run(self):
167 """Run the calculations for the required displacements.
169 This will do a calculation for 6 displacements per atom, +-x, +-y, and
170 +-z. Only those calculations that are not already done will be
171 started. Be aware that an interrupted calculation may produce an empty
172 file (ending with .json), which must be deleted before restarting the
173 job. Otherwise the calculation for that displacement will not be done.
175 """
177 # Atoms in the supercell -- repeated in the lattice vector directions
178 # beginning with the last
179 atoms_N = self.atoms * self.supercell
181 # Set calculator if provided
182 assert self.calc is not None, "Provide calculator in __init__ method"
183 atoms_N.calc = self.calc
185 # Do calculation on equilibrium structure
186 eq_disp = self._eq_disp()
187 with self.cache.lock(eq_disp.name) as handle:
188 if handle is not None:
189 output = self.calculate(atoms_N, eq_disp)
190 handle.save(output)
192 # Positions of atoms to be displaced in the reference cell
193 natoms = len(self.atoms)
194 offset = natoms * self.offset
195 pos = atoms_N.positions[offset: offset + natoms].copy()
197 # Loop over all displacements
198 for a in self.indices:
199 for i in range(3):
200 for sign in [-1, 1]:
201 disp = self._disp(a, i, sign)
202 with self.cache.lock(disp.name) as handle:
203 if handle is None:
204 continue
205 try:
206 atoms_N.positions[offset + a, i] = \
207 pos[a, i] + sign * self.delta
209 result = self.calculate(atoms_N, disp)
210 handle.save(result)
211 finally:
212 # Return to initial positions
213 atoms_N.positions[offset + a, i] = pos[a, i]
215 self.comm.barrier()
217 def clean(self):
218 """Delete generated files."""
219 if self.comm.rank == 0:
220 nfiles = self._clean()
221 else:
222 nfiles = 0
223 self.comm.barrier()
224 return nfiles
226 def _clean(self):
227 name = Path(self.name)
229 nfiles = 0
230 if name.is_dir():
231 for fname in name.iterdir():
232 fname.unlink()
233 nfiles += 1
234 name.rmdir()
235 return nfiles
238class Phonons(Displacement):
239 r"""Class for calculating phonon modes using the finite displacement method.
241 The matrix of force constants is calculated from the finite difference
242 approximation to the first-order derivative of the atomic forces as::
244 2 nbj nbj
245 nbj d E F- - F+
246 C = ------------ ~ ------------- ,
247 mai dR dR 2 * delta
248 mai nbj
250 where F+/F- denotes the force in direction j on atom nb when atom ma is
251 displaced in direction +i/-i. The force constants are related by various
252 symmetry relations. From the definition of the force constants it must
253 be symmetric in the three indices mai::
255 nbj mai bj ai
256 C = C -> C (R ) = C (-R ) .
257 mai nbj ai n bj n
259 As the force constants can only depend on the difference between the m and
260 n indices, this symmetry is more conveniently expressed as shown on the
261 right hand-side.
263 The acoustic sum-rule::
265 _ _
266 aj \ bj
267 C (R ) = - ) C (R )
268 ai 0 /__ ai m
269 (m, b)
270 !=
271 (0, a)
273 Ordering of the unit cells illustrated here for a 1-dimensional system (in
274 case ``refcell=None`` in constructor!):
276 ::
278 m = 0 m = 1 m = -2 m = -1
279 -----------------------------------------------------
280 | | | | |
281 | * b | * | * | * |
282 | | | | |
283 | * a | * | * | * |
284 | | | | |
285 -----------------------------------------------------
287 Example:
289 >>> from ase.build import bulk
290 >>> from ase.phonons import Phonons
291 >>> from gpaw import GPAW, FermiDirac
293 >>> atoms = bulk('Si', 'diamond', a=5.4)
294 >>> calc = GPAW(mode='fd',
295 ... kpts=(5, 5, 5),
296 ... h=0.2,
297 ... occupations=FermiDirac(0.))
298 >>> ph = Phonons(atoms, calc, supercell=(5, 5, 5))
299 >>> ph.run()
300 >>> ph.read(method='frederiksen', acoustic=True)
302 """
304 def __init__(self, *args, **kwargs):
305 """Initialize with base class args and kwargs."""
307 if 'name' not in kwargs:
308 kwargs['name'] = "phonon"
310 self.deprecate_refcell(kwargs)
312 Displacement.__init__(self, *args, **kwargs)
314 # Attributes for force constants and dynamical matrix in real space
315 self.C_N = None # in units of eV / Ang**2
316 self.D_N = None # in units of eV / Ang**2 / amu
318 # Attributes for born charges and static dielectric tensor
319 self.Z_avv = None
320 self.eps_vv = None
322 @staticmethod
323 def deprecate_refcell(kwargs: dict):
324 if 'refcell' in kwargs:
325 warnings.warn('Keyword refcell of Phonons is deprecated.'
326 'Please use center_refcell (bool)', FutureWarning)
327 kwargs['center_refcell'] = bool(kwargs['refcell'])
328 kwargs.pop('refcell')
330 return kwargs
332 def __call__(self, atoms_N):
333 """Calculate forces on atoms in supercell."""
334 return atoms_N.get_forces()
336 def calculate(self, atoms_N, disp):
337 forces = self(atoms_N)
338 return {'forces': forces}
340 def check_eq_forces(self):
341 """Check maximum size of forces in the equilibrium structure."""
343 eq_disp = self._eq_disp()
344 feq_av = self.cache[eq_disp.name]['forces']
346 fmin = feq_av.min()
347 fmax = feq_av.max()
348 i_min = np.where(feq_av == fmin)
349 i_max = np.where(feq_av == fmax)
351 return fmin, fmax, i_min, i_max
353 @deprecated('Current implementation of non-analytical correction is '
354 'likely incorrect, see '
355 'https://gitlab.com/ase/ase/-/issues/941')
356 def read_born_charges(self, name='born', neutrality=True):
357 r"""Read Born charges and dieletric tensor from JSON file.
359 The charge neutrality sum-rule::
361 _ _
362 \ a
363 ) Z = 0
364 /__ ij
365 a
367 Parameters:
369 neutrality: bool
370 Restore charge neutrality condition on calculated Born effective
371 charges.
372 name: str
373 Key used to identify the file with Born charges for the unit cell
374 in the JSON cache.
376 .. deprecated:: 3.22.1
377 Current implementation of non-analytical correction is likely
378 incorrect, see :issue:`941`
379 """
381 # Load file with Born charges and dielectric tensor for atoms in the
382 # unit cell
383 Z_avv, eps_vv = self.cache[name]
385 # Neutrality sum-rule
386 if neutrality:
387 Z_mean = Z_avv.sum(0) / len(Z_avv)
388 Z_avv -= Z_mean
390 self.Z_avv = Z_avv[self.indices]
391 self.eps_vv = eps_vv
393 def read(self, method='Frederiksen', symmetrize=3, acoustic=True,
394 cutoff=None, born=False, **kwargs):
395 """Read forces from json files and calculate force constants.
397 Extra keyword arguments will be passed to ``read_born_charges``.
399 Parameters:
401 method: str
402 Specify method for evaluating the atomic forces.
403 symmetrize: int
404 Symmetrize force constants (see doc string at top) when
405 ``symmetrize != 0`` (default: 3). Since restoring the acoustic sum
406 rule breaks the symmetry, the symmetrization must be repeated a few
407 times until the changes a insignificant. The integer gives the
408 number of iterations that will be carried out.
409 acoustic: bool
410 Restore the acoustic sum rule on the force constants.
411 cutoff: None or float
412 Zero elements in the dynamical matrix between atoms with an
413 interatomic distance larger than the cutoff.
414 born: bool
415 Read in Born effective charge tensor and high-frequency static
416 dielelctric tensor from file.
418 """
420 method = method.lower()
421 assert method in ['standard', 'frederiksen']
422 if cutoff is not None:
423 cutoff = float(cutoff)
425 # Read Born effective charges and optical dielectric tensor
426 if born:
427 self.read_born_charges(**kwargs)
429 # Number of atoms
430 natoms = len(self.indices)
431 # Number of unit cells
432 N = np.prod(self.supercell)
433 # Matrix of force constants as a function of unit cell index in units
434 # of eV / Ang**2
435 C_xNav = np.empty((natoms * 3, N, natoms, 3), dtype=float)
437 # Loop over all atomic displacements and calculate force constants
438 for i, a in enumerate(self.indices):
439 for j, v in enumerate('xyz'):
440 # Atomic forces for a displacement of atom a in direction v
441 # basename = '%s.%d%s' % (self.name, a, v)
442 basename = '%d%s' % (a, v)
443 fminus_av = self.cache[basename + '-']['forces']
444 fplus_av = self.cache[basename + '+']['forces']
446 if method == 'frederiksen':
447 fminus_av[a] -= fminus_av.sum(0)
448 fplus_av[a] -= fplus_av.sum(0)
450 # Finite difference derivative
451 C_av = fminus_av - fplus_av
452 C_av /= 2 * self.delta
454 # Slice out included atoms
455 C_Nav = C_av.reshape((N, len(self.atoms), 3))[:, self.indices]
456 index = 3 * i + j
457 C_xNav[index] = C_Nav
459 # Make unitcell index the first and reshape
460 C_N = C_xNav.swapaxes(0, 1).reshape((N,) + (3 * natoms, 3 * natoms))
462 # Cut off before symmetry and acoustic sum rule are imposed
463 if cutoff is not None:
464 self.apply_cutoff(C_N, cutoff)
466 # Symmetrize force constants
467 if symmetrize:
468 for _ in range(symmetrize):
469 # Symmetrize
470 C_N = self.symmetrize(C_N)
471 # Restore acoustic sum-rule
472 if acoustic:
473 self.acoustic(C_N)
474 else:
475 break
477 # Store force constants and dynamical matrix
478 self.C_N = C_N
479 self.D_N = C_N.copy()
481 # Add mass prefactor
482 m_a = self.atoms.get_masses()
483 self.m_inv_x = np.repeat(m_a[self.indices]**-0.5, 3)
484 M_inv = np.outer(self.m_inv_x, self.m_inv_x)
485 for D in self.D_N:
486 D *= M_inv
488 def symmetrize(self, C_N):
489 """Symmetrize force constant matrix."""
491 # Number of atoms
492 natoms = len(self.indices)
493 # Number of unit cells
494 N = np.prod(self.supercell)
496 # Reshape force constants to (l, m, n) cell indices
497 C_lmn = C_N.reshape(self.supercell + (3 * natoms, 3 * natoms))
499 # Shift reference cell to center index
500 if self.offset == 0:
501 C_lmn = fft.fftshift(C_lmn, axes=(0, 1, 2)).copy()
502 # Make force constants symmetric in indices -- in case of an even
503 # number of unit cells don't include the first cell
504 i, j, k = 1 - np.asarray(self.supercell) % 2
505 C_lmn[i:, j:, k:] *= 0.5
506 C_lmn[i:, j:, k:] += \
507 C_lmn[i:, j:, k:][::-1, ::-1, ::-1].transpose(0, 1, 2, 4, 3).copy()
508 if self.offset == 0:
509 C_lmn = fft.ifftshift(C_lmn, axes=(0, 1, 2)).copy()
511 # Change to single unit cell index shape
512 C_N = C_lmn.reshape((N, 3 * natoms, 3 * natoms))
514 return C_N
516 def acoustic(self, C_N):
517 """Restore acoustic sumrule on force constants."""
519 # Number of atoms
520 natoms = len(self.indices)
521 # Copy force constants
522 C_N_temp = C_N.copy()
524 # Correct atomic diagonals of R_m = (0, 0, 0) matrix
525 for C in C_N_temp:
526 for a in range(natoms):
527 for a_ in range(natoms):
528 C_N[self.offset,
529 3 * a: 3 * a + 3,
530 3 * a: 3 * a + 3] -= C[3 * a: 3 * a + 3,
531 3 * a_: 3 * a_ + 3]
533 def apply_cutoff(self, D_N, r_c):
534 """Zero elements for interatomic distances larger than the cutoff.
536 Parameters:
538 D_N: ndarray
539 Dynamical/force constant matrix.
540 r_c: float
541 Cutoff in Angstrom.
543 """
545 # Number of atoms and primitive cells
546 natoms = len(self.indices)
547 N = np.prod(self.supercell)
548 # Lattice vectors
549 R_cN = self._lattice_vectors_array
550 # Reshape matrix to individual atomic and cartesian dimensions
551 D_Navav = D_N.reshape((N, natoms, 3, natoms, 3))
553 # Cell vectors
554 cell_vc = self.atoms.cell.transpose()
555 # Atomic positions in reference cell
556 pos_av = self.atoms.get_positions()
558 # Zero elements with a distance to atoms in the reference cell
559 # larger than the cutoff
560 for n in range(N):
561 # Lattice vector to cell
562 R_v = np.dot(cell_vc, R_cN[:, n])
563 # Atomic positions in cell
564 posn_av = pos_av + R_v
565 # Loop over atoms and zero elements
566 for i, a in enumerate(self.indices):
567 dist_a = np.sqrt(np.sum((pos_av[a] - posn_av)**2, axis=-1))
568 # Atoms where the distance is larger than the cufoff
569 i_a = dist_a > r_c # np.where(dist_a > r_c)
570 # Zero elements
571 D_Navav[n, i, :, i_a, :] = 0.0
573 def get_force_constant(self):
574 """Return matrix of force constants."""
576 assert self.C_N is not None
577 return self.C_N
579 def get_band_structure(self, path, modes=False, born=False, verbose=True):
580 """Calculate and return the phonon band structure.
582 This method computes the phonon band structure for a given path
583 in reciprocal space. It is a wrapper around the internal
584 `band_structure` method of the `Phonons` class. The method can
585 optionally calculate and return phonon modes.
587 Frequencies and modes are in units of eV and 1/sqrt(amu),
588 respectively.
590 Parameters:
592 path : BandPath object
593 The BandPath object defining the path in the reciprocal
594 space over which the phonon band structure is calculated.
595 modes : bool, optional
596 If True, phonon modes will also be calculated and returned.
597 Defaults to False.
598 born : bool, optional
599 If True, includes the effect of Born effective charges in
600 the phonon calculations.
601 Defaults to False.
602 verbose : bool, optional
603 If True, enables verbose output during the calculation.
604 Defaults to True.
606 Returns:
608 BandStructure or tuple of (BandStructure, ndarray)
609 If ``modes`` is False, returns a ``BandStructure`` object
610 containing the phonon band structure. If ``modes`` is True,
611 returns a tuple, where the first element is the
612 ``BandStructure`` object and the second element is an ndarray
613 of phonon modes.
615 If modes are returned, the array is of shape
616 (k-point, bands, atoms, 3) and the norm-squared of the mode
617 is `1 / m_{eff}`, where `m_{eff}` is the effective mass of the
618 mode.
620 Example:
622 >>> from ase.dft.kpoints import BandPath
623 >>> path = BandPath(...) # Define the band path
624 >>> phonons = Phonons(...)
625 >>> bs, modes = phonons.get_band_structure(path, modes=True)
626 """
627 result = self.band_structure(path.kpts,
628 modes=modes,
629 born=born,
630 verbose=verbose)
631 if modes:
632 omega_kl, omega_modes = result
633 else:
634 omega_kl = result
636 from ase.spectrum.band_structure import BandStructure
637 bs = BandStructure(path, energies=omega_kl[None])
639 # Return based on the modes flag
640 return (bs, omega_modes) if modes else bs
642 def compute_dynamical_matrix(self, q_scaled: np.ndarray, D_N: np.ndarray):
643 """ Computation of the dynamical matrix in momentum space D_ab(q).
644 This is a Fourier transform from real-space dynamical matrix D_N
645 for a given momentum vector q.
647 q_scaled: q vector in scaled coordinates.
649 D_N: the dynamical matrix in real-space. It is necessary, at least
650 currently, to provide this matrix explicitly (rather than use
651 self.D_N) because this matrix is modified by the Born charges
652 contributions and these modifications are momentum (q) dependent.
654 Result:
655 D(q): two-dimensional, complex-valued array of
656 shape=(3 * natoms, 3 * natoms).
657 """
658 # Evaluate fourier sum
659 R_cN = self._lattice_vectors_array
660 phase_N = np.exp(-2.j * pi * np.dot(q_scaled, R_cN))
661 D_q = np.sum(phase_N[:, np.newaxis, np.newaxis] * D_N, axis=0)
662 return D_q
664 def band_structure(self, path_kc, modes=False, born=False, verbose=True):
665 """Calculate phonon dispersion along a path in the Brillouin zone.
667 The dynamical matrix at arbitrary q-vectors is obtained by Fourier
668 transforming the real-space force constants. In case of negative
669 eigenvalues (squared frequency), the corresponding negative frequency
670 is returned.
672 Frequencies and modes are in units of eV and 1/sqrt(amu),
673 respectively.
675 Parameters:
677 path_kc: ndarray
678 List of k-point coordinates (in units of the reciprocal lattice
679 vectors) specifying the path in the Brillouin zone for which the
680 dynamical matrix will be calculated.
681 modes: bool
682 Returns both frequencies and modes when True.
683 born: bool
684 Include non-analytic part given by the Born effective charges and
685 the static part of the high-frequency dielectric tensor. This
686 contribution to the force constant accounts for the splitting
687 between the LO and TO branches for q -> 0.
688 verbose: bool
689 Print warnings when imaginary frequncies are detected.
691 Returns:
693 If modes=False: Array of energies
695 If modes=True: Tuple of two arrays with energies and modes.
696 """
698 assert self.D_N is not None
699 if born:
700 assert self.Z_avv is not None
701 assert self.eps_vv is not None
703 # Dynamical matrix in real-space
704 D_N = self.D_N
706 # Lists for frequencies and modes along path
707 omega_kl = []
708 u_kl = []
710 # Reciprocal basis vectors for use in non-analytic contribution
711 reci_vc = 2 * pi * la.inv(self.atoms.cell)
712 # Unit cell volume in Bohr^3
713 vol = abs(la.det(self.atoms.cell)) / units.Bohr**3
715 for q_c in path_kc:
716 # Add non-analytic part
717 if born:
718 # q-vector in cartesian coordinates
719 q_v = np.dot(reci_vc, q_c)
720 # Non-analytic contribution to force constants in atomic units
721 qdotZ_av = np.dot(q_v, self.Z_avv).ravel()
722 C_na = (4 * pi * np.outer(qdotZ_av, qdotZ_av) /
723 np.dot(q_v, np.dot(self.eps_vv, q_v)) / vol)
724 self.C_na = C_na / units.Bohr**2 * units.Hartree
725 # Add mass prefactor and convert to eV / (Ang^2 * amu)
726 M_inv = np.outer(self.m_inv_x, self.m_inv_x)
727 D_na = C_na * M_inv / units.Bohr**2 * units.Hartree
728 self.D_na = D_na
729 D_N = self.D_N + D_na / np.prod(self.supercell)
731 # if np.prod(self.N_c) == 1:
732 #
733 # q_av = np.tile(q_v, len(self.indices))
734 # q_xx = np.vstack([q_av]*len(self.indices)*3)
735 # D_m += q_xx
737 # Evaluate fourier sum
738 D_q = self.compute_dynamical_matrix(q_c, D_N)
740 if modes:
741 omega2_l, u_xl = la.eigh(D_q, UPLO='U')
742 # Sort eigenmodes according to eigenvalues (see below) and
743 # multiply with mass prefactor. This gives the eigenmode
744 # (which is now not normalized!) with units 1/sqrt(amu).
745 u_lx = (self.m_inv_x[:, np.newaxis] *
746 u_xl[:, omega2_l.argsort()]).T.copy()
747 u_kl.append(u_lx.reshape((-1, len(self.indices), 3)))
748 else:
749 omega2_l = la.eigvalsh(D_q, UPLO='U')
751 # Sort eigenvalues in increasing order
752 omega2_l.sort()
753 # Use dtype=complex to handle negative eigenvalues
754 omega_l = np.sqrt(omega2_l.astype(complex))
756 # Take care of imaginary frequencies
757 if not np.all(omega2_l >= 0.):
758 indices = np.where(omega2_l < 0)[0]
760 if verbose:
761 print('WARNING, %i imaginary frequencies at '
762 'q = (% 5.2f, % 5.2f, % 5.2f) ; (omega_q =% 5.3e*i)'
763 % (len(indices), q_c[0], q_c[1], q_c[2],
764 omega_l[indices][0].imag))
766 omega_l[indices] = -1 * np.sqrt(np.abs(omega2_l[indices].real))
768 omega_kl.append(omega_l.real)
770 # Conversion factor: sqrt(eV / Ang^2 / amu) -> eV
771 s = units._hbar * 1e10 / sqrt(units._e * units._amu)
772 omega_kl = s * np.asarray(omega_kl)
774 if modes:
775 return omega_kl, np.asarray(u_kl)
777 return omega_kl
779 def get_dos(self, kpts=(10, 10, 10), indices=None, verbose=True):
780 """Return a phonon density of states.
782 Parameters:
784 kpts: tuple
785 Shape of Monkhorst-Pack grid for sampling the Brillouin zone.
786 indices: list
787 If indices is not None, the amplitude-weighted atomic-partial
788 DOS for the specified atoms will be calculated.
789 verbose: bool
790 Print warnings when imaginary frequncies are detected.
792 Returns:
793 A RawDOSData object containing the density of states.
794 """
795 from ase.spectrum.dosdata import RawDOSData
797 # dos = self.dos(kpts, npts, delta, indices)
798 kpts_kc = monkhorst_pack(kpts)
799 if indices is None:
800 # Return the total DOS
801 omega_w = self.band_structure(kpts_kc, verbose=verbose)
802 assert omega_w.ndim == 2
803 n_kpt = omega_w.shape[0]
804 omega_w = omega_w.ravel()
805 dos = RawDOSData(omega_w, np.ones_like(omega_w) / n_kpt)
806 else:
807 # Return a partial DOS
808 omegas, amplitudes = self.band_structure(kpts_kc,
809 modes=True,
810 verbose=verbose)
811 # omegas.shape = (k-points, bands)
812 # amplitudes.shape = (k-points, bands, atoms, 3)
813 ampl_sq = (np.abs(amplitudes)**2).sum(axis=3)
814 assert ampl_sq.ndim == 3
815 assert ampl_sq.shape == omegas.shape + (len(self.indices),)
816 weights = ampl_sq[:, :, indices].sum(axis=2) / ampl_sq.sum(axis=2)
817 dos = RawDOSData(omegas.ravel(), weights.ravel() / omegas.shape[0])
818 return dos
820 @deprecated('Please use Phonons.get_dos() instead of Phonons.dos().')
821 def dos(self, kpts=(10, 10, 10), npts=1000, delta=1e-3):
822 """Calculate phonon dos as a function of energy.
824 Parameters:
826 kpts: tuple
827 Shape of Monkhorst-Pack grid for sampling the Brillouin zone.
828 npts: int
829 Number of energy points.
830 delta: float
831 Broadening of Lorentzian line-shape in eV.
833 Returns:
834 Tuple of (frequencies, dos). The frequencies are in units of eV.
836 .. deprecated:: 3.23.1
837 Please use the ``.get_dos()`` method instead, it returns a proper
838 RawDOSData object.
839 """
841 # Monkhorst-Pack grid
842 kpts_kc = monkhorst_pack(kpts)
843 N = np.prod(kpts)
844 # Get frequencies
845 omega_kl = self.band_structure(kpts_kc)
846 # Energy axis and dos
847 omega_e = np.linspace(0., np.amax(omega_kl) + 5e-3, num=npts)
848 dos_e = np.zeros_like(omega_e)
850 # Sum up contribution from all q-points and branches
851 for omega_l in omega_kl:
852 diff_el = (omega_e[:, np.newaxis] - omega_l[np.newaxis, :])**2
853 dos_el = 1. / (diff_el + (0.5 * delta)**2)
854 dos_e += dos_el.sum(axis=1)
856 dos_e *= 1. / (N * pi) * 0.5 * delta
858 return omega_e, dos_e
860 def write_modes(self, q_c, branches=0, kT=units.kB * 300, born=False,
861 repeat=(1, 1, 1), nimages=30, center=False):
862 """Write modes to trajectory file.
864 Parameters:
866 q_c: ndarray of shape (3,)
867 q-vector of the modes.
868 branches: int or list
869 Branch index of modes.
870 kT: float
871 Temperature in units of eV. Determines the amplitude of the atomic
872 displacements in the modes.
873 born: bool
874 Include non-analytic contribution to the force constants at q -> 0.
875 repeat: tuple
876 Repeat atoms (l, m, n) times in the directions of the lattice
877 vectors. Displacements of atoms in repeated cells carry a Bloch
878 phase factor given by the q-vector and the cell lattice vector R_m.
879 nimages: int
880 Number of images in an oscillation.
881 center: bool
882 Center atoms in unit cell if True (default: False).
884 To exaggerate the amplitudes for better visualization, multiply
885 kT by the square of the desired factor.
886 """
888 if isinstance(branches, int):
889 branch_l = [branches]
890 else:
891 branch_l = list(branches)
893 # Calculate modes
894 omega_l, u_l = self.band_structure([q_c], modes=True, born=born)
895 # Repeat atoms
896 atoms = self.atoms * repeat
897 # Center
898 if center:
899 atoms.center()
901 # Here ``Na`` refers to a composite unit cell/atom dimension
902 pos_Nav = atoms.get_positions()
903 # Total number of unit cells
904 N = np.prod(repeat)
906 # Corresponding lattice vectors R_m
907 R_cN = np.indices(repeat).reshape(3, -1)
908 # Bloch phase
909 phase_N = np.exp(2.j * pi * np.dot(q_c, R_cN))
910 phase_Na = phase_N.repeat(len(self.atoms))
912 hbar = units._hbar * units.J * units.second
913 for lval in branch_l:
915 omega = omega_l[0, lval]
916 u_av = u_l[0, lval]
917 assert u_av.ndim == 2
919 # For a classical harmonic oscillator, <x^2> = k T / m omega^2
920 # and <x^2> = 1/2 u^2 where u is the amplitude and m is the
921 # effective mass of the mode.
922 # The reciprocal mass is already included in the normalization
923 # of the modes. The variable omega is actually hbar*omega (it
924 # is in eV, not reciprocal ASE time units).
925 u_av *= hbar * sqrt(2 * kT) / abs(omega)
927 mode_av = np.zeros((len(self.atoms), 3), dtype=complex)
928 # Insert slice with atomic displacements for the included atoms
929 mode_av[self.indices] = u_av
930 # Repeat and multiply by Bloch phase factor
931 mode_Nav = np.vstack(N * [mode_av]) * phase_Na[:, np.newaxis]
933 with Trajectory('%s.mode.%d.traj'
934 % (self.name, lval), 'w') as traj:
935 for x in np.linspace(0, 2 * pi, nimages, endpoint=False):
936 atoms.set_positions((pos_Nav + np.exp(1.j * x) *
937 mode_Nav).real)
938 traj.write(atoms)