Coverage for gpaw/tddft/abc.py: 91%

96 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-19 00:19 +0000

1""" This module implements absorbing boundary conditions to minimize errors 

2caused by boundary reflections. The first classes use negative imaginary 

3potentials of different shapes which are known to absorb waves. The last 

4class PML uses the perfectly matched layer approach. 

5 

6 For information about imaginary potentials please see: 

7 

8 Takashi Nakatsukasa, Kazuhiro Yabana, J. Chem. Phys. 114, 2550 (2001); 

9 doi:10.1063/1.1338527 

10 Daniel Neuhasuer and Michael Baer, J. Chem. Phys. 90, 4351 (1989); 

11 doi:10.1063/1.456646 

12 

13 About PML: 

14 Chunxiong Zheng, Journal of Computational Physics, 

15 volume 227, Issue 1(Nov 2007) pages 537-556; 

16 doi:10.1016/j.jcp.2007.08.004 

17""" 

18 

19import numpy as np 

20 

21from ase.units import Bohr 

22 

23 

24class DummyAbsorbingBoundary: 

25 """ Virtual and not usable class for abosrbing boundaries.""" 

26 def __init__(self): 

27 self.v_imag = None 

28 self.type = None 

29 

30 def set_up(self): 

31 pass 

32 

33 def get_potential_matrix(self): 

34 return self.v_imag 

35 

36 

37class LinearAbsorbingBoundary(DummyAbsorbingBoundary): 

38 """ 

39 This class uses linear negative imaginary potential for absorption. 

40 For distances larger than abc_range, the negative imaginary potential 

41 increases linearly. Positive float abc_strength describes the steepness 

42 of the slope. High values of abc_strength cause more reflection from 

43 the potential and small values do not absorb enough. 

44 

45 Parameters: 

46 abc_range: Positive float number. Absorbing potential is zero 

47 where distance from the middle point or chosen positions 

48 in the grid is smaller and starts to increase linearly 

49 after this point. 

50 abc_strength: Positive float number. A good value for this is usually 

51 about 0.01 

52 

53 positions: An array of positions in the grid. Each of these points 

54 has abc_range of free space around them. If this is left 

55 as None, the middle point of the grid is used. You can use 

56 the position array of an ASE Atoms object here. 

57 """ 

58 def __init__(self, abc_range, abc_strength, positions=None): 

59 DummyAbsorbingBoundary.__init__(self) 

60 self.abc_strength = abc_strength 

61 self.abc_r = abc_range / Bohr 

62 self.positions = positions / Bohr 

63 self.type = 'IPOT' 

64 

65 def set_up(self, gd): 

66 """ 

67 Creates the potential matrix self.v_imag. 

68 

69 Parameters: 

70 

71 gd: grid descriptor 

72 """ 

73 

74 assert gd.orthogonal 

75 

76 # self.v_imag = np.zeros((gd.n_c[0],gd.n_c[1],gd.n_c[2]),dtype=complex) 

77 self.v_imag = gd.zeros(dtype=complex) 

78 

79 # If positions array wasn't given, uses the middle point of the grid. 

80 if self.positions is None: 

81 self.positions = [ 

82 np.array([ 

83 gd.N_c[0] * gd.h_cv[0, 0] * 0.5, # middle 

84 gd.N_c[1] * gd.h_cv[1, 1] * 0.5, 

85 gd.N_c[2] * gd.h_cv[2, 2] * 0.5 

86 ]) 

87 ] 

88 

89 for i in range(gd.n_c[0]): 

90 x = (i + gd.beg_c[0]) * gd.h_cv[0, 0] 

91 for j in range(gd.n_c[1]): 

92 y = (j + gd.beg_c[1]) * gd.h_cv[1, 1] 

93 for k in range(gd.n_c[2]): 

94 z = (k + gd.beg_c[2]) * gd.h_cv[2, 2] 

95 position = np.array([x, y, z]) 

96 # Calculates the distance from the nearest chosen point 

97 # in the grid 

98 position_vectors = self.positions - position 

99 

100 r = np.linalg.norm(position_vectors[0]) 

101 for vector in position_vectors: 

102 if np.linalg.norm(vector) < r: 

103 r = np.linalg.norm(vector) 

104 

105 if r > self.abc_r: 

106 self.v_imag[i][j][k] = \ 

107 -(0 + 1j) * self.abc_strength * (r - self.abc_r) 

108 

109 

110class P4AbsorbingBoundary(DummyAbsorbingBoundary): 

111 """ 

112 The negative imaginary potential used by this class are 4th degree 

113 polynomials which are constructed so that the value is zero at the 

114 beginning and abc_strength at the end. The derivative is 

115 zero at the begininning and zero at the end. 

116 

117 Parameters: 

118 abc_range: Positive float number. Absorbing potential is zero where 

119 distance from the middle point or chosen positions in the 

120 grid is smaller and starts to increase after this point. 

121 

122 abc_strength: Positive float number. 

123 

124 positions: An array of positions in the grid. Each of these points has 

125 abc_range of free space around them. If this is left as None, 

126 the middle point of the grid is used. You can use the position 

127 array of an ASE Atoms object here. You have to define the 

128 width parameter if you use this. 

129 

130 width: The width of the absorbing layer. If you don't define positions 

131 array a value for this is automatically generated so that the 

132 width is from abc_range to the end of the grid (works best for 

133 cube). 

134 If you use the atom-centered model you have to define the 

135 width yourself. 

136 """ 

137 def __init__(self, abc_range, abc_strength, positions=None, width=None): 

138 DummyAbsorbingBoundary.__init__(self) 

139 self.abc_r = abc_range / Bohr 

140 self.abc_strength = abc_strength 

141 self.positions = positions / Bohr 

142 self.width = width / Bohr 

143 self.type = 'IPOT' 

144 

145 def set_up(self, gd): 

146 

147 # self.v_imag = np.zeros((gd.n_c[0],gd.n_c[1],gd.n_c[2]),dtype=complex) 

148 self.v_imag = gd.zeros(dtype=complex) 

149 

150 # If positions array wasn't given, uses the middle point of the 

151 # grid as the center. 

152 if self.positions is None: 

153 self.positions = [ 

154 np.array([ 

155 gd.N_c[0] * gd.h_cv[0, 0] * 0.5, # middle 

156 gd.N_c[1] * gd.h_cv[1, 1] * 0.5, 

157 gd.N_c[2] * gd.h_cv[2, 2] * 0.5 

158 ]) 

159 ] 

160 

161 if self.width is None: 

162 self.width = np.linalg.norm( 

163 self.positions[0]) / np.sqrt(3) - self.abc_r 

164 

165 for i in range(gd.n_c[0]): 

166 x = (i + gd.beg_c[0]) * gd.h_cv[0, 0] 

167 for j in range(gd.n_c[1]): 

168 y = (j + gd.beg_c[1]) * gd.h_cv[1, 1] 

169 for k in range(gd.n_c[2]): 

170 z = (k + gd.beg_c[2]) * gd.h_cv[2, 2] 

171 position = np.array([x, y, z]) 

172 

173 position_vectors = self.positions - position 

174 

175 r = np.linalg.norm(position_vectors[0]) 

176 for vector in position_vectors: 

177 if np.linalg.norm(vector) < r: 

178 r = np.linalg.norm(vector) 

179 

180 if r > self.abc_r: 

181 if r < self.abc_r + self.width: 

182 self.v_imag[i][j][k] = ( 

183 (0 + 1j) * 

184 ((np.sqrt(self.abc_strength) - 

185 np.sqrt(self.abc_strength) / self.width**2 

186 * (r - self.abc_r)**2)**2 - 

187 self.abc_strength)) 

188 else: 

189 self.v_imag[i][j][k] = -self.abc_strength * (0 + 

190 1j) 

191 

192 

193class PML: 

194 """ 

195 Important! You must use the BiCGStab solver or your time propagation 

196 will likely crash. Give the parameter solver='BiCGStab' as you create 

197 the TDDFT object. 

198 

199 Using PML makes the time progation slower so reserve twice more time. 

200 As imaginary potential is usually almost equally good, this is mostly 

201 for testing (until improved?). 

202 

203 And now for something completely different, a Perfectly matched layer. 

204 Note that you can't give positions array as parameter for this class. 

205 

206 Parameters: 

207 abc_range: Starting from the center of the grid, the amount of 

208 free space before PML starts to affect. 

209 

210 abc_strength: Positive float, good amount should be around 0.1 

211 """ 

212 def __init__(self, abc_range, abc_strength): 

213 

214 self.abc_range = abc_range / Bohr 

215 self.abc_strength = abc_strength 

216 self.type = 'PML' 

217 

218 def set_up(self, gd): 

219 r"""Set up matrices for PML. 

220 

221 Creates the matrixes needed when the PML is applied in tdopers.py 

222 when applying time-dependent hamiltonian. 

223 

224 Perfectly matched layer is applied as potential Vpml = Tpml-T, 

225 Where T = -.5*\nabla^{2}\psi (Use latex for equations) 

226 

227 Tpml we get from 

228 

229 'A perfectly matched layer approach to the nonlinear Schrodinger wave 

230 equations', 

231 Journal of Computational Physics, 

232 volume 227, Issue 1(Nov 2007) pages 537-556, 

233 Author: Chunxiong Zheng 

234 

235 T_{pml} = -0.5*(G\nabla G\nabla \psi + G^{2}\nabla^{2}\psi) 

236 

237 where G = \frac{1}{1+R\sigma} 

238 

239 V_{pml} = -0.5 * (G\nabla G\nabla \psi + (G^{2}-1)\nabla^{2}\psi) 

240 

241 This is probably not the most optimal approach and slows the 

242 propagation. 

243 

244 The matrixes created here are G and the gradients of G in all 

245 directions. 

246 

247 """ 

248 R = (0 + 1j) # Complex number, has to be in the first quadrant. 

249 self.G = gd.zeros(dtype=complex) 

250 self.G[:] = 1.0 

251 

252 self.dG = gd.zeros(n=3, dtype=complex) 

253 

254 r0 = np.array([ 

255 gd.N_c[0] * gd.h_cv[0, 0] * 0.5, gd.N_c[1] * gd.h_cv[1, 1] * 0.5, 

256 gd.N_c[2] * gd.h_cv[2, 2] * 0.5 

257 ]) # middle point 

258 for i in range(gd.n_c[0]): 

259 x = (i + gd.beg_c[0]) * gd.h_cv[0, 0] 

260 for j in range(gd.n_c[1]): 

261 y = (j + gd.beg_c[1]) * gd.h_cv[1, 1] 

262 for k in range(gd.n_c[2]): 

263 z = (k + gd.beg_c[2]) * gd.h_cv[2, 2] 

264 position = np.array([x, y, z]) 

265 r = np.linalg.norm(position - r0) 

266 

267 if r > self.abc_range: 

268 self.G[i][j][k] = (1.0 + R * self.abc_strength * 

269 (r - self.abc_range)**2)**-1.0 

270 self.dG[0][i][j][k] = ( 

271 -(1.0 + R * 

272 self.abc_strength * 

273 (r - self.abc_range)**2.0)**-2.0 * 2.0 * R * 

274 self.abc_strength * (r - self.abc_range) * 

275 (x - r0[0]) / r) 

276 

277 self.dG[1][i][j][k] = -( 

278 1.0 + R * self.abc_strength * 

279 (r - self.abc_range)**2.0 

280 )**-2.0 * 2.0 * R * self.abc_strength * ( 

281 r - self.abc_range) * (y - r0[1]) / r 

282 

283 self.dG[2][i][j][k] = -( 

284 1.0 + R * self.abc_strength * 

285 (r - self.abc_range)**2.0 

286 )**-2.0 * 2.0 * R * self.abc_strength * ( 

287 r - self.abc_range) * (z - r0[2]) / r 

288 

289 def get_G(self): 

290 return self.G 

291 

292 def get_dG(self): 

293 return self.dG 

294 

295 def isPML(self): 

296 return True