Coverage for gpaw/fdtd/polarizable_material.py: 86%

279 statements  

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

1"""Part of the module for electrodynamic simulations 

2 

3""" 

4 

5# flake8: noqa 

6 

7from ase.units import Hartree, Bohr 

8from gpaw.fd_operators import Gradient 

9import numpy as np 

10 

11# in atomic units, 1/(4*pi*e_0) = 1 

12_eps0_au = 1.0 / (4.0 * np.pi) 

13 

14# Base class for the classical polarizable material: 

15# -holds various functions at each point in the calculation grid: 

16# 1) the dielectric function (permittivity) 

17# 2) electric field 

18# 3) classical polarization charge density 

19# -contains routines for calculating them from each other and/or external potential 

20 

21class PolarizableMaterial(): 

22 def __init__(self, *, sign=-1.0): 

23 self.gd = None 

24 self.initialized = False 

25 self.sign = sign 

26 self.messages = [] 

27 self.components = [] 

28 

29 def add_component(self, component): 

30 self.components.append(component) 

31 

32 def permittivity_value(self, omega=0.0): 

33 return self.eps_infty + _eps0_au * np.sum(self.beta / (self.bar_omega**2.0 - 1J * self.alpha * omega - omega**2.0), axis=0) 

34 

35 def get_static_permittivity(self): 

36 return self.gd.collect(self.permittivity_value(0.0)) 

37 

38 def initialize(self, gd): 

39 if self.initialized: # double initialization leads to problems 

40 return 

41 self.initialized = True 

42 self.messages.append("Polarizable Material:") 

43 

44 try: 

45 self.Nj = max(component.permittivity.Nj for component in self.components) 

46 except: 

47 self.Nj = 0 

48 

49 self.gd = gd 

50 

51 # 3-dimensional scalar array: rho, eps_infty 

52 self.charge_density = self.gd.zeros() 

53 self.eps_infty = np.ones(self.gd.empty().shape) * _eps0_au 

54 

55 # 3-dimensional vector arrays: 

56 # electric field, total polarization density 

57 dims = [3] + list(self.gd.empty().shape) 

58 self.electric_field = np.zeros(dims) 

59 self.polarization_total = np.zeros(dims) 

60 

61 # 4-dimensional vector arrays: 

62 # currents, polarizations 

63 dims = [3, self.Nj] + list(self.gd.empty().shape) 

64 self.currents = np.zeros(dims) 

65 self.polarizations = np.zeros(dims) 

66 

67 # 4-dimensional scalar arrays: 

68 # oscillator parameters alpha, beta, bar_omega, eps_infty 

69 dims = [self.Nj] + list(self.gd.empty().shape) 

70 self.alpha = np.zeros(dims) 

71 self.beta = np.zeros(dims) 

72 self.bar_omega = np.ones(dims) 

73 

74 # Set the permittivity for each grid point 

75 for component in self.components: 

76 self.apply_mask(mask = component.get_mask(self.gd), 

77 permittivity = component.permittivity) 

78 

79 # Restart by regenerating the structures 

80 # TODO: is everything always fully reproduced? Should there be a test for it (e.g. checksum[mask]) 

81 def read(self, reader): 

82 for component in reader.classmat.components: 

83 name = component['name'] 

84 arguments = component['arguments'] 

85 eps_infty = component['eps_infty'] 

86 eps = component['eps'] # Data as array 

87 eval(f'self.add_component({name}(permittivity = Permittivity(data=eps), {arguments}))') 

88 

89 # Save information on the structures for restarting 

90 def write(self, writer): 

91 writer.write(components=[ 

92 {'name': component.name, 

93 'arguments': component.arguments, 

94 'eps_infty': component.permittivity.eps_infty, 

95 'eps': component.permittivity.data_eVA()} 

96 for component in self.components]) 

97 

98 # Here the 3D-arrays are filled with material-specific information 

99 def apply_mask(self, mask, permittivity): 

100 for j in range(permittivity.Nj): 

101 self.bar_omega[j] = np.logical_not(mask) * self.bar_omega[j] + mask * permittivity.oscillators[j].bar_omega 

102 self.alpha [j] = np.logical_not(mask) * self.alpha[j] + mask * permittivity.oscillators[j].alpha 

103 self.beta [j] = np.logical_not(mask) * self.beta[j] + mask * permittivity.oscillators[j].beta 

104 

105 # Add dummy oscillators if needed 

106 for j in range(permittivity.Nj, self.Nj): 

107 self.bar_omega [j] = np.logical_not(mask) * self.bar_omega[j] + mask * 1.0 

108 self.alpha [j] = np.logical_not(mask) * self.alpha[j] + mask * 0.0 

109 self.beta [j] = np.logical_not(mask) * self.beta[j] + mask * 0.0 

110 

111 # Print the permittivity information 

112 self.messages.append(" Permittivity data:") 

113 self.messages.append(" bar_omega alpha beta") 

114 self.messages.append(" ----------------------------------------") 

115 for j in range(permittivity.Nj): 

116 self.messages.append("{:12.6f} {:12.6f} {:12.6f}".format(permittivity.oscillators[j].bar_omega, 

117 permittivity.oscillators[j].alpha, 

118 permittivity.oscillators[j].beta)) 

119 self.messages.append(" ----------------------------------------") 

120 self.messages.append("...done initializing Polarizable Material") 

121 masksum = self.gd.comm.sum_scalar(int(np.sum(mask))) 

122 masksize = self.gd.comm.sum_scalar(int(np.size(mask))) 

123 self.messages.append("Fill ratio: %f percent" % (100.0 * float(masksum)/float(masksize))) 

124 

125 

126 # E(r) = -Grad V(r) 

127 # NB: Here -V(r) is used (if/when sign=-1), because in GPAW the 

128 # electron unit charge is +1 so that the calculated V(r) is 

129 # positive around negative charge. In order to get the correct 

130 # direction of the electric field, the sign must be changed. 

131 def solve_electric_field(self, phi): 

132 for v in range(3): 

133 Gradient(self.gd, v, n=3).apply(-1.0 * self.sign * phi, self.electric_field[v]) 

134 

135 # n(r) = -Div P(r) 

136 def solve_rho(self): 

137 self.charge_density *= 0.0 

138 dmy = self.gd.empty() 

139 for v in range(3): 

140 Gradient(self.gd, v, n=3).apply(self.polarization_total[v], dmy) 

141 self.charge_density -= dmy 

142 

143 # P(r, omega) = [eps(r, omega) - eps0] E(r, omega) 

144 # P0(r) = [eps_inf(r) - eps0] E0(r) + sum_j P0_j(r) // Gao2012, Eq. 10 

145 def solve_polarizations(self): 

146 for v in range(3): 

147 self.polarizations[v] = _eps0_au * self.beta / (self.bar_omega**2.0) * self.electric_field[v] 

148 self.polarization_total = np.sum(self.polarizations, axis=1) + (self.eps_infty - _eps0_au ) * self.electric_field 

149 

150 def propagate_polarizations(self, timestep): 

151 for v in range(3): 

152 self.polarizations[v] = self.polarizations[v] + timestep * self.currents[v] 

153 self.polarization_total = np.sum(self.polarizations, axis=1) 

154 

155 def propagate_currents(self, timestep): 

156 c1 = (1.0 - 0.5 * self.alpha*timestep)/(1.0 + 0.5 * self.alpha*timestep) 

157 c2 = - timestep / (1.0 + 0.5 * self.alpha*timestep) * (self.bar_omega**2.0) 

158 c3 = - timestep / (1.0 + 0.5 * self.alpha*timestep) * (-1.0) * _eps0_au * self.beta 

159 for v in range(3): 

160 self.currents[v] = c1 * self.currents[v] + c2 * self.polarizations[v] + c3 * self.electric_field[v] 

161 

162 def kick_electric_field(self, timestep, kick): 

163 for v in range(3): 

164 self.electric_field[v] = self.electric_field[v] + kick[v] / timestep 

165 

166# Box-shaped classical material 

167class PolarizableBox(): 

168 def __init__(self, *, corner1, corner2, permittivity): 

169 # sanity check 

170 assert(len(corner1)==3) 

171 assert(len(corner2)==3) 

172 

173 self.corner1 = np.array(corner1)/Bohr # from Angstroms to atomic units 

174 self.corner2 = np.array(corner2)/Bohr # from Angstroms to atomic units 

175 self.permittivity = permittivity 

176 

177 self.name = 'PolarizableBox' 

178 self.arguments = 'corner1=[{:f}, {:f}, {:f}], corner2=[{:f}, {:f}, {:f}]'.format(corner1[0], corner1[1], corner1[2], 

179 corner2[0], corner2[1], corner2[2]) 

180 

181 # Setup grid descriptor and the permittivity values inside the box 

182 def get_mask(self, gd): 

183 

184 # 3D coordinates at each grid point 

185 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

186 

187 # inside or outside 

188 return np.logical_and(np.logical_and( # z 

189 np.logical_and(np.logical_and( # y 

190 np.logical_and( #x 

191 r_gv[:, :, :, 0] > self.corner1[0], 

192 r_gv[:, :, :, 0] < self.corner2[0]), 

193 r_gv[:, :, :, 1] > self.corner1[1]), 

194 r_gv[:, :, :, 1] < self.corner2[1]), 

195 r_gv[:, :, :, 2] > self.corner1[2]), 

196 r_gv[:, :, :, 2] < self.corner2[2]) 

197 

198 

199# Shape from atom positions (surrounding region) 

200class PolarizableAtomisticRegion(): 

201 def __init__(self, *, atoms=None, atom_positions=None, distance=0.0, permittivity=None): 

202 

203 if atoms is not None: 

204 self.atom_positions = np.array(atoms.get_positions()) 

205 else: 

206 self.atom_positions = np.array(atom_positions) 

207 

208 # sanity check 

209 assert(len(self.atom_positions)>1) 

210 

211 self.permittivity = permittivity 

212 self.name = 'PolarizableAtomisticRegion' 

213 

214 # use the minimum interatomic distance 

215 if distance<1e-10: 

216 self.distance = np.sqrt(np.min([np.sort(np.sum((self.atom_positions-ap)**2, axis=1))[1] for ap in self.atom_positions]))/Bohr 

217 else: 

218 self.distance = distance/Bohr # from Angstroms to atomic units 

219 

220 dbohr = self.distance*Bohr 

221 self.arguments = 'distance = %20.12e, atom_positions=[' % dbohr 

222 for ap in self.atom_positions: 

223 self.arguments += f'[{ap[0]:20.12e}, {ap[1]:20.12e}, {ap[2]:20.12e}],' 

224 self.arguments = self.arguments[:-1] + ']' 

225 

226 # Setup grid descriptor and the permittivity values inside the box 

227 def get_mask(self, gd): 

228 

229 # 3D coordinates at each grid point 

230 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

231 

232 # inside or outside 

233 mask_tbl = False * np.ones(r_gv.shape[:-1]) 

234 for ap in self.atom_positions: 

235 mask_tbl = np.logical_or(mask_tbl, np.array( (r_gv[:, :, :, 0] - ap[0]/Bohr)**2.0 + 

236 (r_gv[:, :, :, 1] - ap[1]/Bohr)**2.0 + 

237 (r_gv[:, :, :, 2] - ap[2]/Bohr)**2.0 < self.distance**2)) 

238 return mask_tbl 

239 

240# Sphere-shaped classical material 

241class PolarizableSphere(): 

242 def __init__(self, *, center, radius, permittivity): 

243 self.permittivity = permittivity 

244 self.center = np.array(center)/Bohr # from Angstroms to atomic units 

245 self.radius = radius/Bohr # from Angstroms to atomic units 

246 

247 # sanity check 

248 assert(len(self.center)==3) 

249 

250 self.name = 'PolarizableSphere' 

251 self.arguments = f'center=[{center[0]:20.12e}, {center[1]:20.12e}, {center[2]:20.12e}], radius={radius:20.12e}' 

252 

253 def get_mask(self, gd): 

254 

255 # 3D coordinates at each grid point 

256 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

257 

258 # inside or outside 

259 return np.array( (r_gv[:, :, :, 0] - self.center[0])**2.0 + 

260 (r_gv[:, :, :, 1] - self.center[1])**2.0 + 

261 (r_gv[:, :, :, 2] - self.center[2])**2.0 < self.radius**2) 

262 

263# Sphere-shaped classical material 

264class PolarizableEllipsoid(): 

265 def __init__(self, *, center, radii, permittivity): 

266 # sanity check 

267 assert(len(center)==3) 

268 assert(len(radii)==3) 

269 

270 self.center = np.array(center)/Bohr # from Angstroms to atomic units 

271 self.radii = np.array(radii)/Bohr # from Angstroms to atomic units 

272 self.permittivity = permittivity 

273 

274 self.name = 'PolarizableEllipsoid' 

275 self.arguments = f'center=[{center[0]:20.12e}, {center[1]:20.12e}, {center[2]:20.12e}], radii=[{radii[0]:20.12e}, {radii[1]:20.12e}, {radii[2]:20.12e}]' 

276 

277 

278 def get_mask(self, gd): 

279 

280 # 3D coordinates at each grid point 

281 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

282 

283 # inside or outside 

284 return np.array( (r_gv[:, :, :, 0] - self.center[0])**2.0/self.radii[0]**2.0 + 

285 (r_gv[:, :, :, 1] - self.center[1])**2.0/self.radii[1]**2.0 + 

286 (r_gv[:, :, :, 2] - self.center[2])**2.0/self.radii[2]**2.0 < 1.0) 

287 

288 # Rod-shaped classical material 

289class PolarizableRod(): 

290 def __init__(self, *, corners, radius, round_corners, permittivity): 

291 # sanity check 

292 assert(np.array(corners).shape[0]>1) # at least two points 

293 assert(np.array(corners).shape[1]==3) # 3D 

294 

295 self.name = 'PolarizableRod' 

296 self.arguments = 'radius = %20.12e, corners=[' % radius 

297 for c in corners: 

298 self.arguments += f'[{c[0]:20.12e}, {c[1]:20.12e}, {c[2]:20.12e}],' 

299 self.arguments = self.arguments[:-1] + ']' 

300 self.arguments += f', round_corners={round_corners}' 

301 

302 self.corners = np.array(corners)/Bohr # from Angstroms to atomic units 

303 self.radius = radius/Bohr # from Angstroms to atomic units 

304 self.round_corners = round_corners 

305 self.permittivity = permittivity 

306 

307 def get_mask(self, gd): 

308 

309 # 3D coordinates at each grid point 

310 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

311 ng = r_gv.shape[0:-1] 

312 ngv = r_gv.shape 

313 

314 a = self.corners[0] 

315 

316 mask = False * np.ones(ng) 

317 

318 for p in self.corners[1:]: 

319 # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line: 

320 # d = |(a-p)-((a-p).n)n| point p, line a+tn (|n|=1) 

321 n = (p-a)/np.sqrt((p-a).dot(p-a)) 

322 v1 = np.array([a[w]-r_gv[:, :, :, w] for w in range(3)]).transpose((1, 2, 3, 0)) # a-p 

323 

324 v2 = np.sum(np.array([v1[:, :, :, w]*n[w] for w in range(3)]), axis=0) # (a-p).n 

325 

326 v3 = np.array([v2*n[w] for w in range(3)]).transpose(1, 2, 3, 0) # ((a-p).n)n 

327 

328 d = np.zeros(ng) 

329 for ind, idx in np.ndenumerate(v3[:, :, :, 0]): 

330 d[ind] = np.array(v1[ind]-v3[ind]).dot(v1[ind]-v3[ind]) 

331 

332 # angle between (p-a) and (r-a): 

333 pa = p-a # (3) 

334 ra = np.array([r_gv[:, :, :, w]-a[w] for w in range(3)]).transpose((1, 2, 3, 0)) # (ng1, ng2, ng3, 3) 

335 para = np.sum([pa[w]*ra[:, :, :, w] for w in range(3)], axis=0) 

336 ll2 = pa.dot(pa)*np.sum([ra[:, :, :, w]*ra[:, :, :, w] for w in range(3)], axis=0) 

337 angle1 = np.arccos(para/(1.0e-9+np.sqrt(ll2))) 

338 

339 # angle between (a-p) and (r-p): 

340 ap = a-p # (3) 

341 rp = np.array([r_gv[:, :, :, w]-p[w] for w in range(3)]).transpose((1, 2, 3, 0)) # (ng1, ng2, ng3, 3) 

342 aprp = np.sum([ap[w]*rp[:, :, :, w] for w in range(3)], axis=0) 

343 ll2 = ap.dot(ap)*np.sum([rp[:, :, :, w]*rp[:, :, :, w] for w in range(3)], axis=0) 

344 angle2 = np.arccos(aprp/(1.0e-9+np.sqrt(ll2))) 

345 

346 # Include in the mask 

347 this_mask = np.logical_and(np.logical_and(angle1 < 0.5*np.pi, angle2 < 0.5*np.pi), 

348 d < self.radius**2.0 ) 

349 

350 # Add spheres around current end points 

351 if self.round_corners: 

352 # |r-a| and |r-p| 

353 raDist = np.sum([ra[:, :, :, w]*ra[:, :, :, w] for w in range(3)], axis=0) 

354 rpDist = np.sum([rp[:, :, :, w]*rp[:, :, :, w] for w in range(3)], axis=0) 

355 this_mask = np.logical_or(this_mask, 

356 np.logical_or(raDist < self.radius**2.0, rpDist < self.radius**2.0)) 

357 

358 mask = np.logical_or(mask, this_mask) 

359 

360 # move to next point 

361 a = p 

362 

363 return mask 

364 

365class PolarizableTetrahedron(): 

366 #https://steve.hollasch.net/cgindex/geometry/ptintet.html 

367 #obrecht@imagen.com (Doug Obrecht) writes: 

368 # 

369 # Can someone point me to an algorithm that determines if a point is within a tetrahedron? 

370 # 

371 # Let the tetrahedron have vertices 

372 # V1 = (x1, y1, z1) 

373 # V2 = (x2, y2, z2) 

374 # V3 = (x3, y3, z3) 

375 # V4 = (x4, y4, z4) 

376 # 

377 #and your test point be 

378 # 

379 # P = (x, y, z). 

380 #Then the point P is in the tetrahedron if following five determinants all have the same sign. 

381 # 

382 # |x1 y1 z1 1| 

383 # D0 = |x2 y2 z2 1| 

384 # |x3 y3 z3 1| 

385 # |x4 y4 z4 1| 

386 # 

387 # |x y z 1| 

388 # D1 = |x2 y2 z2 1| 

389 # |x3 y3 z3 1| 

390 # |x4 y4 z4 1| 

391 # 

392 # |x1 y1 z1 1| 

393 # D2 = |x y z 1| 

394 # |x3 y3 z3 1| 

395 # |x4 y4 z4 1| 

396 # 

397 # |x1 y1 z1 1| 

398 # D3 = |x2 y2 z2 1| 

399 # |x y z 1| 

400 # |x4 y4 z4 1| 

401 # 

402 # |x1 y1 z1 1| 

403 # D4 = |x2 y2 z2 1| 

404 # |x3 y3 z3 1| 

405 # |x y z 1| 

406 # 

407 # Some additional notes: 

408 # 

409 # If by chance the D0=0, then your tetrahedron is degenerate (the points are coplanar). 

410 # If any other Di=0, then P lies on boundary i (boundary i being that boundary formed by the three points other than Vi). 

411 # If the sign of any Di differs from that of D0 then P is outside boundary i. 

412 # If the sign of any Di equals that of D0 then P is inside boundary i. 

413 # If P is inside all 4 boundaries, then it is inside the tetrahedron. 

414 # As a check, it must be that D0 = D1+D2+D3+D4. 

415 # The pattern here should be clear; the computations can be extended to simplicies of any dimension. (The 2D and 3D case are the triangle and the tetrahedron). 

416 # If it is meaningful to you, the quantities bi = Di/D0 are the usual barycentric coordinates. 

417 # Comparing signs of Di and D0 is only a check that P and Vi are on the same side of boundary i. 

418 

419 def __init__(self, *, corners, permittivity): 

420 # sanity check 

421 assert(len(corners)==4) # exactly 4 points 

422 assert(len(corners[0])==3) # 3D 

423 

424 self.name = 'PolarizableTetrahedron' 

425 self.arguments = 'corners=[' 

426 for c in corners: 

427 self.arguments += f'[{c[0]:20.12e}, {c[1]:20.12e}, {c[2]:20.12e}],' 

428 self.arguments += ']' 

429 

430 self.corners = np.array(corners)/Bohr # from Angstroms to atomic units 

431 self.permittivity = permittivity 

432 

433 def determinant_value(self, x, y, z, 

434 x1, y1, z1, 

435 x2, y2, z2, 

436 x3, y3, z3, 

437 x4, y4, z4, ind): 

438 mat = np.array([[x1, y1, z1, 1], [x2, y2, z2, 1], [x3, y3, z3, 1], [x4, y4, z4, 1]]) 

439 mat[ind][:] = np.array([x, y, z, 1]) 

440 return np.linalg.det(mat) 

441 

442 def get_mask(self, gd): 

443 r_gv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

444 ng = r_gv.shape[0:-1] 

445 ngv = r_gv.shape 

446 x1, y1, z1 = self.corners[0] 

447 x2, y2, z2 = self.corners[1] 

448 x3, y3, z3 = self.corners[2] 

449 x4, y4, z4 = self.corners[3] 

450 

451 mask = np.ones(ng)==0 

452 

453 # TODO: associate a determinant for each point, and use numpy tools to determine 

454 # the mask without the *very slow* loop over grid points 

455 for ind, pt in np.ndenumerate(r_gv[:, :, :, 0]): 

456 x, y, z = r_gv[ind][:] 

457 d0 = np.array([[x1, y1, z1, 1], 

458 [x2, y2, z2, 1], 

459 [x3, y3, z3, 1], 

460 [x4, y4, z4, 1]]) 

461 d1 = np.array([[x, y, z, 1], 

462 [x2, y2, z2, 1], 

463 [x3, y3, z3, 1], 

464 [x4, y4, z4, 1]]) 

465 d2 = np.array([[x1, y1, z1, 1], 

466 [x, y, z, 1], 

467 [x3, y3, z3, 1], 

468 [x4, y4, z4, 1]]) 

469 d3 = np.array([[x1, y1, z1, 1], 

470 [x2, y2, z2, 1], 

471 [x, y, z, 1], 

472 [x4, y4, z4, 1]]) 

473 d4 = np.array([[x1, y1, z1, 1], 

474 [x2, y2, z2, 1], 

475 [x3, y3, z3, 1], 

476 [x, y, z, 1]]) 

477 s0 = np.linalg.det(d0) 

478 s1 = np.linalg.det(d1) 

479 s2 = np.linalg.det(d2) 

480 s3 = np.linalg.det(d3) 

481 s4 = np.linalg.det(d4) 

482 

483 if (np.sign(s0) == np.sign(s1) or abs(s1) < 1e-12) and \ 

484 (np.sign(s0) == np.sign(s2) or abs(s2) < 1e-12) and \ 

485 (np.sign(s0) == np.sign(s3) or abs(s3) < 1e-12) and \ 

486 (np.sign(s0) == np.sign(s4) or abs(s4) < 1e-12): 

487 mask[ind] = True 

488 return mask 

489 

490# Lorentzian oscillator function: L(omega) = eps0 * beta / (w**2 - i*alpha*omega - omega**2) // Coomar2011, Eq. 2 

491class LorentzOscillator: 

492 def __init__(self, bar_omega, alpha, beta): 

493 self.bar_omega = bar_omega 

494 self.alpha = alpha 

495 self.beta = beta 

496 

497 def value(self, omega): 

498 return _eps0_au * self.beta / (self.bar_omega**2 - 1J * self.alpha * omega - omega**2) 

499 

500# Dieletric function: e(omega) = eps_inf + sum_j L_j(omega) // Coomar2011, Eq. 2 

501class Permittivity: 

502 def __init__(self, fname=None, data=None, eps_infty = _eps0_au ): 

503 

504 # Initialize to vacuum permittivity 

505 self.eps_infty = eps_infty 

506 self.Nj = 0 

507 self.oscillators = [] 

508 

509 # Input as data array 

510 if data is not None: 

511 assert fname is None 

512 self.Nj = len(data) 

513 for v in range(self.Nj): 

514 self.oscillators.append(LorentzOscillator(data[v][0]/Hartree, data[v][1]/Hartree, data[v][2]/Hartree/Hartree)) 

515 return 

516 

517 # Input as filename 

518 if fname != None: # read permittivity from a 3-column file 

519 with open(fname) as fp: 

520 lines = fp.readlines() 

521 

522 self.Nj = len(lines) 

523 

524 for line in lines: 

525 bar_omega = float(line.split()[0]) / Hartree 

526 alpha = float(line.split()[1]) / Hartree 

527 beta = float(line.split()[2]) / Hartree / Hartree 

528 self.oscillators.append(LorentzOscillator(bar_omega, alpha, beta)) 

529 return 

530 

531 

532 

533 def value(self, omega = 0): 

534 return self.eps_infty + sum([osc.value(omega) for osc in self.oscillators]) 

535 

536 def data(self): 

537 return [[osc.bar_omega, osc.alpha, osc.beta] for osc in self.oscillators] 

538 

539 def data_eVA(self): 

540 return [[osc.bar_omega*Hartree, osc.alpha*Hartree, osc.beta*Hartree*Hartree] for osc in self.oscillators] 

541 

542# Dieletric function that renormalizes the static permittivity to the requested value (usually epsZero) 

543class PermittivityPlus(Permittivity): 

544 def __init__(self, fname=None, data=None, eps_infty = _eps0_au, epsZero = _eps0_au, newbar_omega = 0.01, new_alpha = 0.10, **kwargs): 

545 Permittivity.__init__(self, fname=fname, data=data, eps_infty=eps_infty) 

546 

547 # Convert given values from eVs to Hartrees 

548 _newbar_omega = newbar_omega / Hartree 

549 _new_alpha = new_alpha / Hartree 

550 

551 # Evaluate the new value 

552 _new_beta = ((epsZero - self.value(0.0))*_newbar_omega**2.0/_eps0_au).real 

553 self.oscillators.append(LorentzOscillator(_newbar_omega, _new_alpha, _new_beta)) 

554 self.Nj = len(self.oscillators) 

555