Coverage for /builds/ase/ase/ase/transport/calculators.py: 60.30%

267 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3import numpy as np 

4from numpy import linalg 

5from scipy.integrate import trapezoid 

6 

7from ase.transport.greenfunction import GreenFunction 

8from ase.transport.selfenergy import BoxProbe, LeadSelfEnergy 

9from ase.transport.tools import ( 

10 cutcoupling, 

11 dagger, 

12 fermidistribution, 

13 rotate_matrix, 

14 subdiagonalize, 

15) 

16from ase.units import kB 

17 

18 

19class TransportCalculator: 

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

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

22 """ 

23 

24 def __init__(self, **kwargs): 

25 """Create the transport calculator. 

26 

27 Parameters: 

28 

29 h : (N, N) ndarray 

30 Hamiltonian matrix for the central region. 

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

32 Overlap matrix for the central region. 

33 Use None for an orthonormal basis. 

34 h1 : (N1, N1) ndarray 

35 Hamiltonian matrix for lead1. 

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

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

38 are identical. 

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

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

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

42 Hamiltonian coupling matrix between the first principal 

43 layer in lead1 and the central region. 

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

45 Hamiltonian coupling matrix between the first principal 

46 layer in lead2 and the central region. 

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

48 Overlap coupling matrix between the first principal 

49 layer in lead1 and the central region. 

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

51 Overlap coupling matrix between the first principal 

52 layer in lead2 and the central region. 

53 energies : {None, array_like}, optional 

54 Energy points for which calculated transport properties are 

55 evaluated. 

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

57 Infinitesimal for the central region Green function. 

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

59 Infinitesimal for lead1/lead2 Green function. 

60 align_bf : {None, int}, optional 

61 Use align_bf=m to shift the central region 

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

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

64 in lead1 principal layer. 

65 logfile : {None, str}, optional 

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

67 Use '-' to write to std out. 

68 eigenchannels: {0, int}, optional 

69 Number of eigenchannel transmission coefficients to 

70 calculate. 

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

72 Specify which basis functions to calculate the 

73 projected density of states for. 

74 dos : {False, bool}, optional 

75 The total density of states of the central region. 

76 box: XXX 

77 YYY 

78 

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

80 the coupling matrix elements between neareste neighbor 

81 principal layers in lead1/lead2. 

82 

83 Examples: 

84 

85 >>> import numpy as np 

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

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

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

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

90 >>> T = calc.get_transmission() 

91 

92 """ 

93 

94 # The default values for all extra keywords 

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

96 'h': None, 

97 'h1': None, 

98 'h2': None, 

99 's': None, 

100 's1': None, 

101 's2': None, 

102 'hc1': None, 

103 'hc2': None, 

104 'sc1': None, 

105 'sc2': None, 

106 'box': None, 

107 'align_bf': None, 

108 'eta1': 1e-5, 

109 'eta2': 1e-5, 

110 'eta': 1e-5, 

111 'logfile': None, 

112 'eigenchannels': 0, 

113 'dos': False, 

114 'pdos': []} 

115 

116 self.initialized = False # Changed Hamiltonians? 

117 self.uptodate = False # Changed energy grid? 

118 self.set(**kwargs) 

119 

120 def set(self, **kwargs): 

121 for key in kwargs: 

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

123 's', 's1', 's2', 'sc1', 'sc2', 

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

125 self.initialized = False 

126 self.uptodate = False 

127 break 

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

129 self.uptodate = False 

130 elif key not in self.input_parameters: 

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

132 

133 self.input_parameters.update(kwargs) 

134 log = self.input_parameters['logfile'] 

135 if log is None: 

136 class Trash: 

137 def write(self, s): 

138 pass 

139 

140 def flush(self): 

141 pass 

142 

143 self.log = Trash() 

144 elif log == '-': 

145 from sys import stdout 

146 self.log = stdout 

147 elif 'logfile' in kwargs: 

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

149 

150 def initialize(self): 

151 if self.initialized: 

152 return 

153 

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

155 

156 p = self.input_parameters 

157 if p['s'] is None: 

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

159 

160 identical_leads = False 

161 if p['h2'] is None: 

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

163 identical_leads = True 

164 

165 if p['s1'] is None: 

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

167 

168 if identical_leads: 

169 p['s2'] = p['s1'] 

170 else: 

171 if p['s2'] is None: 

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

173 

174 h_mm = p['h'] 

175 s_mm = p['s'] 

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

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

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

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

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

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

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

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

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

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

186 

187 if p['hc1'] is None: 

188 nbf = len(h_mm) 

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

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

191 h1_im[:pl1, :pl1] = h1_ij 

192 s1_im[:pl1, :pl1] = s1_ij 

193 p['hc1'] = h1_im 

194 p['sc1'] = s1_im 

195 else: 

196 h1_im = p['hc1'] 

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

198 s1_im = p['sc1'] 

199 else: 

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

201 p['sc1'] = s1_im 

202 

203 if p['hc2'] is None: 

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

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

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

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

208 p['hc2'] = h2_im 

209 p['sc2'] = s2_im 

210 else: 

211 h2_im = p['hc2'] 

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

213 s2_im = p['sc2'] 

214 else: 

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

216 p['sc2'] = s2_im 

217 

218 align_bf = p['align_bf'] 

219 if align_bf is not None: 

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

221 s_mm[align_bf, align_bf]) 

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

223 file=self.log) 

224 h_mm -= diff * s_mm 

225 

226 # Setup lead self-energies 

227 # All infinitesimals must be > 0 

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

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

230 (h1_ij, s1_ij), 

231 (h1_im, s1_im), 

232 p['eta1']), 

233 LeadSelfEnergy((h2_ii, s2_ii), 

234 (h2_ij, s2_ij), 

235 (h2_im, s2_im), 

236 p['eta2'])] 

237 box = p['box'] 

238 if box is not None: 

239 print('Using box probe!') 

240 self.selfenergies.append( 

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

242 S=s_mm, T=0.3)) 

243 

244 # setup scattering green function 

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

246 H=h_mm, 

247 S=s_mm, 

248 eta=p['eta']) 

249 

250 self.initialized = True 

251 

252 def update(self): 

253 if self.uptodate: 

254 return 

255 

256 p = self.input_parameters 

257 self.energies = p['energies'] 

258 nepts = len(self.energies) 

259 nchan = p['eigenchannels'] 

260 pdos = p['pdos'] 

261 self.T_e = np.empty(nepts) 

262 if p['dos']: 

263 self.dos_e = np.empty(nepts) 

264 if pdos != []: 

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

266 if nchan > 0: 

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

268 

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

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

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

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

273 a_mm = linalg.solve(Ginv_mm, lambda1_mm) 

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

275 T_mm = np.dot(a_mm, b_mm) 

276 if nchan > 0: 

277 t_n = linalg.eigvals(T_mm).real 

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

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

280 else: 

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

282 

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

284 self.log.flush() 

285 

286 if p['dos']: 

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

288 

289 if pdos != []: 

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

291 pdos) 

292 

293 self.uptodate = True 

294 

295 def print_pl_convergence(self): 

296 self.initialize() 

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

298 

299 h_ii = self.selfenergies[0].h_ii 

300 s_ii = self.selfenergies[0].s_ii 

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

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

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

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

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

306 

307 def plot_pl_convergence(self): 

308 self.initialize() 

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

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

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

312 

313 import pylab as pl 

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

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

316 pl.axis('tight') 

317 pl.show() 

318 

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

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

321 bias voltage. 

322 

323 **Parameters:** 

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

325 Specifies the bias voltage. 

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

327 Specifies the temperature. 

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

329 Contains energy grid of the transmission function. 

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

331 Contains the transmission function. 

332 spinpol: {bool}, optional 

333 Specifies whether the current should be 

334 calculated assuming degenerate spins 

335 

336 **Returns:** 

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

338 Contains the electric current. 

339 

340 Examples: 

341 

342 >> import numpy as np 

343 >> import pylab as plt 

344 >> from ase import units 

345 >> 

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

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

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

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

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

351 >> plt.show() 

352 

353 ''' 

354 if E is not None: 

355 if T_e is None: 

356 self.energies = E 

357 self.uptodate = False 

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

359 else: 

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

361 'not defined.') 

362 E = self.energies.copy() 

363 T_e = self.T_e.copy() 

364 

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

366 bias = bias[np.newaxis] 

367 E = E[:, np.newaxis] 

368 T_e = T_e[:, np.newaxis] 

369 

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

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

372 

373 if spinpol: 

374 return .5 * trapezoid((fl - fr) * T_e, x=E, axis=0) 

375 else: 

376 return trapezoid((fl - fr) * T_e, x=E, axis=0) 

377 

378 def get_transmission(self): 

379 self.initialize() 

380 self.update() 

381 return self.T_e 

382 

383 def get_dos(self): 

384 self.initialize() 

385 self.update() 

386 return self.dos_e 

387 

388 def get_eigenchannels(self, n=None): 

389 """Get ``n`` first eigenchannels.""" 

390 self.initialize() 

391 self.update() 

392 if n is None: 

393 n = self.input_parameters['eigenchannels'] 

394 return self.eigenchannels_ne[:n] 

395 

396 def get_pdos(self): 

397 self.initialize() 

398 self.update() 

399 return self.pdos_ne 

400 

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

402 self.initialize() 

403 bfs = np.array(bfs) 

404 p = self.input_parameters 

405 h_mm = p['h'] 

406 s_mm = p['s'] 

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

408 if apply: 

409 self.uptodate = False 

410 h_mm[:] = ht_mm.real 

411 s_mm[:] = st_mm.real 

412 # Rotate coupling between lead and central region 

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

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

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

416 

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

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

419 return ht_mm, st_mm, e_m.real, c_mm 

420 

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

422 self.initialize() 

423 bfs = np.array(bfs) 

424 p = self.input_parameters 

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

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

427 cutcoupling(h_pp, s_pp, bfs) 

428 if apply: 

429 self.uptodate = False 

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

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

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

433 for m in bfs: 

434 sigma.h_im[:, m] = 0.0 

435 sigma.s_im[:, m] = 0.0 

436 return h_pp, s_pp 

437 

438 def lowdin_rotation(self, apply=False): 

439 p = self.input_parameters 

440 h_mm = p['h'] 

441 s_mm = p['s'] 

442 eig, rot_mm = linalg.eigh(s_mm) 

443 eig = np.abs(eig) 

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

445 if apply: 

446 self.uptodate = False 

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

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

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

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

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

452 

453 return rot_mm 

454 

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

456 self.initialize() 

457 g_s_ii = self.greenfunction.retarded(energy) 

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

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

460 

461 if self.greenfunction.S is not None: 

462 s_mm = self.greenfunction.S 

463 s_s_i, s_s_ii = linalg.eig(s_mm) 

464 s_s_i = np.abs(s_s_i) 

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

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

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

468 

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

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

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

472 lambda_i, u_ii = linalg.eig(ab_l_ii) 

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

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

475 T_i, c_in = linalg.eig(m_ii) 

476 T_i = np.abs(T_i) 

477 

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

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

480 T_n = np.take(T_i, channels) 

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

482 

483 return T_n, v_in