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
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
4from numpy import linalg
5from scipy.integrate import trapezoid
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
19class TransportCalculator:
20 """Determine transport properties of a device sandwiched between
21 two semi-infinite leads using a Green function method.
22 """
24 def __init__(self, **kwargs):
25 """Create the transport calculator.
27 Parameters:
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
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.
83 Examples:
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()
92 """
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': []}
116 self.initialized = False # Changed Hamiltonians?
117 self.uptodate = False # Changed energy grid?
118 self.set(**kwargs)
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)
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
140 def flush(self):
141 pass
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')
150 def initialize(self):
151 if self.initialized:
152 return
154 print('# Initializing calculator...', file=self.log)
156 p = self.input_parameters
157 if p['s'] is None:
158 p['s'] = np.identity(len(p['h']))
160 identical_leads = False
161 if p['h2'] is None:
162 p['h2'] = p['h1'] # Lead2 is idendical to lead1
163 identical_leads = True
165 if p['s1'] is None:
166 p['s1'] = np.identity(len(p['h1']))
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']))
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]
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
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
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
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))
244 # setup scattering green function
245 self.greenfunction = GreenFunction(selfenergies=self.selfenergies,
246 H=h_mm,
247 S=s_mm,
248 eta=p['eta'])
250 self.initialized = True
252 def update(self):
253 if self.uptodate:
254 return
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))
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
283 print(energy, self.T_e[e], file=self.log)
284 self.log.flush()
286 if p['dos']:
287 self.dos_e[e] = self.greenfunction.dos(energy)
289 if pdos != []:
290 self.pdos_ne[:, e] = np.take(self.greenfunction.pdos(energy),
291 pdos)
293 self.uptodate = True
295 def print_pl_convergence(self):
296 self.initialize()
297 pl1 = len(self.input_parameters['h1']) // 2
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}')
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]
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()
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.
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
336 **Returns:**
337 I : {float, (M,) ndarray}, units: 2e/h*eV
338 Contains the electric current.
340 Examples:
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()
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()
365 if not isinstance(bias, (int, float)):
366 bias = bias[np.newaxis]
367 E = E[:, np.newaxis]
368 T_e = T_e[:, np.newaxis]
370 fl = fermidistribution(E - bias / 2., kB * T)
371 fr = fermidistribution(E + bias / 2., kB * T)
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)
378 def get_transmission(self):
379 self.initialize()
380 self.update()
381 return self.T_e
383 def get_dos(self):
384 self.initialize()
385 self.update()
386 return self.dos_e
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]
396 def get_pdos(self):
397 self.initialize()
398 self.update()
399 return self.pdos_ne
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)
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
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
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)
453 return rot_mm
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)
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))
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)
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)
483 return T_n, v_in