Coverage for tbkit/bdg.py: 100%

130 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 06:13 +0000

1r""" 

2Superconductivity in the Bogoliubov-de Gennes (BdG) formalism. With a 

3pairing term :math:`\sum_{ij}\Delta_{ij}c^\dagger_ic^\dagger_j + h.c.`, 

4the Hamiltonian is :math:`\frac12\Psi^\dagger H_{BdG}\Psi` in the Nambu 

5basis :math:`\Psi = (c_1, \dots, c_N, c_1^\dagger, \dots, c_N^\dagger)^T`, 

6 

7.. math:: 

8 

9 H_{BdG} = \begin{pmatrix} h - \mu & \Delta \\ \Delta^\dagger & -(h-\mu)^T \end{pmatrix}\, , 

10 

11with :math:`\Delta^T = -\Delta` (Fermi statistics). Its spectrum is 

12symmetric, :math:`\pm E_n`, by the particle-hole symmetry 

13:math:`\tau_x H_{BdG}^*\tau_x = -H_{BdG}`; the quasiparticle energies are 

14the :math:`E_n \ge 0`, and a zero mode is a Majorana mode. Real-space 

15models come from *bdg_ham*, periodic ones from *bdg_kspace* (a 

16:class:`tbkit.kspace.KSpace` with the holes as extra orbitals, so its bands, 

17Berry phases, *finite_ham*, ... all apply). 

18""" 

19from __future__ import annotations 

20 

21import numpy as np 

22from numpy.typing import NDArray 

23import scipy.sparse as sparse 

24 

25import tbkit.error_handling as error_handling 

26from tbkit.lattice import Lattice 

27 

28 

29def bdg_ham(ham, pairing, mu: float = 0.) -> sparse.csr_matrix: 

30 r''' 

31 Get the real-space BdG Hamiltonian (see the module docstring). 

32 

33 :param ham: Square Hermitian matrix, shape (N, N) (e.g. *System.ham*). 

34 :param pairing: Square antisymmetric matrix, shape (N, N): :math:`\Delta` 

35 (see *pairing_bonds*, *pairing_s_wave*). 

36 :param mu: Real number. Default value 0. Chemical potential. 

37 

38 :returns: 

39 * **ham_bdg** -- Sparse CSR matrix, shape (2N, 2N). 

40 ''' 

41 h = sparse.csr_matrix(ham, dtype='c16') 

42 delta = sparse.csr_matrix(pairing, dtype='c16') 

43 error_handling.square_matrix(h, 'ham') 

44 error_handling.hermitian(h) 

45 error_handling.pairing(delta, h.shape[0]) 

46 error_handling.real_number(mu, 'mu') 

47 h = h - mu * sparse.identity(h.shape[0], dtype='c16', format='csr') 

48 return sparse.csr_matrix(sparse.bmat([[h, delta], [delta.conj().T, -h.T]])) 

49 

50 

51def pairing_bonds(sys, delta: complex) -> sparse.csr_matrix: 

52 r''' 

53 Get a p-wave (odd) pairing on the bonds of *sys.hop*: 

54 :math:`\Delta_{ij} = \Delta`, :math:`\Delta_{ji} = -\Delta` for every 

55 stored hopping :math:`i\to j` (oriented as *System.set_hopping* stores 

56 them: angle in :math:`[0, 180)`). On a chain it is Kitaev's 

57 :math:`\Delta\,c^\dagger_jc^\dagger_{j+1}`. 

58 

59 :param sys: **System** instance, with hoppings. 

60 :param delta: Complex number. :math:`\Delta`. 

61 

62 :returns: 

63 * **pairing** -- Sparse CSR matrix, shape (sites, sites). 

64 ''' 

65 error_handling.empty_hop(sys.hop) 

66 error_handling.number(delta, 'delta') 

67 i, j = sys.hop['i'].astype(int), sys.hop['j'].astype(int) 

68 n = sys.lat.sites 

69 vals = np.full(len(i), delta, dtype='c16') 

70 return sparse.csr_matrix((np.concatenate([vals, -vals]), (np.concatenate([i, j]), np.concatenate([j, i]))), 

71 shape=(n, n)) 

72 

73 

74def pairing_s_wave(n_sites: int, delta: complex) -> sparse.csr_matrix: 

75 r''' 

76 Get an onsite singlet (s-wave) pairing 

77 :math:`\Delta\sum_i c^\dagger_{i\uparrow}c^\dagger_{i\downarrow} + h.c.` 

78 for a spinful model with rows ordered site by site, spin up then down 

79 (as *KSpace* with ``spin=True`` and *OrbitalSystem* with one orbital). 

80 

81 :param n_sites: Positive integer. Number of sites. 

82 :param delta: Complex number. :math:`\Delta`. 

83 

84 :returns: 

85 * **pairing** -- Sparse CSR matrix, shape (2 n_sites, 2 n_sites). 

86 ''' 

87 error_handling.positive_int(n_sites, 'n_sites') 

88 error_handling.number(delta, 'delta') 

89 block = np.array([[0., delta], [-delta, 0.]], dtype='c16') 

90 return sparse.csr_matrix(sparse.kron(sparse.identity(n_sites), block)) 

91 

92 

93def bdg_kspace(ks, pairing: list[dict], mu: float = 0.): 

94 r''' 

95 Get the BdG Bloch Hamiltonian of a periodic model, 

96 

97 .. math:: 

98 

99 H_{BdG}(\mathbf{k}) = \begin{pmatrix} h(\mathbf{k})-\mu & \Delta(\mathbf{k}) \\ 

100 \Delta(\mathbf{k})^\dagger & -[h(-\mathbf{k})-\mu]^T\end{pmatrix}\, ,\qquad 

101 \Delta(\mathbf{k}) = \sum_{\mathbf{R}}\Delta_{ij}(\mathbf{R})\,e^{i\mathbf{k}\cdot\mathbf{R}}\, , 

102 

103 as a new **KSpace** whose orbitals are those of *ks* (particles, 

104 ``0 ... norb-1``) followed by their holes (``norb ... 2 norb-1``, at the 

105 same positions). Each pairing :math:`\Delta\,c^\dagger_{i,\mathbf{0}}c^\dagger_{j,\mathbf{R}} + h.c.` 

106 enters antisymmetrized, :math:`\Delta_{ij}(\mathbf{R}) = -\Delta_{ji}(-\mathbf{R})`. 

107 

108 :param ks: **KSpace** instance (Hermitian, orthogonal basis). 

109 :param pairing: List of dictionaries with keys ('i', 'j', 'R', 'delta'): 

110 orbital indices of *ks* (``0 ... norb-1``; with ``spin=True``, 

111 ``2*site + spin``), lattice vector, amplitude. 

112 :param mu: Real number. Default value 0. Chemical potential. 

113 

114 :returns: 

115 * **ks_bdg** -- **KSpace** instance, 2 norb orbitals. 

116 

117 Example usage:: 

118 

119 # Kitaev chain: E(k) = +-sqrt((2t cos k - mu)^2 + 4 Delta^2 sin^2 k) 

120 chain = KSpace(lattices.chain()) 

121 chain.set_hopping([{'i': 0, 'j': 0, 'R': (1,), 't': t}]) 

122 kit = bdg_kspace(chain, [{'i': 0, 'j': 0, 'R': (1,), 'delta': delta}], mu=mu) 

123 ''' 

124 from tbkit.kspace import KSpace 

125 error_handling.hermitian_kspace(ks.is_hermitian(), ks._overlap_hop) 

126 error_handling.pairing_kspace(pairing, ks.norb, ks.dim) 

127 error_handling.real_number(mu, 'mu') 

128 norb = ks.norb 

129 tau = ks.orbital_positions() 

130 tags = np.repeat(ks.tags, 2) if ks.spin else ks.tags 

131 cell = [{'tag': str(t), 'r0': tuple(float(c) for c in r)} for t, r in zip(tags, tau)] 

132 new = KSpace(Lattice(unit_cell=cell + [dict(c) for c in cell], prim_vec=ks.lat.prim_vec)) 

133 h0 = np.diag(ks.onsite) + ks._onsite_offdiag - mu * np.eye(norb) 

134 new.onsite = np.concatenate([np.diag(h0), -np.diag(h0)]).astype('c16') 

135 off = h0 - np.diag(np.diag(h0)) 

136 new._onsite_offdiag = np.block([[off, np.zeros((norb, norb))], [np.zeros((norb, norb)), -off.T]]).astype('c16') 

137 hops = [(i, j, R, t) for i, j, R, t in ks._hop] 

138 hops += [(j + norb, i + norb, -R, -t) for i, j, R, t in ks._hop] 

139 for dic in pairing: 

140 R = np.zeros(ks.space_dim) 

141 for n, a in zip(dic['R'], ks.lat.prim_vec): 

142 R += n * np.array(a) 

143 i, j, d = dic['i'], dic['j'], complex(dic['delta']) 

144 hops += [(i, j + norb, R, d), (j, i + norb, -R, -d), 

145 (j + norb, i, -R, np.conj(d)), (i + norb, j, R, -np.conj(d))] 

146 new._hop = hops 

147 return new 

148 

149 

150def particle_hole(n: int) -> NDArray[np.float64]: 

151 r''' 

152 Get the unitary part :math:`\tau_x` of the BdG particle-hole symmetry 

153 :math:`C = \tau_x\mathcal{K}` (:math:`C^2 = +1`), for *n* particle 

154 orbitals: pass it to *KSpace.symmetry_error* (``antiunitary=True``, 

155 ``anti=True``) or *KSpace.tenfold_class* (``particle_hole=``). 

156 

157 :param n: Positive integer. Number of particle orbitals. 

158 

159 :returns: 

160 * **tau_x** -- Real ndarray, shape (2n, 2n). 

161 ''' 

162 error_handling.positive_int(n, 'n') 

163 return np.kron(np.array([[0., 1.], [1., 0.]]), np.eye(n)) 

164 

165 

166def pfaffian(a) -> complex: 

167 r''' 

168 Get the Pfaffian of an antisymmetric matrix, 

169 :math:`\mathrm{Pf}(A)^2 = \det A`, by the Parlett-Reid reduction to 

170 tridiagonal form with pivoting (Wimmer, ACM Trans. Math. Softw. 38, 30 

171 (2012)). For a 2x2 block :math:`\begin{pmatrix}0 & a\\ -a & 0\end{pmatrix}` 

172 it is :math:`a`; it is 0 for an odd dimension. 

173 

174 :param a: Square antisymmetric matrix, shape (n, n). 

175 

176 :returns: 

177 * **pf** -- Complex number (a float for a real matrix). 

178 ''' 

179 real = not np.iscomplexobj(a) 

180 a = np.array(a, dtype='c16') 

181 error_handling.square_matrix(a, 'a') 

182 error_handling.antisymmetric(a) 

183 n = len(a) 

184 if n % 2: 

185 return 0. 

186 pf = 1. + 0j 

187 for k in range(0, n - 1, 2): 

188 # bring the largest entry of column k below the diagonal to row k + 1 

189 p = k + 1 + int(np.argmax(np.abs(a[k + 1:, k]))) 

190 if p != k + 1: 

191 a[[k + 1, p], :] = a[[p, k + 1], :] 

192 a[:, [k + 1, p]] = a[:, [p, k + 1]] 

193 pf = -pf 

194 if a[k + 1, k] == 0.: 

195 return 0. 

196 pf *= a[k, k + 1] 

197 if k + 2 < n: 

198 tau = a[k, k + 2:] / a[k, k + 1] 

199 a[k + 2:, k + 2:] += np.outer(tau, a[k + 2:, k + 1]) - np.outer(a[k + 2:, k + 1], tau) 

200 return float(pf.real) if real else complex(pf) 

201 

202 

203def majorana_number(ks) -> int: 

204 r''' 

205 Get Kitaev's Majorana number of a BdG model (Kitaev, Physics-Uspekhi 

206 44, 131 (2001)): the product, over the time-reversal-invariant momenta 

207 :math:`\Gamma_i`, of the signs of the Pfaffians of the Hamiltonian in 

208 the Majorana basis, 

209 

210 .. math:: 

211 

212 \mathcal{M} = \prod_i \mathrm{sign}\,\mathrm{Pf}\,A(\Gamma_i)\, ,\qquad 

213 A = -i\,U H_{BdG} U^\dagger\, ,\qquad 

214 U = \frac{1}{\sqrt2}\begin{pmatrix}1 & 1\\ -i & i\end{pmatrix}\, , 

215 

216 :math:`U` taking the Nambu spinor :math:`(c, c^\dagger)` to the Majorana 

217 operators :math:`(c + c^\dagger, -i(c - c^\dagger))`. Particle-hole 

218 symmetry makes :math:`A(\Gamma_i)` real and antisymmetric. In 1D, 

219 :math:`\mathcal{M} = \mathrm{sign}\,\mathrm{Pf}A(0)\,\mathrm{Pf}A(\pi) = -1` 

220 in the topological phase (a Majorana zero mode at each end of an open 

221 chain), +1 in the trivial one: the :math:`\mathbb{Z}_2` invariant of 

222 class D. In 2D the product over the four momenta is 

223 :math:`(-1)^C`, the parity of the Chern number (Ghosh et al., Phys. Rev. 

224 B 82, 184525 (2010)). The invariant changes only where the gap closes 

225 at one of the :math:`\Gamma_i`. 

226 

227 :param ks: **KSpace** instance of a BdG model, 2n orbitals: n particles 

228 then their n holes (see *bdg_kspace*). 

229 

230 :returns: 

231 * **m** -- Integer, -1 (topological) or +1 (trivial). 

232 ''' 

233 from tbkit.kspace import KSpace 

234 error_handling.kspace(ks, KSpace) 

235 error_handling.bdg_orbitals(ks.norb) 

236 n = ks.norb // 2 

237 u = np.kron(np.array([[1., 1.], [-1j, 1j]]) / np.sqrt(2), np.eye(n)) 

238 sign = 1 

239 for k in ks._trims(): 

240 a = -1j * u @ ks.get_ham(k) @ u.conj().T 

241 error_handling.majorana_form(a) 

242 pf = pfaffian(a.real) 

243 error_handling.gapped_trim(pf) 

244 sign *= int(np.sign(pf)) 

245 return sign 

246 

247 

248class GapResult(): 

249 r''' 

250 Self-consistent s-wave pairing of *s_wave_gap*. 

251 

252 :ivar delta: Complex ndarray. Gap :math:`\Delta_i` on every site. 

253 :ivar energies: Real ndarray, shape (2N,). Spectrum of the reduced BdG 

254 Hamiltonian (symmetric, :math:`\pm E_n`). 

255 :ivar states: Complex ndarray, shape (2N, 2N). Its eigenvectors 

256 :math:`(u_n, v_n)` (columns). 

257 :ivar iterations: Integer. Number of iterations. 

258 ''' 

259 

260 def __init__(self, delta, energies, states, iterations) -> None: 

261 self.delta = delta 

262 self.energies = energies 

263 self.states = states 

264 self.iterations = iterations 

265 

266 @property 

267 def mean_gap(self) -> float: 

268 r''' 

269 Average :math:`|\Delta_i|` over the sites. 

270 ''' 

271 return float(np.mean(np.abs(self.delta))) 

272 

273 

274def s_wave_gap( 

275 ham, V: float | NDArray, mu: float = 0., temperature: float = 0., delta0: complex | NDArray = 0.1, 

276 mixing: float = 1., tol: float = 1e-8, max_iter: int = 10000, 

277) -> GapResult: 

278 r''' 

279 Solve the real-space gap equation of an s-wave superconductor 

280 self-consistently (the attractive Hubbard model 

281 :math:`-\sum_iV_in_{i\uparrow}n_{i\downarrow}` in the BCS mean-field 

282 approximation, de Gennes 1966). For a spin-independent *ham* the singlet 

283 BdG Hamiltonian reduces to the Nambu basis 

284 :math:`(c_{i\uparrow}, c^\dagger_{i\downarrow})`, 

285 

286 .. math:: 

287 

288 H = \begin{pmatrix} h-\mu & \Delta\\ \Delta^* & -(h-\mu)^*\end{pmatrix}\, , 

289 \qquad \Delta = \mathrm{diag}(\Delta_i)\, , 

290 

291 whose eigenvectors :math:`(u_n, v_n)`, :math:`E_n > 0`, give the gap 

292 equation 

293 

294 .. math:: 

295 

296 \Delta_i = V_i\,\langle c_{i\downarrow}c_{i\uparrow}\rangle 

297 = V_i\sum_{E_n>0}u_n(i)\,v_n^*(i)\tanh\frac{E_n}{2T}\, , 

298 

299 iterated (with linear mixing) until self-consistent. On a clean lattice 

300 it is the BCS gap equation :math:`1 = \frac{V}{N}\sum_{\mathbf{k}} 

301 \tanh(E_{\mathbf{k}}/2T)/2E_{\mathbf{k}}`, with 

302 :math:`\Delta(T_c) = 0` at :math:`T_c \approx \Delta(0)/1.764` at weak 

303 coupling. The chemical potential is fixed (the Hartree shift 

304 :math:`-V_in_i/2` is left out, i.e. absorbed in :math:`\mu`). Near 

305 :math:`T_c` the iteration slows down (the linearized map has an 

306 eigenvalue close to 1): raise *max_iter* there. 

307 

308 :param ham: Square Hermitian matrix, (N, N), spin independent (e.g. *System.ham*). 

309 :param V: Positive real, or array of N non-negative reals. Attraction. 

310 :param mu: Real number. Default value 0. Chemical potential. 

311 :param temperature: Positive real or zero. Default value 0. 

312 :param delta0: Number, or array of N numbers. Default value 0.1. 

313 Initial gap. 

314 :param mixing: Real in (0, 1]. Default value 1 (plain iteration). 

315 :param tol: Positive real. Default value 1e-8. Convergence threshold 

316 on the largest change of :math:`\Delta_i`. 

317 :param max_iter: Positive integer. Default value 10000. 

318 

319 :returns: 

320 * **result** -- :class:`GapResult`. 

321 ''' 

322 h = ham.toarray() if hasattr(ham, 'toarray') else np.asarray(ham) 

323 error_handling.square_matrix(h, 'ham') 

324 error_handling.hermitian_dense(h) 

325 n = len(h) 

326 error_handling.interaction(V, n) 

327 error_handling.real_number(mu, 'mu') 

328 error_handling.positive_real_zero(temperature, 'temperature') 

329 error_handling.pairing_amplitudes(delta0, n) 

330 error_handling.mixing(mixing) 

331 error_handling.positive_real(tol, 'tol') 

332 error_handling.positive_int(max_iter, 'max_iter') 

333 V = np.broadcast_to(np.asarray(V, dtype='f8'), (n,)) 

334 delta = np.broadcast_to(np.asarray(delta0, dtype='c16'), (n,)).copy() 

335 xi = h.astype('c16') - mu * np.eye(n) 

336 big = np.zeros((2 * n, 2 * n), 'c16') 

337 big[:n, :n], big[n:, n:] = xi, -xi.conj() 

338 diag = np.arange(n) 

339 for it in range(1, max_iter + 1): 

340 big[diag, diag + n] = delta 

341 big[diag + n, diag] = delta.conj() 

342 en, vec = np.linalg.eigh(big) 

343 pos = en > 0 

344 weight = np.tanh(en[pos] / (2 * temperature)) if temperature > 0 else np.ones(pos.sum()) 

345 new = V * np.einsum('in,in,n->i', vec[:n, pos], vec[n:, pos].conj(), weight) 

346 change = np.max(np.abs(new - delta)) 

347 delta = (1 - mixing) * delta + mixing * new 

348 if change < tol: 

349 break 

350 else: 

351 error_handling.converged(False, 's_wave_gap') 

352 return GapResult(new, en, vec, it)