Coverage for hiphive/structure_generation/phonon.py: 94%

62 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-08-01 17:04 +0000

1import numpy as np 

2from ase import Atoms 

3import ase.units as aunits 

4from hiphive.input_output.logging_tools import logger 

5 

6logger = logger.getChild(__name__) 

7 

8 

9def generate_phonon_rattled_structures( 

10 atoms: Atoms, 

11 fc2: np.ndarray, 

12 n_structures: int, 

13 temperature: float, 

14 QM_statistics: bool = False, 

15 imag_freq_factor: float = 1.0, 

16) -> list[Atoms]: 

17 r""" 

18 Returns list of phonon-rattled configurations. 

19 

20 Configurations are generated by superimposing harmonic phonon 

21 eigenmodes with random amplitudes and phase factors consistent 

22 with a certain temperature. 

23 

24 Let :math:`\boldsymbol{X}_{ai}` be the phonon modes indexed by atom 

25 :math:`a` and mode :math:`i`, :math:`\omega_i` the phonon 

26 frequencies, and let :math:`0 < Q_i \leq 1` and :math:`0 \leq 

27 U_i < 1` be uniformly random numbers. Then 

28 

29 .. math:: 

30 

31 \boldsymbol{R}_a 

32 = \boldsymbol{R}^0_a 

33 + \left<\frac{k_B T}{m_a} \right>^{1/2} 

34 \sum_i \frac{1}{\omega_i} \boldsymbol{X}_{ai} 

35 \sqrt{-2 \ln Q_i} \cos(\pi \omega_i U_i) 

36 

37 See: West and Estreicher, Physical Review Letters **96**, 115504 (2006) 

38 

39 or 

40 

41 .. math:: 

42 

43 \boldsymbol{R}_a 

44 = \boldsymbol{R}^0_a 

45 + \sum_i 

46 \left(\frac{\hbar(0.5 + n(\omega_i, T))}{m_a\omega_i} \right)^{1/2} 

47 \boldsymbol{X}_{ai} 

48 \sqrt{-2 \ln Q_i} \cos(\pi \omega_i U_i) 

49 

50 where 

51 

52 .. math:: 

53 n = \frac{1}{e^{\hbar\omega_i/k_BT}-1} 

54 

55 Parameters 

56 ---------- 

57 atoms 

58 Prototype structure. 

59 fc2 

60 Second order force constant matrix, with shape `(3N, 3N)` or 

61 `(N, N, 3, 3)`. The conversion will be done internally if. 

62 n_structures 

63 Number of structures to generate. 

64 temperature 

65 Temperature in Kelvin. 

66 QM_statistics 

67 If the amplitude of the quantum harmonic oscillator shoule be used 

68 instead of the classical amplitude. 

69 imag_freq_factor 

70 If a squared frequency, `w2`, is negative it is set to 

71 `w2 = imag_freq_factor * np.abs(w2)`. 

72 

73 Returns 

74 ------- 

75 Generated structures. 

76 """ 

77 structures = [] 

78 pr = _PhononRattler(atoms.get_masses(), fc2, imag_freq_factor) 

79 for _ in range(n_structures): 

80 atoms_tmp = atoms.copy() 

81 pr(atoms_tmp, temperature, QM_statistics) 

82 structures.append(atoms_tmp) 

83 return structures 

84 

85 

86def _n_BE(T, w_s): 

87 """ 

88 Bose-Einstein distribution function. 

89 

90 Parameters 

91 --------- 

92 T : float 

93 Temperature in Kelvin 

94 w_s: numpy.ndarray 

95 frequencies in eV (3*N,) 

96 

97 Returns 

98 ------ 

99 Bose-Einstein distribution for each energy at a given temperature 

100 """ 

101 

102 with np.errstate(divide='raise', over='raise'): 

103 try: 

104 n = 1 / (np.exp(w_s / (aunits.kB * T)) - 1) 

105 except Exception: 

106 n = np.zeros_like(w_s) 

107 return n 

108 

109 

110def _phonon_rattle(m_a, T, w2_s, e_sai, QM_statistics): 

111 """ Thermal excitation of phonon modes as described by West and 

112 Estreicher, Physical Review Letters **96**, 115504 (2006). 

113 

114 _s is a mode index 

115 _i is a Carteesian index 

116 _a is an atom index 

117 

118 Parameters 

119 ---------- 

120 m_a : numpy.ndarray 

121 masses (N,) 

122 T : float 

123 temperature in Kelvin 

124 w2_s : numpy.ndarray 

125 the squared frequencies from the eigenvalue problem (3*N,) 

126 e_sai : numpy.ndarray 

127 polarizations (3*N, N, 3) 

128 QM_statistics : bool 

129 if the amplitude of the quantum harmonic oscillator shoule be used 

130 instead of the classical amplitude 

131 

132 Returns 

133 ------- 

134 displacements : numpy.ndarray 

135 shape (N, 3) 

136 """ 

137 n_modes = 3 * len(m_a) 

138 

139 # skip 3 translational modes 

140 argsort = np.argsort(np.abs(w2_s)) 

141 e_sai = e_sai[argsort][3:] 

142 w2_s = w2_s[argsort][3:] 

143 

144 w_s = np.sqrt(np.abs(w2_s)) 

145 

146 prefactor_a = np.sqrt(1 / m_a).reshape(-1, 1) 

147 if QM_statistics: 

148 hbar = aunits._hbar * aunits.J * aunits.s 

149 frequencyfactor_s = np.sqrt(hbar * (0.5 + _n_BE(T, hbar * w_s)) / w_s) 

150 else: 

151 frequencyfactor_s = 1 / w_s 

152 prefactor_a *= np.sqrt(aunits.kB * T) 

153 

154 phases_s = np.random.uniform(0, 2 * np.pi, size=n_modes - 3) 

155 amplitudes_s = np.sqrt(-2 * np.log(1 - np.random.random(n_modes - 3))) 

156 

157 u_ai = prefactor_a * np.tensordot( 

158 amplitudes_s * np.cos(phases_s) * frequencyfactor_s, e_sai, (0, 0)) 

159 return u_ai # displacements 

160 

161 

162class _PhononRattler: 

163 """ 

164 Class to be able to conveniently save modes and frequencies needed 

165 for phonon rattle. 

166 

167 Parameters 

168 ---------- 

169 masses : numpy.ndarray 

170 masses (N,) 

171 force_constants : numpy.ndarray 

172 second order force constant matrix, with shape `(3N, 3N)` or 

173 `(N, N, 3, 3)`. The conversion will be done internally if. 

174 imag_freq_factor: float 

175 If a squared frequency, w2, is negative then it is set to 

176 w2 = imag_freq_factor * np.abs(w2) 

177 """ 

178 def __init__(self, masses, force_constants, imag_freq_factor=1.0): 

179 n_atoms = len(masses) 

180 if len(force_constants.shape) == 4: # assume shape = (n_atoms, n_atoms, 3, 3) 

181 force_constants = force_constants.transpose(0, 2, 1, 3) 

182 force_constants = force_constants.reshape(3 * n_atoms, 3 * n_atoms) 

183 # Now the fc should have shape = (n_atoms * 3, n_atoms * 3) 

184 # Construct the dynamical matrix 

185 inv_root_masses = (1 / np.sqrt(masses)).repeat(3) 

186 D = np.outer(inv_root_masses, inv_root_masses) 

187 D *= force_constants 

188 # find frequnecies and eigenvectors 

189 w2_s, e_sai = np.linalg.eigh(D) 

190 # reshape to get atom index and Cartesian index separate 

191 e_sai = e_sai.T.reshape(-1, n_atoms, 3) 

192 

193 # The three modes closest to zero are assumed to be zero, ie acoustic sum rules are assumed 

194 frequency_tol = 1e-6 

195 argsort = np.argsort(np.abs(w2_s)) 

196 w2_gamma = w2_s[argsort][:3] 

197 if np.any(np.abs(w2_gamma) > frequency_tol): 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true

198 logger.warning(f'Acoustic sum rules not enforced, squared frequencies: {w2_gamma}') 

199 

200 # warning if any imaginary modes 

201 if np.any(w2_s < -frequency_tol): 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 logger.warning('Imaginary modes present') 

203 

204 # treat imaginary modes as real 

205 imag_mask = w2_s < -frequency_tol 

206 w2_s[imag_mask] = imag_freq_factor * np.abs(w2_s[imag_mask]) 

207 

208 self.w2_s = w2_s 

209 self.e_sai = e_sai 

210 self.masses = masses 

211 

212 def __call__(self, atoms, T, QM_statistics): 

213 """ rattle atoms by adding displacements 

214 

215 Parameters 

216 ---------- 

217 atoms : ase.Atoms 

218 Ideal structure to add displacements to. 

219 T : float 

220 temperature in Kelvin 

221 """ 

222 u_ai = _phonon_rattle(self.masses, T, self.w2_s, self.e_sai, 

223 QM_statistics) 

224 atoms.positions += u_ai