Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright 2011-2016 Kwant authors. 

2# 

3# This file is part of Kwant. It is subject to the license terms in the file 

4# LICENSE.rst found in the top-level directory of this distribution and at 

5# https://kwant-project.org/license. A list of Kwant authors can be found in 

6# the file AUTHORS.rst at the top-level directory of this distribution and at 

7# https://kwant-project.org/authors. 

8 

9 

10from math import sin, cos, sqrt, pi, copysign 

11from collections import namedtuple 

12 

13from itertools import combinations_with_replacement 

14import numpy as np 

15import numpy.linalg as npl 

16import scipy.linalg as la 

17from .. import linalg as kla 

18from scipy.linalg import block_diag 

19from scipy.sparse import (identity as sp_identity, hstack as sp_hstack, 

20 csr_matrix) 

21 

22dot = np.dot 

23 

24__all__ = ['selfenergy', 'modes', 'PropagatingModes', 'StabilizedModes'] 

25 

26 

27def nonzero_symm_projection(matrix): 

28 """Check whether a discrete symmetry relation between two blocks of the 

29 Hamiltonian vanishes or not. 

30 

31 For a discrete symmetry S, the projection that maps from the block i to 

32 block j of the Hamiltonian with projectors P_i and P_j is 

33 S_ji = P_j^+ S P_i. 

34 This function determines whether S_ji vanishes or not, i. e. whether a 

35 symmetry relation exists between blocks i and j. 

36 

37 Here, discrete symmetries and projectors are in canonical form, such that 

38 S maps each block at most either to itself or to a single other block. 

39 In other words, for each j, the block S_ji is nonzero at most for one i, 

40 while all other blocks vanish. If a nonzero block exists, it is unitary 

41 and hence has norm 1. To identify nonzero blocks without worrying about 

42 numerical errors, we thus check that its norm is larger than 0.5. 

43 """ 

44 if not isinstance(matrix, np.ndarray): 

45 matrix = matrix.data 

46 return np.linalg.norm(matrix) > 0.5 

47 

48 

49def group_halves(arr_list): 

50 """Split and rearrange a list of arrays. 

51 

52 Combine a list of arrays into a single array where the first halves 

53 of each array appear first: 

54 `[a b], [c d], [e f] -> [a c e b d f]` 

55 """ 

56 list_ = [np.split(arr, 2) for arr in arr_list] 

57 lefts, rights = zip(*list_) 

58 return np.r_[tuple(lefts + rights)] 

59 

60 

61# Container classes 

62Linsys = namedtuple('Linsys', ['eigenproblem', 'v', 'extract']) 

63 

64 

65class PropagatingModes: 

66 """The calculated propagating modes of a lead. 

67 

68 Attributes 

69 ---------- 

70 wave_functions : numpy array 

71 The wave functions of the propagating modes. 

72 momenta : numpy array 

73 Momenta of the modes. 

74 velocities : numpy array 

75 Velocities of the modes. 

76 block_nmodes: list of integers 

77 Number of left or right moving propagating modes 

78 per conservation law block of the Hamiltonian. 

79 

80 Notes 

81 ===== 

82 The sort order of all the three arrays is identical. The first half of the 

83 modes have negative velocity, the second half have positive velocity. 

84 Within these halves the modes are ordered by the eigenvalues of any 

85 declared conservation laws. Within blocks with the same conservation law 

86 eigenvalue the modes with negative velocity are ordered by increasing 

87 momentum, and the modes with positive velocity are ordered by decreasing 

88 momentum. Finally, modes are ordered by the magnitude of their velocity. 

89 To summarize, the modes are ordered according to the key 

90 `(sign(v), conserved_quantity, sign(v) * k , abs(v))` where `v` is 

91 velocity, `k` is momentum and `conserved_quantity` is the conservation 

92 law eigenvalue. 

93 

94 In the above, the positive velocity and momentum directions are defined 

95 with respect to the translational symmetry direction of the system. 

96 

97 The first dimension of `wave_functions` corresponds to the orbitals of all 

98 the sites in a unit cell, the second one to the number of the mode. Each 

99 mode is normalized to carry unit current. If several modes have the same 

100 momentum and velocity, an arbitrary orthonormal basis in the subspace of 

101 these modes is chosen. 

102 

103 If a conservation law is specified to block diagonalize the Hamiltonian, 

104 then `block_nmodes[i]` is the number of left or right moving propagating 

105 modes in conservation law block `i`. The ordering of blocks is the same as 

106 the ordering of the projectors used to block diagonalize the Hamiltonian. 

107 """ 

108 def __init__(self, wave_functions, velocities, momenta): 

109 kwargs = locals() 

110 kwargs.pop('self') 

111 self.__dict__.update(kwargs) 

112 

113 

114class StabilizedModes: 

115 """Stabilized eigendecomposition of the translation operator. 

116 

117 Due to the lack of Hermiticity of the translation operator, its 

118 eigendecomposition is frequently poorly conditioned. Solvers in Kwant use 

119 this stabilized decomposition of the propagating and evanescent modes in 

120 the leads. If the hopping between the unit cells of an infinite system is 

121 invertible, the translation eigenproblem is written in the basis `psi_n, 

122 h_hop^+ * psi_(n+1)`, with ``h_hop`` the hopping between unit cells. If 

123 `h_hop` is not invertible, and has the singular value decomposition `u s 

124 v^+`, then the eigenproblem is written in the basis `sqrt(s) v^+ psi_n, 

125 sqrt(s) u^+ psi_(n+1)`. In this basis we calculate the eigenvectors of the 

126 propagating modes, and the Schur vectors (an orthogonal basis) in the space 

127 of evanescent modes. 

128 

129 `vecs` and `vecslmbdainv` are the first and the second halves of the wave 

130 functions. The first `nmodes` are eigenmodes moving in the negative 

131 direction (hence they are incoming into the system in Kwant convention), 

132 the second `nmodes` are eigenmodes moving in the positive direction. The 

133 remaining modes are the Schur vectors of the modes evanescent in the 

134 positive direction. Propagating modes with the same eigenvalue are 

135 orthogonalized, and all the propagating modes are normalized to carry unit 

136 current. Finally the `sqrt_hop` attribute is `v sqrt(s)`. 

137 

138 Attributes 

139 ---------- 

140 vecs : numpy array 

141 Translation eigenvectors. 

142 vecslmbdainv : numpy array 

143 Translation eigenvectors divided by the corresponding eigenvalues. 

144 nmodes : int 

145 Number of left-moving (or right-moving) modes. 

146 sqrt_hop : numpy array 

147 Part of the SVD of `h_hop`. 

148 """ 

149 

150 def __init__(self, vecs, vecslmbdainv, nmodes, sqrt_hop): 

151 kwargs = locals() 

152 kwargs.pop('self') 

153 self.__dict__.update(kwargs) 

154 

155 def selfenergy(self): 

156 """ 

157 Compute the self-energy generated by lead modes. 

158 

159 Returns 

160 ------- 

161 Sigma : numpy array, real or complex, shape (M,M) 

162 The computed self-energy. Note that even if `h_cell` and `h_hop` are 

163 both real, `Sigma` will typically be complex. (More precisely, if 

164 there is a propagating mode, `Sigma` will definitely be complex.) 

165 """ 

166 v = self.sqrt_hop 

167 # Only include the *outgoing* modes (propagating + evanescent), 

168 # meaning this is the *retarded* self-energy. 

169 outgoing = slice(self.nmodes, None) 

170 vecs = self.vecs[:, outgoing] 

171 vecslmbdainv = self.vecslmbdainv[:, outgoing] 

172 return dot(v, dot(vecs, la.solve(vecslmbdainv, v.T.conj()))) 

173 

174 

175# Auxiliary functions that perform different parts of the calculation. 

176def setup_linsys(h_cell, h_hop, tol=1e6, stabilization=None): 

177 """Make an eigenvalue problem for eigenvectors of translation operator. 

178 

179 Parameters 

180 ---------- 

181 h_cell : numpy array with shape (n, n) 

182 Hamiltonian of a single lead unit cell. 

183 h_hop : numpy array with shape (n, m), m <= n 

184 Hopping Hamiltonian from a cell to the next one. 

185 tol : float 

186 Numbers are considered zero when they are smaller than `tol` times 

187 the machine precision. 

188 stabilization : sequence of 2 booleans or None 

189 Which steps of the eigenvalue problem stabilization to perform. If the 

190 value is `None`, then Kwant chooses the fastest (and least stable) 

191 algorithm that is expected to be sufficient. For any other value, 

192 Kwant forms the eigenvalue problem in the basis of the hopping singular 

193 values. The first element set to `True` forces Kwant to add an 

194 anti-Hermitian term to the cell Hamiltonian before inverting. If it is 

195 set to `False`, the extra term will only be added if the cell 

196 Hamiltonian isn't invertible. The second element set to `True` forces 

197 Kwant to solve a generalized eigenvalue problem, and not to reduce it 

198 to the regular one. If it is `False`, reduction to a regular problem 

199 is performed if possible. 

200 

201 Returns 

202 ------- 

203 linsys : namedtuple 

204 A named tuple containing `matrices` a matrix pencil defining 

205 the eigenproblem, `v` a hermitian conjugate of the last matrix in 

206 the hopping singular value decomposition, and functions for 

207 extracting the wave function in the unit cell from the wave function 

208 in the basis of the nonzero singular exponents of the hopping. 

209 

210 Notes 

211 ----- 

212 The lead problem with degenerate hopping is rather complicated, and the 

213 details of the algorithm will be published elsewhere. 

214 

215 """ 

216 n = h_cell.shape[0] 

217 m = h_hop.shape[1] 

218 if stabilization is not None: 

219 stabilization = list(stabilization) 

220 

221 if not np.any(h_hop): # skip coverage 

222 # Inter-cell hopping is zero. The current algorithm is not suited to 

223 # treat this extremely singular case. 

224 raise ValueError("Inter-cell hopping is exactly zero.") 

225 

226 # If both h and t are real, it may be possible to use the real eigenproblem. 

227 if (not np.any(h_hop.imag)) and (not np.any(h_cell.imag)): 

228 h_hop = h_hop.real 

229 h_cell = h_cell.real 

230 

231 eps = np.finfo(np.common_type(h_cell, h_hop)).eps * tol 

232 

233 # First check if the hopping matrix has singular values close to 0. 

234 # (Close to zero is defined here as |x| < eps * tol * s[0] , where 

235 # s[0] is the largest singular value.) 

236 

237 u, s, vh = la.svd(h_hop) 

238 assert m == vh.shape[1], "Corrupt output of svd." 

239 n_nonsing = np.sum(s > eps * s[0]) 

240 

241 if (n_nonsing == n and stabilization is None): 

242 # The hopping matrix is well-conditioned and can be safely inverted. 

243 # Hence the regular transfer matrix may be used. 

244 h_hop_sqrt = sqrt(np.linalg.norm(h_hop)) 

245 A = h_hop / h_hop_sqrt 

246 B = h_hop_sqrt 

247 B_H_inv = 1.0 / B # just a real scalar here 

248 A_inv = la.inv(A) 

249 

250 lhs = np.zeros((2*n, 2*n), dtype=np.common_type(h_cell, h_hop)) 

251 lhs[:n, :n] = -dot(A_inv, h_cell) * B_H_inv 

252 lhs[:n, n:] = -A_inv * B 

253 lhs[n:, :n] = A.T.conj() * B_H_inv 

254 

255 def extract_wf(psi, lmbdainv): 

256 return B_H_inv * np.copy(psi[:n]) 

257 

258 matrices = (lhs, None) 

259 v_out = h_hop_sqrt * np.eye(n) 

260 else: 

261 if stabilization is None: 

262 stabilization = [None, False] 

263 

264 # The hopping matrix has eigenvalues close to 0 - those 

265 # need to be eliminated. 

266 

267 # Recast the svd of h_hop = u s v^dagger such that 

268 # u, v are matrices with shape n x n_nonsing. 

269 u = u[:, :n_nonsing] 

270 s = s[:n_nonsing] 

271 u = u * np.sqrt(s) 

272 # pad v with zeros if necessary 

273 v = np.zeros((n, n_nonsing), dtype=vh.dtype) 

274 v[:vh.shape[1]] = vh[:n_nonsing].T.conj() 

275 v = v * np.sqrt(s) 

276 

277 # Eliminating the zero eigenvalues requires inverting the on-site 

278 # Hamiltonian, possibly including a self-energy-like term. The 

279 # self-energy-like term stabilizes the inversion, but the most stable 

280 # choice is inherently complex. This can be disadvantageous if the 

281 # Hamiltonian is real, since staying in real arithmetics can be 

282 # significantly faster. The strategy here is to add a complex 

283 # self-energy-like term always if the original Hamiltonian is complex, 

284 # and check for invertibility first if it is real 

285 

286 matrices_real = issubclass(np.common_type(h_cell, h_hop), np.floating) 

287 add_imaginary = stabilization[0] or ((stabilization[0] is None) and 

288 not matrices_real) 

289 # Check if there is a chance we will not need to add an imaginary term. 

290 if not add_imaginary: 

291 h = h_cell 

292 sol = kla.lu_factor(h) 

293 rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) 

294 

295 if rcond < eps: 

296 need_to_stabilize = True 

297 else: 

298 need_to_stabilize = False 

299 

300 if add_imaginary or need_to_stabilize: 

301 need_to_stabilize = True 

302 # Matrices are complex or need self-energy-like term to be 

303 # stabilized. 

304 temp = dot(u, u.T.conj()) + dot(v, v.T.conj()) 

305 h = h_cell + 1j * temp 

306 

307 sol = kla.lu_factor(h) 

308 rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) 

309 

310 # If the condition number of the stabilized h is 

311 # still bad, there is nothing we can do. 

312 if rcond < eps: 312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true

313 raise RuntimeError("Flat band encountered at the requested " 

314 "energy, result is badly defined.") 

315 

316 # The function that can extract the full wave function psi from 

317 # the projected one (v^dagger psi lambda^-1, s u^dagger psi). 

318 

319 def extract_wf(psi, lmbdainv): 

320 wf = -dot(u, psi[: n_nonsing] * lmbdainv) - dot(v, psi[n_nonsing:]) 

321 if need_to_stabilize: 

322 wf += 1j * (dot(v, psi[: n_nonsing]) + 

323 dot(u, psi[n_nonsing:] * lmbdainv)) 

324 return kla.lu_solve(sol, wf) 

325 

326 # Setup the generalized eigenvalue problem. 

327 

328 A = np.zeros((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) 

329 B = np.zeros((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) 

330 

331 begin, end = slice(n_nonsing), slice(n_nonsing, None) 

332 

333 A[end, begin] = np.identity(n_nonsing) 

334 temp = kla.lu_solve(sol, v) 

335 temp2 = dot(u.T.conj(), temp) 

336 if need_to_stabilize: 

337 A[begin, begin] = -1j * temp2 

338 A[begin, end] = temp2 

339 temp2 = dot(v.T.conj(), temp) 

340 if need_to_stabilize: 

341 A[end, begin] -= 1j *temp2 

342 A[end, end] = temp2 

343 

344 B[begin, end] = -np.identity(n_nonsing) 

345 temp = kla.lu_solve(sol, u) 

346 temp2 = dot(u.T.conj(), temp) 

347 B[begin, begin] = -temp2 

348 if need_to_stabilize: 

349 B[begin, end] += 1j * temp2 

350 temp2 = dot(v.T.conj(), temp) 

351 B[end, begin] = -temp2 

352 if need_to_stabilize: 

353 B[end, end] = 1j * temp2 

354 

355 v_out = v[:m] 

356 

357 # Solving a generalized eigenproblem is about twice as expensive 

358 # as solving a regular eigenvalue problem. 

359 # Computing the LU factorization is negligible compared to both 

360 # (approximately 1/30th of a regular eigenvalue problem). 

361 # Because of this, it makes sense to try to reduce 

362 # the generalized eigenvalue problem to a regular one, provided 

363 # the matrix B can be safely inverted. 

364 

365 lu_b = kla.lu_factor(B) 

366 if not stabilization[1]: 

367 rcond = kla.rcond_from_lu(lu_b, npl.norm(B, 1)) 

368 # A more stringent condition is used here since errors can 

369 # accumulate from here to the eigenvalue calculation later. 

370 stabilization[1] = rcond > eps * tol 

371 

372 if stabilization[1]: 

373 matrices = (kla.lu_solve(lu_b, A), None) 

374 else: 

375 matrices = (A, B) 

376 return Linsys(matrices, v_out, extract_wf) 

377 

378 

379def unified_eigenproblem(a, b=None, tol=1e6): 

380 """A helper routine for modes(), that wraps eigenproblems. 

381 

382 This routine wraps the regular and general eigenproblems that can arise 

383 in a unfied way. 

384 

385 Parameters 

386 ---------- 

387 a : numpy array 

388 The matrix on the left hand side of a regular or generalized eigenvalue 

389 problem. 

390 b : numpy array or None 

391 The matrix on the right hand side of the generalized eigenvalue problem. 

392 tol : float 

393 The tolerance for separating eigenvalues with absolute value 1 from the 

394 rest. 

395 

396 Returns 

397 ------- 

398 ev : numpy array 

399 An array of eigenvalues (can contain NaNs and Infs, but those 

400 are not accessed in `modes()`) The number of eigenvalues equals 

401 twice the number of nonzero singular values of 

402 `h_hop` (so `2*h_cell.shape[0]` if `h_hop` is invertible). 

403 evanselect : numpy integer array 

404 Index array of right-decaying modes. 

405 propselect : numpy integer array 

406 Index array of propagating modes (both left and right). 

407 vec_gen(select) : function 

408 A function that computes the eigenvectors chosen by the array select. 

409 ord_schur(select) : function 

410 A function that computes the unitary matrix (corresponding to the right 

411 eigenvector space) of the (general) Schur decomposition reordered such 

412 that the eigenvalues chosen by the array select are in the top left 

413 block. 

414 """ 

415 if b is None: 

416 eps = np.finfo(a.dtype).eps * tol 

417 t, z, ev = kla.schur(a) 

418 

419 # Right-decaying modes. 

420 select = abs(ev) > 1 + eps 

421 # Propagating modes. 

422 propselect = abs(abs(ev) - 1) < eps 

423 

424 vec_gen = lambda x: kla.evecs_from_schur(t, z, select=x) 

425 ord_schur = lambda x: kla.order_schur(x, t, z, calc_ev=False)[1] 

426 

427 else: 

428 eps = np.finfo(np.common_type(a, b)).eps * tol 

429 s, t, z, alpha, beta = kla.gen_schur(a, b, calc_q=False) 

430 

431 # Right-decaying modes. 

432 select = abs(alpha) > (1 + eps) * abs(beta) 

433 # Propagating modes. 

434 propselect = (abs(abs(alpha) - abs(beta)) < eps * abs(beta)) 

435 

436 with np.errstate(divide='ignore', invalid='ignore'): 

437 ev = alpha / beta 

438 # Note: the division is OK here, since we later only access 

439 # eigenvalues close to the unit circle 

440 

441 vec_gen = lambda x: kla.evecs_from_gen_schur(s, t, z=z, select=x) 

442 ord_schur = lambda x: kla.order_gen_schur(x, s, t, z=z, 

443 calc_ev=False)[2] 

444 

445 return ev, select, propselect, vec_gen, ord_schur 

446 

447 

448def phs_symmetrization(wfs, particle_hole): 

449 """Makes the wave functions that have the same velocity at a time-reversal 

450 invariant momentum (TRIM) particle-hole symmetric. 

451 

452 If P is the particle-hole operator and P^2 = 1, then a particle-hole 

453 symmetric wave function at a TRIM is an eigenstate of P with eigenvalue 1. 

454 If P^2 = -1, wave functions with the same velocity at a TRIM come in pairs. 

455 Such a pair is particle-hole symmetric if the wave functions are related by 

456 P, i. e. the pair can be expressed as [psi_n, P psi_n] where psi_n is a wave 

457 function. 

458 

459 To ensure proper ordering of modes, this function also returns an array 

460 of indices which ensures that particle-hole partners are properly ordered 

461 in this subspace of modes. These are later used with np.lexsort to ensure 

462 proper ordering. 

463 

464 Parameters 

465 ---------- 

466 wfs : numpy array 

467 A matrix of propagating wave functions at a TRIM that all have the same 

468 velocity. The orthonormal wave functions form the columns of this matrix. 

469 particle_hole : numpy array 

470 The matrix representation of the unitary part of the particle-hole 

471 operator, expressed in the tight binding basis. 

472 

473 Returns 

474 ------- 

475 new_wfs : numpy array 

476 The matrix of particle-hole symmetric wave functions. 

477 TRIM_sort: numpy integer array 

478 Index array that stores the proper sort order of particle-hole symmetric 

479 wave functions in this subspace. 

480 """ 

481 

482 def Pdot(mat): 

483 """Apply the particle-hole operator to an array. """ 

484 return particle_hole.dot(mat.conj()) 

485 

486 # Take P in the subspace of W = wfs: U = W^+ @ P @ W^*. 

487 U = wfs.T.conj().dot(Pdot(wfs)) 

488 # Check that wfs are orthonormal and the space spanned 

489 # by them is closed under ph, meaning U is unitary. 

490 if not np.allclose(U.dot(U.T.conj()), np.eye(U.shape[0])): 490 ↛ 491line 490 didn't jump to line 491, because the condition on line 490 was never true

491 raise ValueError('wfs are not orthonormal or not closed under particle_hole.') 

492 P_squared = U.dot(U.conj()) 

493 if np.allclose(P_squared, np.eye(U.shape[0])): 

494 P_squared = 1 

495 elif np.allclose(P_squared, -np.eye(U.shape[0])): 495 ↛ 498line 495 didn't jump to line 498, because the condition on line 495 was never false

496 P_squared = -1 

497 else: 

498 raise ValueError('particle_hole must square to +1 or -1. P_squared = {}'.format(P_squared)) 

499 

500 if P_squared == 1: 

501 # Use the matrix square root method from 

502 # Applied Mathematics and Computation 234 (2014) 380-384. 

503 assert np.allclose(U, U.T) 

504 # Schur decomposition guarantees that vecs are orthonormal. 

505 vals, vecs = la.schur(U) 

506 # U should be normal, so vals is diagonal. 

507 assert np.allclose(np.diag(np.diag(vals)), vals) 

508 vals = np.diag(vals) 

509 # Need to take safe square root of U, the branch cut should not go 

510 # through any eigenvalues. Otherwise the square root may not be symmetric. 

511 # Find largest gap between eigenvalues 

512 phases = np.sort(np.angle(vals)) 

513 dph = np.append(np.diff(phases), phases[0] + 2*np.pi - phases[-1]) 

514 i = np.argmax(dph) 

515 shift = -np.pi - (phases[i] + dph[i]/2) 

516 # Take matrix square root with branch cut in largest gap 

517 vals = np.sqrt(vals * np.exp(1j * shift)) * np.exp(-0.5j * shift) 

518 sqrtU = vecs.dot(np.diag(vals)).dot(vecs.T.conj()) 

519 # For symmetric U sqrt(U) is also symmetric. 

520 assert np.allclose(sqrtU, sqrtU.T) 

521 # We want a new basis W_new such that W_new^+ @ P @ W_new^* = 1. 

522 # This is achieved by W_new = W @ sqrt(U). 

523 new_wfs = wfs.dot(sqrtU) 

524 # If P^2 = 1, there is no need to sort the modes further. 

525 TRIM_sort = np.zeros((wfs.shape[1],), dtype=int) 

526 else: 

527 # P^2 = -1. 

528 # Iterate over wave functions to construct 

529 # particle-hole partners. 

530 new_wfs = [] 

531 # The number of modes. This is always an even number >=2. 

532 N_modes = wfs.shape[1] 

533 # If there are only two modes in this subspace, they are orthogonal 

534 # so we replace the second one with the P applied to the first one. 

535 if N_modes == 2: 

536 wf = wfs[:, 0] 

537 # Store psi_n and P psi_n. 

538 new_wfs.append(wf) 

539 new_wfs.append(Pdot(wf)) 

540 # If there are more than two modes, iterate over wave functions 

541 # and construct their particle-hole partners one by one. 

542 else: 

543 # We construct pairs of modes that are particle-hole partners. 

544 # Need to iterate over all pairs except the final one. 

545 iterations = range((N_modes-2)//2) 

546 for i in iterations: 

547 # Take a mode psi_n from the basis - the first column 

548 # of the matrix of remaining modes. 

549 wf = wfs[:, 0] 

550 # Store psi_n and P psi_n. 

551 new_wfs.append(wf) 

552 P_wf = Pdot(wf) 

553 new_wfs.append(P_wf) 

554 # Remove psi_n and P psi_n from the basis matrix of modes. 

555 # First remove psi_n. 

556 wfs = wfs[:, 1:] 

557 # Now we project the remaining modes onto the orthogonal 

558 # complement of P psi_n. projector: 

559 projector = wfs.dot(wfs.T.conj()) - \ 

560 np.outer(P_wf, P_wf.T.conj()) 

561 # After the projection, the mode matrix is rank deficient - 

562 # the span of the column space has dimension one less than 

563 # the number of columns. 

564 wfs = projector.dot(wfs) 

565 wfs = la.qr(wfs, mode='economic', pivoting=True)[0] 

566 # Remove the redundant column. 

567 wfs = wfs[:, :-1] 

568 # If this is the final iteration, we only have two modes 

569 # left and can construct particle-hole partners without 

570 # the projection. 

571 if i == iterations[-1]: 

572 assert wfs.shape[1] == 2 

573 wf = wfs[:, 0] 

574 # Store psi_n and P psi_n. 

575 new_wfs.append(wf) 

576 new_wfs.append(Pdot(wf)) 

577 assert np.allclose(wfs.T.conj().dot(wfs), 

578 np.eye(wfs.shape[1])) 

579 new_wfs = np.hstack([col.reshape(len(col), 1)/npl.norm(col) for 

580 col in new_wfs]) 

581 assert np.allclose(new_wfs[:, 1::2], Pdot(new_wfs[:, ::2])) 

582 # Store sort ordering in this subspace of modes 

583 TRIM_sort = np.arange(new_wfs.shape[1]) 

584 assert np.allclose(new_wfs.T.conj().dot(new_wfs), np.eye(new_wfs.shape[1])) 

585 return new_wfs, TRIM_sort 

586 

587 

588def make_proper_modes(lmbdainv, psi, extract, tol, particle_hole, 

589 time_reversal, chiral): 

590 """ 

591 Find, normalize and sort the propagating eigenmodes. 

592 

593 Special care is taken of the case of degenerate k-values, where the 

594 numerically computed modes are typically a superposition of the real 

595 modes. In this case, also the proper (orthogonal) modes are computed. 

596 """ 

597 vel_eps = np.finfo(psi.dtype).eps * tol 

598 

599 nmodes = psi.shape[1] 

600 n = len(psi) // 2 

601 

602 # Array for the velocities. 

603 velocities = np.empty(nmodes, dtype=float) 

604 

605 # Array of indices to sort modes at a TRIM by PHS. 

606 TRIM_PHS_sort = np.zeros(nmodes, dtype=int) 

607 

608 # Calculate the full wave function in real space. 

609 full_psi = extract(psi, lmbdainv) 

610 

611 # Cast the types if any of the symmetry operators is complex 

612 for symm in time_reversal, particle_hole, chiral: 

613 if symm is None: 

614 continue 

615 full_psi = full_psi.astype(np.common_type(symm, full_psi)) 

616 psi = psi.astype(np.common_type(symm, psi)) 

617 

618 # Find clusters of nearby eigenvalues. Since the eigenvalues occupy the 

619 # unit circle, special care has to be taken to not introduce a cut at 

620 # lambda = -1. 

621 eps = np.finfo(lmbdainv.dtype).eps * tol 

622 angles = np.angle(lmbdainv) 

623 sort_order = np.resize(np.argsort(angles), (2 * len(angles,))) 

624 boundaries = np.argwhere(np.abs(np.diff(lmbdainv[sort_order])) 

625 > eps).flatten() + 1 

626 

627 # Detect the singular case of all eigenvalues equal. 

628 if boundaries.shape == (0,) and len(angles): 

629 boundaries = np.array([0, len(angles)]) 

630 

631 for interval in zip(boundaries[:-1], boundaries[1:]): 

632 if interval[1] > boundaries[0] + len(angles): 

633 break 

634 

635 indx = sort_order[interval[0] : interval[1]] 

636 

637 # If there is a degenerate eigenvalue with several different 

638 # eigenvectors, the numerical routines return some arbitrary 

639 # overlap of the real, physical solutions. In order 

640 # to figure out the correct wave function, we need to 

641 # have the full, not the projected wave functions 

642 # (at least to our current knowledge). 

643 

644 # Finding the true modes is done in two steps: 

645 

646 # 1. The true transversal modes should be orthogonal to each other, as 

647 # they share the same Bloch momentum (note that transversal modes with 

648 # different Bloch momenta k1 and k2 need not be orthogonal, the full 

649 # modes are orthogonal because of the longitudinal dependence e^{i k1 

650 # x} and e^{i k2 x}). The modes with the same k are therefore 

651 # orthogonalized. Moreover for the velocity to have a proper value the 

652 # modes should also be normalized. 

653 

654 q, r = la.qr(full_psi[:, indx], mode='economic') 

655 

656 # If the eigenvectors were purely real up to this stage, 

657 # they will typically become complex after the rotation. 

658 if psi.dtype != np.common_type(psi, r): 658 ↛ 659line 658 didn't jump to line 659, because the condition on line 658 was never true

659 psi = psi.astype(np.common_type(psi, r)) 

660 if full_psi.dtype != np.common_type(full_psi, q): 660 ↛ 661line 660 didn't jump to line 661, because the condition on line 660 was never true

661 full_psi = full_psi.astype(np.common_type(psi, q)) 

662 

663 full_psi[:, indx] = q 

664 psi[:, indx] = la.solve(r.T, psi[:, indx].T).T 

665 

666 # 2. Moving infinitesimally away from the degeneracy 

667 # point, the modes should diagonalize the velocity 

668 # operator (i.e. when they are non-degenerate any more) 

669 # The modes are therefore rotated properly such that they 

670 # diagonalize the velocity operator. 

671 # Note that step 2. does not give a unique result if there are 

672 # two modes with the same velocity, or if the modes stay 

673 # degenerate even for a range of Bloch momenta (and hence 

674 # must have the same velocity). However, this does not matter, 

675 # since we are happy with any superposition in this case. 

676 

677 vel_op = -1j * dot(psi[n:, indx].T.conj(), psi[:n, indx]) 

678 vel_op = vel_op + vel_op.T.conj() 

679 vel_vals, rot = la.eigh(vel_op) 

680 

681 # If the eigenvectors were purely real up to this stage, 

682 # they will typically become complex after the rotation. 

683 

684 if psi.dtype != np.common_type(psi, rot): 

685 psi = psi.astype(np.common_type(psi, rot)) 

686 if full_psi.dtype != np.common_type(full_psi, rot): 

687 full_psi = full_psi.astype(np.common_type(psi, rot)) 

688 

689 psi[:, indx] = dot(psi[:, indx], rot) 

690 full_psi[:, indx] = dot(full_psi[:, indx], rot) 

691 velocities[indx] = vel_vals 

692 

693 # With particle-hole symmetry, treat TRIMs individually. 

694 # Particle-hole conserves velocity. 

695 # If P^2 = 1, we can pick modes at a TRIM as particle-hole eigenstates. 

696 # If P^2 = -1, a mode at a TRIM and its particle-hole partner are 

697 # orthogonal, and we pick modes such that they are related by 

698 # particle-hole symmetry. 

699 

700 # At a TRIM, propagating translation eigenvalues are +1 or -1. 

701 if (particle_hole is not None and 

702 (np.abs(np.abs(lmbdainv[indx].real) - 1) < eps).all()): 

703 assert not len(indx) % 2 

704 # Set the eigenvalues to the exact TRIM values. 

705 if (np.abs(lmbdainv[indx].real - 1) < eps).all(): 

706 lmbdainv[indx] = 1 

707 else: 

708 # Momenta are the negative arguments of the translation 

709 # eigenvalues, as computed below using np.angle. np.angle of -1 

710 # is pi, so this assigns k = -pi to modes with translation 

711 # eigenvalue -1. 

712 lmbdainv[indx] = -1 

713 

714 # Original wave functions 

715 orig_wf = full_psi[:, indx] 

716 

717 # Modes are already sorted by velocity in ascending order, as 

718 # returned by la.eigh above. The first half is thus incident, 

719 # and the second half outgoing. 

720 # Here we work within a subspace of modes with a fixed velocity. 

721 # Mostly, this is done to ensure that modes of different velocities 

722 # are not mixed when particle-hole partners are constructed for 

723 # P^2 = -1. First, we identify which modes have the same velocity. 

724 # In each such subspace of modes, we construct wave functions that 

725 # are particle-hole partners. 

726 vels = velocities[indx] 

727 # Velocities are sorted in ascending order. Find the indices of the 

728 # last instance of each unique velocity. 

729 inds = [ind+1 for ind, vel in enumerate(vels[:-1]) 

730 if np.abs(vel-vels[ind+1])>vel_eps] 

731 inds = [0] + inds + [len(vels)] 

732 inds = zip(inds[:-1], inds[1:]) 

733 # Now possess an iterator over tuples, where each tuple (i,j) 

734 # contains the starting and final indices i and j of a submatrix 

735 # of the modes matrix, such that all modes in the submatrix 

736 # have the same velocity. 

737 

738 # Iterate over all submatrices of modes with the same velocity. 

739 new_wf = [] 

740 TRIM_sorts = [] 

741 for ind_tuple in inds: 

742 # Pick out wave functions that have a given velocity 

743 wfs = orig_wf[:, slice(*ind_tuple)] 

744 # Make particle-hole symmetric modes 

745 new_modes, TRIM_sort = phs_symmetrization(wfs, particle_hole) 

746 new_wf.append(new_modes) 

747 # Store sorting indices of the TRIM modes with the given 

748 # velocity. 

749 TRIM_sorts.append(TRIM_sort) 

750 # Gather into a matrix of modes 

751 new_wf = np.hstack(new_wf) 

752 # Store the sort order of all modes at the TRIM. 

753 # Used later with np.lexsort when the ordering 

754 # of modes is done. 

755 TRIM_PHS_sort[indx] = np.hstack(TRIM_sorts) 

756 # Replace the old modes. 

757 full_psi[:, indx] = new_wf 

758 # For both cases P^2 = +-1, must rotate wave functions in the 

759 # singular value basis. Find the rotation from new basis to old. 

760 rot = new_wf.T.conj().dot(orig_wf) 

761 # Rotate the wave functions in the singular value basis 

762 psi[:, indx] = psi[:, indx].dot(rot.T.conj()) 

763 

764 # Ensure proper usage of chiral symmetry. 

765 if chiral is not None and time_reversal is None: 

766 out_orig = full_psi[:, indx[len(indx)//2:]] 

767 out = chiral.dot(full_psi[:, indx[:len(indx)//2]]) 

768 # No least squares below because the modes should be orthogonal. 

769 rot = out_orig.T.conj().dot(out) 

770 full_psi[:, indx[len(indx)//2:]] = out 

771 psi[:, indx[len(indx)//2:]] = psi[:, indx[len(indx)//2:]].dot(rot) 

772 

773 if np.any(abs(velocities) < vel_eps): 773 ↛ 774line 773 didn't jump to line 774, because the condition on line 773 was never true

774 raise RuntimeError("Found a mode with zero or close to zero velocity.") 

775 if 2 * np.sum(velocities < 0) != len(velocities): 775 ↛ 776line 775 didn't jump to line 776, because the condition on line 775 was never true

776 raise RuntimeError("Numbers of left- and right-propagating " 

777 "modes differ, possibly due to a numerical " 

778 "instability.") 

779 

780 momenta = -np.angle(lmbdainv) 

781 # Sort the modes. The modes are sorted first by velocity and momentum, 

782 # and finally TRIM modes are properly ordered. 

783 order = np.lexsort([TRIM_PHS_sort, velocities, 

784 -np.sign(velocities) * momenta, np.sign(velocities)]) 

785 

786 velocities = velocities[order] 

787 momenta = momenta[order] 

788 full_psi = full_psi[:, order] 

789 psi = psi[:, order] 

790 

791 # Use particle-hole symmetry to relate modes that propagate in the 

792 # same direction but at opposite momenta. 

793 # Modes are sorted by velocity (first incident, then outgoing). 

794 # Incident modes are then sorted by momentum in ascending order, 

795 # and outgoing modes in descending order. 

796 # Adopted convention is to get modes with negative k (both in and out) 

797 # by applying particle-hole operator to modes with positive k. 

798 if particle_hole is not None: 

799 N = nmodes//2 # Number of incident or outgoing modes. 

800 # With particle-hole symmetry, N must be an even number. 

801 # Incident modes 

802 positive_k = (np.pi - eps > momenta[:N]) * (momenta[:N] > eps) 

803 # Original wave functions with negative values of k 

804 orig_neg_k = full_psi[:, :N][:, positive_k[::-1]] 

805 # For incident modes, ordering of wfs by momentum as returned by kwant 

806 # is [-k2, -k1, k1, k2], if k2, k1 > 0 and k2 > k1. 

807 # To maintain this ordering with ki and -ki as particle-hole partners, 

808 # reverse the order of the product at the end. 

809 wf_neg_k = particle_hole.dot( 

810 (full_psi[:, :N][:, positive_k]).conj())[:, ::-1] 

811 rot = la.lstsq(orig_neg_k, wf_neg_k)[0] 

812 full_psi[:, :N][:, positive_k[::-1]] = wf_neg_k 

813 psi[:, :N][:, positive_k[::-1]] = \ 

814 psi[:, :N][:, positive_k[::-1]].dot(rot) 

815 

816 # Outgoing modes 

817 positive_k = (np.pi - eps > momenta[N:]) * (momenta[N:] > eps) 

818 # Original wave functions with negative values of k 

819 orig_neg_k = full_psi[:, N:][:, positive_k[::-1]] 

820 # For outgoing modes, ordering of wfs by momentum as returned by kwant 

821 # is like [k2, k1, -k1, -k2], if k2, k1 > 0 and k2 > k1. 

822 

823 # Reverse order at the end to match momenta of opposite sign. 

824 wf_neg_k = particle_hole.dot( 

825 full_psi[:, N:][:, positive_k].conj())[:, ::-1] 

826 rot = la.lstsq(orig_neg_k, wf_neg_k)[0] 

827 full_psi[:, N:][:, positive_k[::-1]] = wf_neg_k 

828 psi[:, N:][:, positive_k[::-1]] = \ 

829 psi[:, N:][:, positive_k[::-1]].dot(rot) 

830 

831 # Modes are ordered by velocity. 

832 # Use time-reversal symmetry to relate modes of opposite velocity. 

833 if time_reversal is not None: 

834 # Note: within this function, nmodes refers to the total number 

835 # of propagating modes, not either left or right movers. 

836 out_orig = full_psi[:, nmodes//2:] 

837 out = time_reversal.dot(full_psi[:, :nmodes//2].conj()) 

838 rot = la.lstsq(out_orig, out)[0] 

839 full_psi[:, nmodes//2:] = out 

840 psi[:, nmodes//2:] = psi[:, nmodes//2:].dot(rot) 

841 

842 norm = np.sqrt(abs(velocities)) 

843 full_psi = full_psi / norm 

844 psi = psi / norm 

845 

846 return psi, PropagatingModes(full_psi, velocities, momenta) 

847 

848 

849def compute_block_modes(h_cell, h_hop, tol, stabilization, 

850 time_reversal, particle_hole, chiral): 

851 """Calculate modes corresponding to a single projector. """ 

852 n, m = h_hop.shape 

853 

854 # Defer most of the calculation to helper routines. 

855 matrices, v, extract = setup_linsys(h_cell, h_hop, tol, stabilization) 

856 ev, evanselect, propselect, vec_gen, ord_schur = unified_eigenproblem( 

857 *(matrices + (tol,))) 

858 

859 # v is never None. 

860 # h_hop.shape[0] and v.shape[1] not always the same. 

861 # Adding this makes the difference for some tests 

862 n = v.shape[1] 

863 

864 nrightmovers = np.sum(propselect) // 2 

865 nevan = n - nrightmovers 

866 evan_vecs = ord_schur(evanselect)[:, :nevan] 

867 

868 # Compute the propagating eigenvectors. 

869 prop_vecs = vec_gen(propselect) 

870 # Compute their velocity, and, if necessary, rotate them. 

871 

872 # prop_vecs here is 'psi' in make_proper_modes, i.e. the wf in the SVD 

873 # basis. It is in turn used to construct vecs and vecslmbdainv (the 

874 # propagating parts). The evanescent parts of vecs and vecslmbdainv come 

875 # from evan_vecs above. 

876 prop_vecs, real_space_data = make_proper_modes(ev[propselect], prop_vecs, 

877 extract, tol, particle_hole, 

878 time_reversal, chiral) 

879 

880 vecs = np.c_[prop_vecs[n:], evan_vecs[n:]] 

881 vecslmbdainv = np.c_[prop_vecs[:n], evan_vecs[:n]] 

882 

883 # Prepare output for a single block 

884 wave_functions = real_space_data.wave_functions 

885 momenta = real_space_data.momenta 

886 velocities = real_space_data.velocities 

887 

888 return (wave_functions, momenta, velocities, vecs, vecslmbdainv, v) 

889 

890 

891def transform_modes(modes_data, unitary=None, time_reversal=None, 

892 particle_hole=None, chiral=None): 

893 """Transform the modes data for a given block of the Hamiltonian using a 

894 discrete symmetry (see arguments). The symmetry operator can also be 

895 specified as can also be identity, for the case when blocks are identical. 

896 

897 Assume that modes_data has the form returned by compute_block_modes, i.e. a 

898 tuple (wave_functions, momenta, velocities, nmodes, vecs, vecslmbdainv, v) 

899 containing the block modes data. 

900 

901 Assume that the symmetry operator is written in the proper basis (block 

902 basis, not full TB). 

903 

904 """ 

905 wave_functions, momenta, velocities, vecs, vecslmbdainv, v = modes_data 

906 

907 # Copy to not overwrite modes from previous blocks 

908 wave_functions = wave_functions.copy() 

909 momenta = momenta.copy() 

910 velocities = velocities.copy() 

911 vecs = vecs.copy() 

912 vecslmbdainv = vecslmbdainv.copy() 

913 v = v.copy() 

914 

915 nmodes = wave_functions.shape[1] // 2 

916 

917 if unitary is not None: 

918 perm = np.arange(2*nmodes) 

919 conj = False 

920 flip_energy = False 

921 elif time_reversal is not None: 

922 unitary = time_reversal 

923 perm = np.arange(2*nmodes)[::-1] 

924 conj = True 

925 flip_energy = False 

926 elif particle_hole is not None: 

927 unitary = particle_hole 

928 perm = ((-1-np.arange(2*nmodes)) % nmodes + 

929 nmodes * (np.arange(2*nmodes) // nmodes)) 

930 conj = True 

931 flip_energy = True 

932 elif chiral is not None: 

933 unitary = chiral 

934 perm = (np.arange(2*nmodes) % nmodes + 

935 nmodes * (np.arange(2*nmodes) < nmodes)) 

936 conj = False 

937 flip_energy = True 

938 else: # skip coverage 

939 raise ValueError("No relation between blocks was provided.") 

940 

941 if conj: 

942 momenta *= -1 

943 vecs = vecs.conj() 

944 vecslmbdainv = vecslmbdainv.conj() 

945 wave_functions = wave_functions.conj() 

946 v = v.conj() 

947 

948 if flip_energy: 

949 vecslmbdainv *= -1 

950 

951 if flip_energy != conj: 

952 velocities *= -1 

953 

954 wave_functions = unitary.dot(wave_functions)[:, perm] 

955 v = unitary.dot(v) 

956 vecs[:, :2*nmodes] = vecs[:, perm] 

957 vecslmbdainv[:, :2*nmodes] = vecslmbdainv[:, perm] 

958 velocities = velocities[perm] 

959 momenta = momenta[perm] 

960 return (wave_functions, momenta, velocities, vecs, vecslmbdainv, v) 

961 

962 

963def modes(h_cell, h_hop, tol=1e6, stabilization=None, *, 

964 discrete_symmetry=None, projectors=None, time_reversal=None, 

965 particle_hole=None, chiral=None): 

966 """Compute the eigendecomposition of a translation operator of a lead. 

967 

968 Parameters 

969 ---------- 

970 h_cell : numpy array, real or complex, shape (N,N) The unit cell 

971 Hamiltonian of the lead unit cell. 

972 h_hop : numpy array, real or complex, shape (N,M) 

973 The hopping matrix from a lead cell to the one on which self-energy 

974 has to be calculated (and any other hopping in the same direction). 

975 tol : float 

976 Numbers and differences are considered zero when they are smaller 

977 than `tol` times the machine precision. 

978 stabilization : sequence of 2 booleans or None 

979 Which steps of the eigenvalue prolem stabilization to perform. If the 

980 value is `None`, then Kwant chooses the fastest (and least stable) 

981 algorithm that is expected to be sufficient. For any other value, 

982 Kwant forms the eigenvalue problem in the basis of the hopping singular 

983 values. The first element set to `True` forces Kwant to add an 

984 anti-Hermitian term to the cell Hamiltonian before inverting. If it is 

985 set to `False`, the extra term will only be added if the cell 

986 Hamiltonian isn't invertible. The second element set to `True` forces 

987 Kwant to solve a generalized eigenvalue problem, and not to reduce it 

988 to the regular one. If it is `False`, reduction to a regular problem 

989 is performed if possible. Selecting the stabilization manually is 

990 mostly necessary for testing purposes. 

991 particle_hole : sparse or dense square matrix 

992 The unitary part of the particle-hole symmetry operator. 

993 time_reversal : sparse or dense square matrix 

994 The unitary part of the time-reversal symmetry operator. 

995 chiral : sparse or dense square matrix 

996 The chiral symmetry operator. 

997 projectors : an iterable of sparse or dense matrices 

998 Projectors that block diagonalize the Hamiltonian in accordance 

999 with a conservation law. 

1000 

1001 Returns 

1002 ------- 

1003 propagating : `~kwant.physics.PropagatingModes` 

1004 Contains the array of the wave functions of propagating modes, their 

1005 momenta, and their velocities. It can be used to identify the gauge in 

1006 which the scattering problem is solved. 

1007 stabilized : `~kwant.physics.StabilizedModes` 

1008 A basis of propagating and evanescent modes used by the solvers. 

1009 

1010 Notes 

1011 ----- 

1012 The sorting of the propagating modes is fully described in the 

1013 documentation for `~kwant.physics.PropagatingModes`. In simple cases where 

1014 bands do not cross, this ordering corresponds to "lowest modes first". In 

1015 general, however, it is necessary to examine the band structure -- 

1016 something this function is not doing by design. 

1017 

1018 Propagating modes with the same momentum are orthogonalized. All the 

1019 propagating modes are normalized by current. 

1020 

1021 `projectors`, `time_reversal`, `particle_hole`, and `chiral` affect the 

1022 basis in which the scattering modes are expressed - see 

1023 `~kwant.physics.DiscreteSymmetry` for details. 

1024 

1025 This function uses the most stable and efficient algorithm for calculating 

1026 the mode decomposition that the Kwant authors are aware about. Its details 

1027 are to be published. 

1028 """ 

1029 if discrete_symmetry is not None: 

1030 projectors, time_reversal, particle_hole, chiral = discrete_symmetry 

1031 n, m = h_hop.shape 

1032 

1033 if h_cell.shape != (n, n): 1033 ↛ 1034line 1033 didn't jump to line 1034, because the condition on line 1033 was never true

1034 raise ValueError("Incompatible matrix sizes for h_cell and h_hop.") 

1035 

1036 if not np.any(h_hop): 

1037 wf = np.zeros((n, 0)) 

1038 v = np.zeros((m, 0)) 

1039 m = np.zeros((0, 0)) 

1040 vec = np.zeros((0,)) 

1041 return (PropagatingModes(wf, vec, vec), StabilizedModes(m, m, 0, v)) 

1042 

1043 ham = h_cell 

1044 # Avoid the trouble of dealing with non-square hopping matrices. 

1045 # TODO: How to avoid this while not doing a lot of book-keeping? 

1046 hop = np.empty_like(ham, dtype=h_hop.dtype) 

1047 hop[:, :m] = h_hop 

1048 hop[:, m:] = 0 

1049 

1050 # Provide default values to not deal with special cases. 

1051 if projectors is None: 

1052 projectors = [sp_identity(ham.shape[0], format='csr')] 

1053 

1054 def to_dense_or_csr(matrix): 

1055 if matrix is None: 

1056 return csr_matrix(ham.shape) 

1057 try: 

1058 return matrix.tocsr() 

1059 except AttributeError: 

1060 return matrix 

1061 

1062 symmetries = map(to_dense_or_csr, 

1063 (time_reversal, particle_hole, chiral)) 

1064 time_reversal, particle_hole, chiral = symmetries 

1065 

1066 offsets = np.cumsum([0] + [projector.shape[1] for projector in projectors]) 

1067 indices = [slice(*i) for i in np.vstack([offsets[:-1], offsets[1:]]).T] 

1068 projection_op = sp_hstack(projectors) 

1069 

1070 def basis_change(a, antiunitary=False): 

1071 b = projection_op 

1072 # We need the extra transposes to ensure that sparse dot is used. 

1073 if antiunitary: 

1074 # b.T.conj() @ a @ b.conj() 

1075 return (b.T.conj().dot((b.T.conj().dot(a)).T)).T 

1076 else: 

1077 # b.T.conj() @ a @ b 

1078 return (b.T.dot((b.T.conj().dot(a)).T)).T 

1079 

1080 # Conservation law basis 

1081 ham_cons = basis_change(ham) 

1082 hop_cons = basis_change(hop) 

1083 trs = basis_change(time_reversal, True) 

1084 phs = basis_change(particle_hole, True) 

1085 sls = basis_change(chiral) 

1086 

1087 # Check that the Hamiltonian has the conservation law 

1088 block_modes = len(projectors) * [None] 

1089 numbers_coords = combinations_with_replacement(enumerate(indices), 2) 

1090 for (i, x), (j, y) in numbers_coords: 

1091 h = ham_cons[x, y] 

1092 t = hop_cons[x, y] 

1093 # Symmetries that project from block x to block y 

1094 symmetries = [symm[y, x] for symm in (trs, phs, sls)] 

1095 symmetries = [(symm if nonzero_symm_projection(symm) else None) for 

1096 symm in symmetries] 

1097 if i == j: 

1098 if block_modes[i] is not None: 

1099 continue 

1100 # We did not compute this block yet. 

1101 block_modes[i] = compute_block_modes(h, t, tol, stabilization, 

1102 *symmetries) 

1103 else: 

1104 if block_modes[j] is not None: 1104 ↛ 1106line 1104 didn't jump to line 1106, because the condition on line 1104 was never true

1105 # Modes in the block already computed. 

1106 continue 

1107 x, y = tuple(np.mgrid[x, x]), tuple(np.mgrid[y, y]) 

1108 

1109 if ham_cons[x].shape != ham_cons[y].shape: 

1110 continue 

1111 if (np.allclose(ham_cons[x], ham_cons[y]) and 

1112 np.allclose(hop_cons[x], hop_cons[y])): 

1113 unitary = sp_identity(h.shape[0]) 

1114 else: 

1115 unitary = None 

1116 if any(op is not None for op in symmetries + [unitary]): 

1117 block_modes[j] = transform_modes(block_modes[i], unitary, 

1118 *symmetries) 

1119 (wave_functions, momenta, velocities, 

1120 vecs, vecslmbdainv, sqrt_hops) = zip(*block_modes) 

1121 

1122 # Reorder by direction of propagation 

1123 wave_functions = group_halves([(projector.dot(wf)).T for wf, projector in 

1124 zip(wave_functions, projectors)]).T 

1125 # Propagating modes object to return 

1126 prop_modes = PropagatingModes(wave_functions, group_halves(velocities), 

1127 group_halves(momenta)) 

1128 nmodes = [len(v) // 2 for v in velocities] 

1129 # Store the number of modes per block as an attribute. 

1130 # nmodes[i] is the number of left or right moving modes in block i. 

1131 # In the module that makes leads with conservation laws, this is necessary 

1132 # to keep track of the block structure of the scattering matrix. 

1133 prop_modes.block_nmodes = nmodes 

1134 

1135 parts = zip(*([v[:, :n], v[:, n:2*n], v[:, 2*n:]] 

1136 for n, v in zip(nmodes, vecs))) 

1137 vecs = np.hstack([block_diag(*part) for part in parts]) 

1138 

1139 parts = zip(*([v[:, :n], v[:, n:2*n], v[:, 2*n:]] 

1140 for n, v in zip(nmodes, vecslmbdainv))) 

1141 vecslmbdainv = np.hstack([block_diag(*part) for part in parts]) 

1142 

1143 sqrt_hops = np.hstack([projector.dot(hop) for projector, hop in 

1144 zip(projectors, sqrt_hops)])[:h_hop.shape[1]] 

1145 

1146 stab_modes = StabilizedModes(vecs, vecslmbdainv, sum(nmodes), sqrt_hops) 

1147 

1148 return prop_modes, stab_modes 

1149 

1150 

1151def selfenergy(h_cell, h_hop, tol=1e6): 

1152 """ 

1153 Compute the self-energy generated by the lead. 

1154 

1155 Parameters 

1156 ---------- 

1157 h_cell : numpy array, real or complex, shape (N,N) The unit cell Hamiltonian 

1158 of the lead unit cell. 

1159 h_hop : numpy array, real or complex, shape (N,M) 

1160 The hopping matrix from a lead cell to the one on which self-energy 

1161 has to be calculated (and any other hopping in the same direction). 

1162 tol : float 

1163 Numbers are considered zero when they are smaller than `tol` times 

1164 the machine precision. 

1165 

1166 Returns 

1167 ------- 

1168 Sigma : numpy array, real or complex, shape (M,M) 

1169 The computed self-energy. Note that even if `h_cell` and `h_hop` are 

1170 both real, `Sigma` will typically be complex. (More precisely, if there 

1171 is a propagating mode, `Sigma` will definitely be complex.) 

1172 

1173 Notes 

1174 ----- 

1175 For simplicity this function internally calculates the modes first. 

1176 This may cause a small slowdown, and can be improved if necessary. 

1177 """ 

1178 stabilized = modes(h_cell, h_hop, tol)[1] 

1179 return stabilized.selfenergy() 

1180 

1181 

1182def square_selfenergy(width, hopping, fermi_energy): 

1183 """ 

1184 Calculate analytically the self energy for a square lattice. 

1185 

1186 The lattice is assumed to have a single orbital per site and 

1187 nearest-neighbor hopping. 

1188 

1189 Parameters 

1190 ---------- 

1191 width : integer 

1192 width of the lattice 

1193 """ 

1194 

1195 # Following appendix C of M. Wimmer's diploma thesis: 

1196 # http://www.physik.uni-regensburg.de/forschung/\ 

1197 # richter/richter/media/research/publications2004/wimmer-Diplomarbeit.pdf 

1198 

1199 # p labels transversal modes. i and j label the sites of a cell. 

1200 

1201 # Precalculate the transverse wave function. 

1202 psi_p_i = np.empty((width, width)) 

1203 factor = pi / (width + 1) 

1204 prefactor = sqrt(2 / (width + 1)) 

1205 for p in range(width): 

1206 for i in range(width): 

1207 psi_p_i[p, i] = prefactor * sin(factor * (p + 1) * (i + 1)) 

1208 

1209 # Precalculate the integrals of the longitudinal wave functions. 

1210 def f(q): 

1211 if abs(q) <= 2: 

1212 return q/2 - 1j * sqrt(1 - (q / 2) ** 2) 

1213 else: 

1214 return q/2 - copysign(sqrt((q / 2) ** 2 - 1), q) 

1215 f_p = np.empty((width,), dtype=complex) 

1216 for p in range(width): 

1217 e = 2 * hopping * (1 - cos(factor * (p + 1))) 

1218 q = (fermi_energy - e) / hopping - 2 

1219 f_p[p] = f(q) 

1220 

1221 # Put everything together into the self energy and return it. 

1222 result = np.empty((width, width), dtype=complex) 

1223 for i in range(width): 

1224 for j in range(width): 

1225 result[i, j] = hopping * ( 

1226 psi_p_i[:, i] * psi_p_i[:, j].conj() * f_p).sum() 

1227 return result