Coverage for /builds/debichem-team/python-ase/ase/transport/calculators.py: 60.15%

266 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1import numpy as np 

2from numpy import linalg 

3 

4from ase.transport.greenfunction import GreenFunction 

5from ase.transport.selfenergy import BoxProbe, LeadSelfEnergy 

6from ase.transport.tools import ( 

7 cutcoupling, 

8 dagger, 

9 fermidistribution, 

10 rotate_matrix, 

11 subdiagonalize, 

12) 

13from ase.units import kB 

14 

15 

16class TransportCalculator: 

17 """Determine transport properties of a device sandwiched between 

18 two semi-infinite leads using a Green function method. 

19 """ 

20 

21 def __init__(self, **kwargs): 

22 """Create the transport calculator. 

23 

24 Parameters: 

25 

26 h : (N, N) ndarray 

27 Hamiltonian matrix for the central region. 

28 s : {None, (N, N) ndarray}, optional 

29 Overlap matrix for the central region. 

30 Use None for an orthonormal basis. 

31 h1 : (N1, N1) ndarray 

32 Hamiltonian matrix for lead1. 

33 h2 : {None, (N2, N2) ndarray}, optional 

34 Hamiltonian matrix for lead2. You may use None if lead1 and lead2 

35 are identical. 

36 s1 : {None, (N1, N1) ndarray}, optional 

37 Overlap matrix for lead1. Use None for an orthonomormal basis. 

38 hc1 : {None, (N1, N) ndarray}, optional 

39 Hamiltonian coupling matrix between the first principal 

40 layer in lead1 and the central region. 

41 hc2 : {None, (N2, N} ndarray), optional 

42 Hamiltonian coupling matrix between the first principal 

43 layer in lead2 and the central region. 

44 sc1 : {None, (N1, N) ndarray}, optional 

45 Overlap coupling matrix between the first principal 

46 layer in lead1 and the central region. 

47 sc2 : {None, (N2, N) ndarray}, optional 

48 Overlap coupling matrix between the first principal 

49 layer in lead2 and the central region. 

50 energies : {None, array_like}, optional 

51 Energy points for which calculated transport properties are 

52 evaluated. 

53 eta : {1.0e-5, float}, optional 

54 Infinitesimal for the central region Green function. 

55 eta1/eta2 : {1.0e-5, float}, optional 

56 Infinitesimal for lead1/lead2 Green function. 

57 align_bf : {None, int}, optional 

58 Use align_bf=m to shift the central region 

59 by a constant potential such that the m'th onsite element 

60 in the central region is aligned to the m'th onsite element 

61 in lead1 principal layer. 

62 logfile : {None, str}, optional 

63 Write a logfile to file with name `logfile`. 

64 Use '-' to write to std out. 

65 eigenchannels: {0, int}, optional 

66 Number of eigenchannel transmission coefficients to 

67 calculate. 

68 pdos : {None, (N,) array_like}, optional 

69 Specify which basis functions to calculate the 

70 projected density of states for. 

71 dos : {False, bool}, optional 

72 The total density of states of the central region. 

73 box: XXX 

74 YYY 

75 

76 If hc1/hc2 are None, they are assumed to be identical to 

77 the coupling matrix elements between neareste neighbor 

78 principal layers in lead1/lead2. 

79 

80 Examples: 

81 

82 >>> import numpy as np 

83 >>> h = np.array((0,)).reshape((1,1)) 

84 >>> h1 = np.array((0, -1, -1, 0)).reshape(2,2) 

85 >>> energies = np.arange(-3, 3, 0.1) 

86 >>> calc = TransportCalculator(h=h, h1=h1, energies=energies) 

87 >>> T = calc.get_transmission() 

88 

89 """ 

90 

91 # The default values for all extra keywords 

92 self.input_parameters = {'energies': None, 

93 'h': None, 

94 'h1': None, 

95 'h2': None, 

96 's': None, 

97 's1': None, 

98 's2': None, 

99 'hc1': None, 

100 'hc2': None, 

101 'sc1': None, 

102 'sc2': None, 

103 'box': None, 

104 'align_bf': None, 

105 'eta1': 1e-5, 

106 'eta2': 1e-5, 

107 'eta': 1e-5, 

108 'logfile': None, 

109 'eigenchannels': 0, 

110 'dos': False, 

111 'pdos': []} 

112 

113 self.initialized = False # Changed Hamiltonians? 

114 self.uptodate = False # Changed energy grid? 

115 self.set(**kwargs) 

116 

117 def set(self, **kwargs): 

118 for key in kwargs: 

119 if key in ['h', 'h1', 'h2', 'hc1', 'hc2', 

120 's', 's1', 's2', 'sc1', 'sc2', 

121 'eta', 'eta1', 'eta2', 'align_bf', 'box']: 

122 self.initialized = False 

123 self.uptodate = False 

124 break 

125 elif key in ['energies', 'eigenchannels', 'dos', 'pdos']: 

126 self.uptodate = False 

127 elif key not in self.input_parameters: 

128 raise KeyError('%r not a vaild keyword' % key) 

129 

130 self.input_parameters.update(kwargs) 

131 log = self.input_parameters['logfile'] 

132 if log is None: 

133 class Trash: 

134 def write(self, s): 

135 pass 

136 

137 def flush(self): 

138 pass 

139 

140 self.log = Trash() 

141 elif log == '-': 

142 from sys import stdout 

143 self.log = stdout 

144 elif 'logfile' in kwargs: 

145 self.log = open(log, 'w') 

146 

147 def initialize(self): 

148 if self.initialized: 

149 return 

150 

151 print('# Initializing calculator...', file=self.log) 

152 

153 p = self.input_parameters 

154 if p['s'] is None: 

155 p['s'] = np.identity(len(p['h'])) 

156 

157 identical_leads = False 

158 if p['h2'] is None: 

159 p['h2'] = p['h1'] # Lead2 is idendical to lead1 

160 identical_leads = True 

161 

162 if p['s1'] is None: 

163 p['s1'] = np.identity(len(p['h1'])) 

164 

165 if identical_leads: 

166 p['s2'] = p['s1'] 

167 else: 

168 if p['s2'] is None: 

169 p['s2'] = np.identity(len(p['h2'])) 

170 

171 h_mm = p['h'] 

172 s_mm = p['s'] 

173 pl1 = len(p['h1']) // 2 

174 pl2 = len(p['h2']) // 2 

175 h1_ii = p['h1'][:pl1, :pl1] 

176 h1_ij = p['h1'][:pl1, pl1:2 * pl1] 

177 s1_ii = p['s1'][:pl1, :pl1] 

178 s1_ij = p['s1'][:pl1, pl1:2 * pl1] 

179 h2_ii = p['h2'][:pl2, :pl2] 

180 h2_ij = p['h2'][pl2: 2 * pl2, :pl2] 

181 s2_ii = p['s2'][:pl2, :pl2] 

182 s2_ij = p['s2'][pl2: 2 * pl2, :pl2] 

183 

184 if p['hc1'] is None: 

185 nbf = len(h_mm) 

186 h1_im = np.zeros((pl1, nbf), complex) 

187 s1_im = np.zeros((pl1, nbf), complex) 

188 h1_im[:pl1, :pl1] = h1_ij 

189 s1_im[:pl1, :pl1] = s1_ij 

190 p['hc1'] = h1_im 

191 p['sc1'] = s1_im 

192 else: 

193 h1_im = p['hc1'] 

194 if p['sc1'] is not None: 

195 s1_im = p['sc1'] 

196 else: 

197 s1_im = np.zeros(h1_im.shape, complex) 

198 p['sc1'] = s1_im 

199 

200 if p['hc2'] is None: 

201 h2_im = np.zeros((pl2, nbf), complex) 

202 s2_im = np.zeros((pl2, nbf), complex) 

203 h2_im[-pl2:, -pl2:] = h2_ij 

204 s2_im[-pl2:, -pl2:] = s2_ij 

205 p['hc2'] = h2_im 

206 p['sc2'] = s2_im 

207 else: 

208 h2_im = p['hc2'] 

209 if p['sc2'] is not None: 

210 s2_im = p['sc2'] 

211 else: 

212 s2_im = np.zeros(h2_im.shape, complex) 

213 p['sc2'] = s2_im 

214 

215 align_bf = p['align_bf'] 

216 if align_bf is not None: 

217 diff = ((h_mm[align_bf, align_bf] - h1_ii[align_bf, align_bf]) / 

218 s_mm[align_bf, align_bf]) 

219 print('# Aligning scat. H to left lead H. diff=', diff, 

220 file=self.log) 

221 h_mm -= diff * s_mm 

222 

223 # Setup lead self-energies 

224 # All infinitesimals must be > 0 

225 assert np.all(np.array((p['eta'], p['eta1'], p['eta2'])) > 0.0) 

226 self.selfenergies = [LeadSelfEnergy((h1_ii, s1_ii), 

227 (h1_ij, s1_ij), 

228 (h1_im, s1_im), 

229 p['eta1']), 

230 LeadSelfEnergy((h2_ii, s2_ii), 

231 (h2_ij, s2_ij), 

232 (h2_im, s2_im), 

233 p['eta2'])] 

234 box = p['box'] 

235 if box is not None: 

236 print('Using box probe!') 

237 self.selfenergies.append( 

238 BoxProbe(eta=box[0], a=box[1], b=box[2], energies=box[3], 

239 S=s_mm, T=0.3)) 

240 

241 # setup scattering green function 

242 self.greenfunction = GreenFunction(selfenergies=self.selfenergies, 

243 H=h_mm, 

244 S=s_mm, 

245 eta=p['eta']) 

246 

247 self.initialized = True 

248 

249 def update(self): 

250 if self.uptodate: 

251 return 

252 

253 p = self.input_parameters 

254 self.energies = p['energies'] 

255 nepts = len(self.energies) 

256 nchan = p['eigenchannels'] 

257 pdos = p['pdos'] 

258 self.T_e = np.empty(nepts) 

259 if p['dos']: 

260 self.dos_e = np.empty(nepts) 

261 if pdos != []: 

262 self.pdos_ne = np.empty((len(pdos), nepts)) 

263 if nchan > 0: 

264 self.eigenchannels_ne = np.empty((nchan, nepts)) 

265 

266 for e, energy in enumerate(self.energies): 

267 Ginv_mm = self.greenfunction.retarded(energy, inverse=True) 

268 lambda1_mm = self.selfenergies[0].get_lambda(energy) 

269 lambda2_mm = self.selfenergies[1].get_lambda(energy) 

270 a_mm = linalg.solve(Ginv_mm, lambda1_mm) 

271 b_mm = linalg.solve(dagger(Ginv_mm), lambda2_mm) 

272 T_mm = np.dot(a_mm, b_mm) 

273 if nchan > 0: 

274 t_n = linalg.eigvals(T_mm).real 

275 self.eigenchannels_ne[:, e] = np.sort(t_n)[-nchan:] 

276 self.T_e[e] = np.sum(t_n) 

277 else: 

278 self.T_e[e] = np.trace(T_mm).real 

279 

280 print(energy, self.T_e[e], file=self.log) 

281 self.log.flush() 

282 

283 if p['dos']: 

284 self.dos_e[e] = self.greenfunction.dos(energy) 

285 

286 if pdos != []: 

287 self.pdos_ne[:, e] = np.take(self.greenfunction.pdos(energy), 

288 pdos) 

289 

290 self.uptodate = True 

291 

292 def print_pl_convergence(self): 

293 self.initialize() 

294 pl1 = len(self.input_parameters['h1']) // 2 

295 

296 h_ii = self.selfenergies[0].h_ii 

297 s_ii = self.selfenergies[0].s_ii 

298 ha_ii = self.greenfunction.H[:pl1, :pl1] 

299 sa_ii = self.greenfunction.S[:pl1, :pl1] 

300 c1 = np.abs(h_ii - ha_ii).max() 

301 c2 = np.abs(s_ii - sa_ii).max() 

302 print(f'Conv (h,s)={c1:.2e}, {c2:2.e}') 

303 

304 def plot_pl_convergence(self): 

305 self.initialize() 

306 pl1 = len(self.input_parameters['h1']) // 2 

307 hlead = self.selfenergies[0].h_ii.real.diagonal() 

308 hprincipal = self.greenfunction.H.real.diagonal[:pl1] 

309 

310 import pylab as pl 

311 pl.plot(hlead, label='lead') 

312 pl.plot(hprincipal, label='principal layer') 

313 pl.axis('tight') 

314 pl.show() 

315 

316 def get_current(self, bias, T=0., E=None, T_e=None, spinpol=False): 

317 '''Returns the current as a function of the 

318 bias voltage. 

319 

320 **Parameters:** 

321 bias : {float, (M,) ndarray}, units: V 

322 Specifies the bias voltage. 

323 T : {float}, units: K, optional 

324 Specifies the temperature. 

325 E : {(N,) ndarray}, units: eV, optional 

326 Contains energy grid of the transmission function. 

327 T_e {(N,) ndarray}, units: unitless, optional 

328 Contains the transmission function. 

329 spinpol: {bool}, optional 

330 Specifies whether the current should be 

331 calculated assuming degenerate spins 

332 

333 **Returns:** 

334 I : {float, (M,) ndarray}, units: 2e/h*eV 

335 Contains the electric current. 

336 

337 Examples: 

338 

339 >> import numpy as np 

340 >> import pylab as plt 

341 >> from ase import units 

342 >> 

343 >> bias = np.arange(0, 2, .1) 

344 >> current = calc.get_current(bias, T = 0.) 

345 >> plt.plot(bias, 2.*units._e**2/units._hplanck*current) 

346 >> plt.xlabel('U [V]') 

347 >> plt.ylabel('I [A]') 

348 >> plt.show() 

349 

350 ''' 

351 if E is not None: 

352 if T_e is None: 

353 self.energies = E 

354 self.uptodate = False 

355 T_e = self.get_transmission().copy() 

356 else: 

357 assert self.uptodate, ('Energy grid and transmission function ' 

358 'not defined.') 

359 E = self.energies.copy() 

360 T_e = self.T_e.copy() 

361 

362 if not isinstance(bias, (int, float)): 

363 bias = bias[np.newaxis] 

364 E = E[:, np.newaxis] 

365 T_e = T_e[:, np.newaxis] 

366 

367 fl = fermidistribution(E - bias / 2., kB * T) 

368 fr = fermidistribution(E + bias / 2., kB * T) 

369 

370 if spinpol: 

371 return .5 * np.trapz((fl - fr) * T_e, x=E, axis=0) 

372 else: 

373 return np.trapz((fl - fr) * T_e, x=E, axis=0) 

374 

375 def get_transmission(self): 

376 self.initialize() 

377 self.update() 

378 return self.T_e 

379 

380 def get_dos(self): 

381 self.initialize() 

382 self.update() 

383 return self.dos_e 

384 

385 def get_eigenchannels(self, n=None): 

386 """Get ``n`` first eigenchannels.""" 

387 self.initialize() 

388 self.update() 

389 if n is None: 

390 n = self.input_parameters['eigenchannels'] 

391 return self.eigenchannels_ne[:n] 

392 

393 def get_pdos(self): 

394 self.initialize() 

395 self.update() 

396 return self.pdos_ne 

397 

398 def subdiagonalize_bfs(self, bfs, apply=False): 

399 self.initialize() 

400 bfs = np.array(bfs) 

401 p = self.input_parameters 

402 h_mm = p['h'] 

403 s_mm = p['s'] 

404 ht_mm, st_mm, c_mm, e_m = subdiagonalize(h_mm, s_mm, bfs) 

405 if apply: 

406 self.uptodate = False 

407 h_mm[:] = ht_mm.real 

408 s_mm[:] = st_mm.real 

409 # Rotate coupling between lead and central region 

410 for alpha, sigma in enumerate(self.selfenergies): 

411 sigma.h_im[:] = np.dot(sigma.h_im, c_mm) 

412 sigma.s_im[:] = np.dot(sigma.s_im, c_mm) 

413 

414 c_mm = np.take(c_mm, bfs, axis=0) 

415 c_mm = np.take(c_mm, bfs, axis=1) 

416 return ht_mm, st_mm, e_m.real, c_mm 

417 

418 def cutcoupling_bfs(self, bfs, apply=False): 

419 self.initialize() 

420 bfs = np.array(bfs) 

421 p = self.input_parameters 

422 h_pp = p['h'].copy() 

423 s_pp = p['s'].copy() 

424 cutcoupling(h_pp, s_pp, bfs) 

425 if apply: 

426 self.uptodate = False 

427 p['h'][:] = h_pp 

428 p['s'][:] = s_pp 

429 for alpha, sigma in enumerate(self.selfenergies): 

430 for m in bfs: 

431 sigma.h_im[:, m] = 0.0 

432 sigma.s_im[:, m] = 0.0 

433 return h_pp, s_pp 

434 

435 def lowdin_rotation(self, apply=False): 

436 p = self.input_parameters 

437 h_mm = p['h'] 

438 s_mm = p['s'] 

439 eig, rot_mm = linalg.eigh(s_mm) 

440 eig = np.abs(eig) 

441 rot_mm = np.dot(rot_mm / np.sqrt(eig), dagger(rot_mm)) 

442 if apply: 

443 self.uptodate = False 

444 h_mm[:] = rotate_matrix(h_mm, rot_mm) # rotate C region 

445 s_mm[:] = rotate_matrix(s_mm, rot_mm) 

446 for alpha, sigma in enumerate(self.selfenergies): 

447 sigma.h_im[:] = np.dot(sigma.h_im, rot_mm) # rotate L-C coupl. 

448 sigma.s_im[:] = np.dot(sigma.s_im, rot_mm) 

449 

450 return rot_mm 

451 

452 def get_left_channels(self, energy, nchan=1): 

453 self.initialize() 

454 g_s_ii = self.greenfunction.retarded(energy) 

455 lambda_l_ii = self.selfenergies[0].get_lambda(energy) 

456 lambda_r_ii = self.selfenergies[1].get_lambda(energy) 

457 

458 if self.greenfunction.S is not None: 

459 s_mm = self.greenfunction.S 

460 s_s_i, s_s_ii = linalg.eig(s_mm) 

461 s_s_i = np.abs(s_s_i) 

462 s_s_sqrt_i = np.sqrt(s_s_i) # sqrt of eigenvalues 

463 s_s_sqrt_ii = np.dot(s_s_ii * s_s_sqrt_i, dagger(s_s_ii)) 

464 s_s_isqrt_ii = np.dot(s_s_ii / s_s_sqrt_i, dagger(s_s_ii)) 

465 

466 lambdab_r_ii = np.dot(np.dot(s_s_isqrt_ii, lambda_r_ii), s_s_isqrt_ii) 

467 a_l_ii = np.dot(np.dot(g_s_ii, lambda_l_ii), dagger(g_s_ii)) 

468 ab_l_ii = np.dot(np.dot(s_s_sqrt_ii, a_l_ii), s_s_sqrt_ii) 

469 lambda_i, u_ii = linalg.eig(ab_l_ii) 

470 ut_ii = np.sqrt(lambda_i / (2.0 * np.pi)) * u_ii 

471 m_ii = 2 * np.pi * np.dot(np.dot(dagger(ut_ii), lambdab_r_ii), ut_ii) 

472 T_i, c_in = linalg.eig(m_ii) 

473 T_i = np.abs(T_i) 

474 

475 channels = np.argsort(-T_i)[:nchan] 

476 c_in = np.take(c_in, channels, axis=1) 

477 T_n = np.take(T_i, channels) 

478 v_in = np.dot(np.dot(s_s_isqrt_ii, ut_ii), c_in) 

479 

480 return T_n, v_in