Coverage for tbkit/kspace.py: 100%

1465 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-30 11:04 +0000

1from __future__ import annotations 

2 

3from concurrent.futures import ThreadPoolExecutor 

4from itertools import product 

5from typing import Literal, Sequence, overload 

6 

7import numpy as np 

8from numpy.typing import ArrayLike, NDArray 

9import matplotlib.pyplot as plt 

10from matplotlib.figure import Figure 

11from matplotlib.collections import LineCollection, PolyCollection 

12from matplotlib.colors import LogNorm, Normalize 

13from mpl_toolkits.mplot3d.art3d import Poly3DCollection 

14import scipy.linalg as LA 

15import scipy.sparse as sp 

16from scipy.integrate import cumulative_trapezoid 

17from scipy.special import expit, spence 

18import tbkit.error_handling as error_handling 

19import tbkit.dos as dos 

20import tbkit.occupation as occupation 

21import tbkit.neighbours as neighbours 

22import tbkit.values as values 

23from tbkit.lattice import Lattice 

24from tbkit.transport import _green_surface_bulk 

25 

26 

27PI = np.pi 

28 

29#: Pauli matrices (plus the identity, key ``'0'``), for building spinful 

30#: hoppings/onsite terms (spin-orbit coupling, Zeeman splitting, ...) when 

31#: :class:`KSpace` is constructed with ``spin=True``. 

32PAULI = { 

33 '0': np.eye(2, dtype='c16'), 

34 'x': np.array([[0., 1.], [1., 0.]], dtype='c16'), 

35 'y': np.array([[0., -1j], [1j, 0.]], dtype='c16'), 

36 'z': np.array([[1., 0.], [0., -1.]], dtype='c16'), 

37} 

38 

39# The symmetry kinds of KSpace.non_hermitian_class, in its argument order: 

40# (complex conjugation, transposition, minus sign) of U op(H(k)) U^dagger = 

41# +-H(+-k). TRS, PHS, CS, TRS^dagger, PHS^dagger, SLS, pseudo-Hermiticity. 

42_NH_KINDS = ((1, 0, 0), (0, 1, 1), (1, 1, 1), (0, 1, 0), (1, 0, 1), (0, 0, 1), (1, 1, 0)) 

43 

44# Names of KSpace.non_hermitian_class equivalent under H -> iH (Kawabata et 

45# al. 2019, Tables XII and XIV), mapped to the representative of the 38 classes. 

46_NH_CANONICAL = { 

47 'D†': 'AI', 'C†': 'AII', 'AII + S-': 'AI + S-', 'DIII + S-+': 'BDI + S-+', 

48 'DIII + S--': 'BDI + S--', 'CI + S-+': 'CII + S-+', 'CI + S--': 'CII + S--', 

49 'A + η': 'AIII', 'AI + η+': 'BDI†', 'AI + η-': 'DIII†', 'AII + η+': 'CII†', 

50 'AII + η-': 'CI†', 'D + η+': 'BDI', 'D + η-': 'DIII', 'C + η+': 'CII', 'C + η-': 'CI', 

51} 

52 

53 

54################################# 

55# CLASS KSPACE 

56################################# 

57 

58 

59def reciprocal_vectors(prim_vec: Sequence[tuple[float, ...]]) -> list[tuple[float, ...]]: 

60 r''' 

61 Get the reciprocal lattice vectors :math:`\mathbf{b}_i` such that 

62 :math:`\mathbf{a}_i\cdot\mathbf{b}_j = 2\pi\delta_{ij}`, lying in the 

63 span of the :math:`\mathbf{a}_i`. 

64 

65 Parameters 

66 ---------- 

67 prim_vec : List of one/two/three tuples 

68 Primitive vectors (see class **lattice**). 

69 

70 Returns 

71 ------- 

72 rec_vec : List of one/two/three tuples 

73 Reciprocal vectors, with 

74 as many components as the primitive vectors. 

75 ''' 

76 if len(prim_vec[0]) == 3: 

77 # b = 2 pi (A^+)^T: the dual basis within the span of the a_i 

78 rec = 2 * PI * np.linalg.pinv(np.array(prim_vec, dtype='f8')).T 

79 return [tuple(float(c) for c in b) for b in rec] 

80 if len(prim_vec) == 1: 

81 ax, ay = prim_vec[0] 

82 norm2 = ax ** 2 + ay ** 2 

83 return [(2*PI*ax/norm2, 2*PI*ay/norm2)] 

84 (a1x, a1y), (a2x, a2y) = prim_vec 

85 area = a1x * a2y - a1y * a2x 

86 b1 = (2*PI*a2y/area, -2*PI*a2x/area) 

87 b2 = (-2*PI*a1y/area, 2*PI*a1x/area) 

88 return [b1, b2] 

89 

90 

91class KSpace(): 

92 r''' 

93 Build and solve the Tight-Binding Bloch Hamiltonian :math:`H(\mathbf{k})` 

94 of a periodic lattice defined by the class **lattice**. 

95 

96 Hoppings are defined between orbitals of the unit cell, separated by a 

97 lattice vector :math:`\mathbf{R} = n_1\mathbf{a}_1+n_2\mathbf{a}_2`: 

98 

99 .. math:: 

100 

101 H_{ij}(\mathbf{k}) = \sum_{\mathbf{R}} t_{ij}(\mathbf{R})\, 

102 e^{i\mathbf{k}\cdot\mathbf{R}} 

103 

104 Parameters 

105 ---------- 

106 lat : **lattice** class instance 

107 Only *unit_cell* and *prim_vec* 

108 are used (the instance need not call *get_lattice*). 

109 spin : Boolean, default False 

110 If True, every site of 

111 *unit_cell* carries a spin-1/2 degree of freedom (*norb* doubles to 

112 ``2*len(unit_cell)``, ordered site-major: orbitals ``2*i, 2*i+1`` 

113 are the up/down components of site *i*). *set_onsite* and 

114 *set_hopping* then accept 2x2 (spin) matrices in addition to plain 

115 numbers, to build spin-orbit coupling or Zeeman terms -- see 

116 :data:`PAULI` for ready-made Pauli matrices. 

117 

118 Examples 

119 -------- 

120 Example usage:: 

121 

122 # graphene, nearest-neighbor hopping t 

123 DX, DY = 0.5 * 3 ** 0.5, 0.5 

124 unit_cell = [{'tag': 'a', 'r0': (0., 0.)}, {'tag': 'b', 'r0': (DX, DY)}] 

125 prim_vec = [(2*DX, 0.), (DX, 1.5)] 

126 lat = Lattice(unit_cell=unit_cell, prim_vec=prim_vec) 

127 gra = KSpace(lat) 

128 gra.set_hopping([{'i': 0, 'j': 1, 'R': (0, 0), 't': 1.}, 

129 {'i': 0, 'j': 1, 'R': (-1, 0), 't': 1.}, 

130 {'i': 0, 'j': 1, 'R': (0, -1), 't': 1.}]) 

131 ''' 

132 

133 workers = 1 # threads diagonalizing over k, see set_workers 

134 # _bloch_sum groups the hoppings by lattice vector when they fill at 

135 # least this fraction of the dense (n_R, norb, norb) blocks; sparser 

136 # models (large supercells) are summed hopping by hopping 

137 group_fill = 0.01 

138 

139 def __init__(self, lat: Lattice, spin: bool = False) -> None: 

140 error_handling.lat(lat) 

141 error_handling.boolean(spin, 'spin') 

142 error_handling.independent(lat.prim_vec) 

143 self.lat = lat 

144 self.dim = len(lat.prim_vec) 

145 self.space_dim = len(lat.prim_vec[0]) 

146 self.spin = spin 

147 self.n_sites = len(lat.unit_cell) 

148 self.norb = 2*self.n_sites if spin else self.n_sites 

149 self.tags = np.array([dic['tag'] for dic in lat.unit_cell]) 

150 self.onsite = np.zeros(self.norb, 'c16') 

151 # spin-off-diagonal onsite terms (in-plane Zeeman, onsite Rashba): 

152 # they have no place on the diagonal `onsite` array. 

153 self._onsite_offdiag = np.zeros((self.norb, self.norb), 'c16') 

154 self._hop_const: list = [] # list of (i, j, R_cartesian (np.ndarray), t) 

155 # value functions (see tbkit.values): (i, j, R_cartesian, function, 

156 # hermitian), with arrays of sites i, j and bond vectors R 

157 self._hop_values: list = [] 

158 #: Parameters of the value functions (see *set_params*). 

159 self.params: dict = {} 

160 self._nonreciprocal = False # set_hopping(hermitian=False) was used 

161 self._overlap_hop: list = [] # overlaps, as _hop: list of (i, j, R_cartesian, s) 

162 self.rec_vec = reciprocal_vectors(lat.prim_vec) 

163 # Orthonormal basis (columns) of the span of prim_vec, in which k is 

164 # given: the identity when the lattice fills its space, a1/|a1| for a 

165 # chain, the Gram-Schmidt basis of (a1, a2) for a 2D lattice in 3D. 

166 self.k_basis = np.linalg.qr(np.array(lat.prim_vec, dtype='f8').T)[0] 

167 if self.dim == self.space_dim: 

168 self.k_basis = np.eye(self.space_dim) 

169 elif self.dim == 1: 

170 a1 = np.asarray(lat.prim_vec[0], dtype='f8') 

171 self.k_basis = (a1 / np.linalg.norm(a1))[:, None] 

172 else: 

173 # keep the first axis along a1 (QR may flip signs) 

174 signs = np.sign(np.diag(np.array(lat.prim_vec, dtype='f8') @ self.k_basis)) 

175 self.k_basis = self.k_basis * np.where(signs == 0, 1., signs)[None, :] 

176 #: Reciprocal vectors in the k coordinates used by *get_ham* (rows). 

177 self.rec_vec_k = np.array(self.rec_vec, dtype='f8') @ self.k_basis 

178 self.ks = np.array([]) # k-points of the last band-structure calculation 

179 self.ks_dist = np.array([]) # cumulative distance along the k-path 

180 self.nodes = np.array([]) # positions, along ks_dist, of the k-path nodes 

181 self.en = np.array([]) # bands, shape (len(ks), norb) 

182 

183 @property 

184 def _hop(self) -> list: 

185 ''' 

186 Private. The hoppings, as a list of (i, j, R_cartesian, t): those 

187 given as numbers, then the value functions evaluated with *params*. 

188 Assigning it replaces both. 

189 ''' 

190 return self._hops(self.params) 

191 

192 @_hop.setter 

193 def _hop(self, hops: list) -> None: 

194 self._hop_const = list(hops) 

195 self._hop_values = [] 

196 

197 def _hops(self, params: dict) -> list: 

198 ''' 

199 Private method. *_hop*, with the value functions evaluated with *params*. 

200 ''' 

201 hops = list(self._hop_const) 

202 if not self._hop_values: 

203 return hops 

204 tau = np.array([dic['r0'] for dic in self.lat.unit_cell], dtype='f8').reshape(self.n_sites, -1) 

205 for i, j, R, func, hermitian in self._hop_values: 

206 site_i = values.sites(tau[i], self.tags[i], i) 

207 site_j = values.sites(tau[j] + R, self.tags[j], j) 

208 t = values.evaluate(func, (site_i, site_j), params, len(i), self.spin) 

209 if self.spin: 

210 pairs = [(2*ii + a, 2*jj + b, RR, tt[a, b], np.conj(tt[a, b])) 

211 for ii, jj, RR, tt in zip(i, j, R, t) for a in range(2) for b in range(2)] 

212 else: 

213 pairs = [(ii, jj, RR, tt, np.conj(tt)) for ii, jj, RR, tt in zip(i, j, R, t)] 

214 for ii, jj, RR, tt, tc in pairs: 

215 hops.append((int(ii), int(jj), RR, tt)) 

216 if hermitian: 

217 hops.append((int(jj), int(ii), -RR, tc)) 

218 return hops 

219 

220 def set_params(self, **params) -> None: 

221 ''' 

222 Set the parameters of the value functions of *set_hopping*: every 

223 method (*get_bands*, *chern_number*, *finite_ham*, ...) then uses 

224 these values. *get_ham* also takes them per call. 

225 

226 Parameters 

227 ---------- 

228 params 

229 Parameter values, added to (or replacing those of) *params*. 

230 

231 Examples 

232 -------- 

233 Example usage:: 

234 

235 ks.set_params(m=0.5) 

236 ks.chern_number() 

237 ''' 

238 self.params.update(params) 

239 

240 def set_onsite(self, dict_onsite: dict[str, complex | Sequence[complex]]) -> None: 

241 ''' 

242 Set the onsite energies, by sublattice tag. 

243 

244 Parameters 

245 ---------- 

246 dict_onsite : Dictionary 

247 key: tag, val: onsite energy 

248 (a plain number), or, if ``spin=True``, a plain number (applied 

249 equally to both spins), a pair ``(E_up, E_down)`` of numbers (a 

250 spin splitting along z), or a 2x2 complex matrix (a general spin 

251 structure, e.g. an in-plane Zeeman field built from :data:`PAULI`). 

252 A complex onsite energy (gain/loss), or a non-Hermitian 2x2 

253 block, makes :math:`H(\\mathbf{k})` non-Hermitian: *get_bands* 

254 then returns complex energies, sorted by their real part. 

255 

256 Examples 

257 -------- 

258 Example usage:: 

259 

260 kag.set_onsite({'a': 1., 'b': -1.}) 

261 # spinful: same onsite energy for both spins on 'a', a Zeeman 

262 # splitting along z on 'b': 

263 kag_spin.set_onsite({'a': 1., 'b': (1., -1.)}) 

264 # spinful: an in-plane Zeeman field on 'a': 

265 kag_spin.set_onsite({'a': Bx*PAULI['x']}) 

266 ''' 

267 error_handling.set_onsite_kspace(dict_onsite, self.lat.tags, self.spin) 

268 for tag, val in dict_onsite.items(): 

269 sites = np.where(self.tags == tag)[0] 

270 if self.spin: 

271 if np.ndim(val) == 0: 

272 block = val * PAULI['0'] 

273 elif np.ndim(val) == 2: 

274 block = np.asarray(val, 'c16') 

275 else: 

276 block = np.diag(np.asarray(val, 'c16')) 

277 self.onsite[2*sites] = block[0, 0] 

278 self.onsite[2*sites + 1] = block[1, 1] 

279 for site in sites: 

280 self._onsite_offdiag[2*site, 2*site + 1] = block[0, 1] 

281 self._onsite_offdiag[2*site + 1, 2*site] = block[1, 0] 

282 else: 

283 self.onsite[sites] = val 

284 

285 def set_hopping(self, list_hop: list[dict], hermitian: bool = True) -> None: 

286 r''' 

287 Set the hoppings between orbitals of the unit cell. 

288 

289 Only one representative of each hopping needs to be given: its 

290 Hermitian conjugate (:math:`j\to i`, :math:`\mathbf{R}\to-\mathbf{R}`) 

291 is added automatically (unless ``hermitian=False``). 

292 

293 Parameters 

294 ---------- 

295 list_hop 

296 List of dictionaries with keys ('i', 'j', 'R', 't'): 

297 

298 * 'i', 'j': Positive integers. Site indices within the unit cell 

299 (following the order of *unit_cell*). 

300 * 'R': Tuple of one/two integers :math:`(n_1, n_2)`. Lattice vector 

301 :math:`\mathbf{R}=n_1\mathbf{a}_1+n_2\mathbf{a}_2` separating the 

302 two sites. 

303 * 't': Complex number, or, if ``spin=True``, either a complex 

304 number (spin-independent hopping) or a 2x2 complex matrix (a 

305 general, possibly spin-mixing, hopping -- e.g. built from 

306 :data:`PAULI` for Rashba or intrinsic spin-orbit coupling). 

307 Or a value function ``t(site_i, site_j, **params)``, 

308 evaluated by ``get_ham(k, **params)`` (and by every other 

309 method, with the parameters of *set_params*): *site_i* and 

310 *site_j* are structured arrays of the bonds' end points, 

311 *site_i* in the home cell and *site_j* in cell 

312 :math:`\mathbf{R}` (fields 'x', 'y', ('z'), 'tag', 'index' -- 

313 the site index within *unit_cell*). It is called once with 

314 every bond of the dictionaries that share it, and returns one 

315 value, or one per bond (or, if ``spin=True``, a 2x2 matrix, 

316 or one per bond). 

317 

318 *list_hop* may instead use the neighbour-order form of 

319 *System.set_hopping*: dictionaries with keys ('n', 't') and 

320 optionally 'ang' and/or 'tag' -- 'n' the neighbour order (1st, 

321 2nd, ... shortest distance between orbitals of the infinite 

322 lattice), 'ang' a bond angle in degrees, 'tag' a sublattice pair 

323 such as ``'ab'``. The bonds are found from *unit_cell* and 

324 *prim_vec* (in 1D, 2D and 3D) and turned into the explicit form 

325 by *tbkit.neighbours.neighbour_hoppings* (see there for the bond 

326 orientations; a negative angle addresses the reversed bonds). 

327 A list uses one form or the other, not both. 

328 hermitian : Boolean, default True 

329 If False, the Hermitian 

330 conjugates are *not* added: each dictionary is one matrix element 

331 :math:`H_{ij}(\mathbf{R})` only, so that non-reciprocal 

332 (non-Hermitian) hoppings can be built -- give the reverse hopping 

333 explicitly, with its own amplitude. 

334 

335 Examples 

336 -------- 

337 Example usage:: 

338 

339 # graphene, by neighbour order: nearest (t) and next-nearest 

340 # (t2) neighbours, as in System.set_hopping 

341 gra.set_hopping([{'n': 1, 't': t}, {'n': 2, 't': t2}]) 

342 # square lattice, anisotropic nearest neighbours 

343 sq.set_hopping([{'n': 1, 'ang': 0., 't': tx}, {'n': 1, 'ang': 90., 't': ty}]) 

344 

345 # 1D chain, nearest-neighbor hopping t between the only orbital 

346 # and its right neighbor: 

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

348 # spinful: spin-independent hopping t, plus a Rashba-like 

349 # spin-flip term of strength alpha: 

350 chain_spin.set_hopping([{'i': 0, 'j': 0, 'R': (1,), 

351 't': t*PAULI['0'] + 1j*alpha*PAULI['y']}]) 

352 # value function: a staggered hopping, swept without rebuilding 

353 chain.set_hopping([{'n': 1, 't': lambda si, sj, t, d: t + d * (-1) ** si['index']}]) 

354 ham = chain.get_ham((0.,), t=1., d=0.2) 

355 ''' 

356 error_handling.boolean(hermitian, 'hermitian') 

357 if error_handling.hopping_form(list_hop): 

358 list_hop = neighbours.neighbour_hoppings(self.lat, list_hop, self.spin, hermitian) 

359 error_handling.set_hopping_kspace(list_hop, self.n_sites, self.dim, self.spin, values=True) 

360 if not hermitian: 

361 self._nonreciprocal = True 

362 funcs: dict = {} # the bonds of each value function: id -> (function, [(i, j, R_cart)]) 

363 for dic in list_hop: 

364 R_cart = np.zeros(self.space_dim) 

365 for n, vec in zip(dic['R'], self.lat.prim_vec): 

366 R_cart += n * np.array(vec) 

367 i, j, t = dic['i'], dic['j'], dic['t'] 

368 if callable(t): 

369 funcs.setdefault(id(t), (t, []))[1].append((i, j, R_cart)) 

370 elif self.spin: 

371 block = t*PAULI['0'] if np.ndim(t) == 0 else np.asarray(t, 'c16') 

372 for a in range(2): 

373 for b in range(2): 

374 self._hop_const.append((2*i+a, 2*j+b, R_cart, block[a, b])) 

375 if hermitian: 

376 self._hop_const.append((2*j+b, 2*i+a, -R_cart, np.conj(block[a, b]))) 

377 else: 

378 self._hop_const.append((i, j, R_cart, t)) 

379 if hermitian: 

380 self._hop_const.append((j, i, -R_cart, np.conj(t))) 

381 for func, bonds in funcs.values(): 

382 i, j, R = zip(*bonds) 

383 self._hop_values.append((np.array(i), np.array(j), np.array(R), func, hermitian)) 

384 

385 def set_overlap(self, list_hop: list[dict]) -> None: 

386 r''' 

387 Set the overlaps :math:`s_{ij}(\mathbf{R}) = \langle i,\mathbf{0}|j,\mathbf{R}\rangle` 

388 of a non-orthogonal basis, in the format of *set_hopping* (key 't' 

389 for the overlap; the Hermitian conjugates are added): 

390 

391 .. math:: 

392 

393 S(\mathbf{k}) = \mathbb{1} + \sum_{\mathbf{R}} s(\mathbf{R})\,e^{i\mathbf{k}\cdot\mathbf{R}}\, , 

394 

395 after which the bands solve :math:`H(\mathbf{k})v = E\,S(\mathbf{k})v`. 

396 (With graphene's nearest-neighbour overlap :math:`s`, the 

397 :math:`\pi` bands become :math:`E = (\epsilon \pm t|f|)/(1 \pm s|f|)`: 

398 no longer symmetric about :math:`\epsilon`.) The topological tools use 

399 the Lowdin-orthonormalized states :math:`S^{1/2}v`. 

400 

401 Parameters 

402 ---------- 

403 list_hop 

404 List of dictionaries ('i', 'j', 'R', 't'), or by 

405 neighbour order ('n', 't', optionally 'ang', 'tag'), see 

406 *set_hopping* (numbers only, no value functions). 

407 ''' 

408 if error_handling.hopping_form(list_hop): 

409 list_hop = neighbours.neighbour_hoppings(self.lat, list_hop, self.spin) 

410 error_handling.set_hopping_kspace(list_hop, self.n_sites, self.dim, self.spin) 

411 saved = self._hop_const, self._hop_values, self._nonreciprocal 

412 self._hop = [] 

413 self.set_hopping(list_hop) 

414 self._overlap_hop += self._hop_const 

415 self._hop_const, self._hop_values, self._nonreciprocal = saved 

416 

417 def get_overlap(self, k: ArrayLike) -> NDArray[np.complex128]: 

418 r''' 

419 Get the overlap matrix :math:`S(\mathbf{k})` (the identity for an 

420 orthogonal basis), see *set_overlap*. 

421 

422 Parameters 

423 ---------- 

424 k : k point (see *get_ham*) 

425 

426 Returns 

427 ------- 

428 S : Complex ndarray, shape (norb, norb) 

429 ''' 

430 error_handling.k_vector(k, 'k', self.dim) 

431 k_cart = self.k_basis @ np.asarray(k, dtype='f8') 

432 return np.eye(self.norb) + self._bloch_sum(self._overlap_hop, k_cart[None])[0][0] 

433 

434 def clear_hopping(self) -> None: 

435 ''' 

436 Clear the hoppings set by *set_hopping* (and the overlaps of *set_overlap*). 

437 ''' 

438 self._hop = [] 

439 self._nonreciprocal = False 

440 self._overlap_hop = [] 

441 

442 def get_ham(self, k: ArrayLike, **params) -> NDArray[np.complex128]: 

443 r''' 

444 Get the dense Bloch Hamiltonian :math:`H(\mathbf{k})`. 

445 

446 Parameters 

447 ---------- 

448 k : Tuple/list/ndarray of *dim* real numbers 

449 When the lattice 

450 fills its space (2D in the plane, 3D in space), the 

451 :math:`\mathbf{k}` point in the same Cartesian frame as *prim_vec*. 

452 In 1D, the crystal momentum *along* the primitive vector (so the 

453 Brillouin zone spans :math:`2\pi/|\mathbf{a}_1|`), which 

454 coincides with :math:`k_x` for a chain aligned with :math:`x`. 

455 For a 2D lattice in 3D space (a slab), the components along the 

456 orthonormal basis *k_basis* of its plane (the first along 

457 :math:`\mathbf{a}_1`). *rec_vec_k* holds the reciprocal vectors in 

458 these coordinates. 

459 params 

460 Values of the parameters of the value functions of 

461 *set_hopping*, for this call only: they override those of 

462 *set_params*. Each function gets the ones its signature names 

463 (all of them with ``**kwargs``); the others are ignored. 

464 

465 Returns 

466 ------- 

467 ham : Complex ndarray, shape (norb, norb) 

468 

469 Examples 

470 -------- 

471 Example usage:: 

472 

473 hams = [ks.get_ham((0., 0.), m=m) for m in np.linspace(-1., 1., 21)] 

474 ''' 

475 error_handling.k_vector(k, 'k', self.dim) 

476 return self._bloch_ham((self.k_basis @ np.asarray(k, dtype='f8'))[None], 

477 params={**self.params, **params})[0] 

478 

479 def get_ham_peierls(self, k: ArrayLike, A: ArrayLike) -> NDArray[np.complex128]: 

480 r''' 

481 Get the Bloch Hamiltonian in a uniform vector potential 

482 :math:`\mathbf{A}` (:math:`\hbar = e = 1`): every hopping picks up the 

483 Peierls phase :math:`e^{i\mathbf{A}\cdot\mathbf{d}}` of its bond 

484 vector :math:`\mathbf{d} = \mathbf{R} + \boldsymbol\tau_j - \boldsymbol\tau_i` 

485 (see *System.set_peierls_phase*). A time-dependent :math:`\mathbf{A}(t)` 

486 is a uniform electric field :math:`-\partial_t\mathbf{A}`, e.g. light 

487 (see *tbkit.floquet*). 

488 

489 Parameters 

490 ---------- 

491 k : k point (see *get_ham*) 

492 A : Tuple/ndarray of space_dim real numbers 

493 Vector potential. 

494 

495 Returns 

496 ------- 

497 ham : Complex ndarray, shape (norb, norb) 

498 ''' 

499 error_handling.k_vector(k, 'k', self.dim) 

500 A = np.asarray(A, dtype='f8') 

501 error_handling.ndarray(A, 'A', self.space_dim) 

502 return self._bloch_ham((self.k_basis @ np.asarray(k, dtype='f8'))[None], A)[0] 

503 

504 def _bloch_ham( 

505 self, k_cart: NDArray[np.float64], A: NDArray[np.float64] | None = None, params: dict | None = None, 

506 ) -> NDArray[np.complex128]: 

507 ''' 

508 Private method. *get_ham* (or, with a vector potential *A*, 

509 *get_ham_peierls*) at many k-points at once: *k_cart* has shape 

510 (nk, space_dim) and the result (nk, norb, norb). The value functions 

511 take *params* (default: *self.params*). 

512 ''' 

513 hops = self._hops(self.params if params is None else params) 

514 return self._bloch_sum(hops, k_cart, A=A)[0] + (np.diag(self.onsite) + self._onsite_offdiag)[None] 

515 

516 def get_bands( 

517 self, ks: ArrayLike, eigenvec: bool = False, 

518 ) -> NDArray[np.float64] | tuple[NDArray[np.float64], NDArray[np.complex128]]: 

519 r''' 

520 Diagonalize :math:`H(\mathbf{k})` over a set of k-points, and keep 

521 the result for *plot_bands* (plotted against the cumulative distance 

522 between consecutive k-points). 

523 

524 Parameters 

525 ---------- 

526 ks : ndarray, shape (nk, dim) 

527 k-points. 

528 eigenvec : Boolean, default False 

529 If True, also return 

530 the eigenvectors. 

531 

532 Returns 

533 ------- 

534 en : Real ndarray, shape (nk, norb) 

535 Band energies, sorted ascending. 

536 Complex, and sorted by real part, if :math:`H(\mathbf{k})` is 

537 non-Hermitian (complex onsite energies, see *set_onsite*). 

538 vn : Complex ndarray, shape (nk, norb, norb), only if *eigenvec* is True 

539 vn[k, :, n] is the nth (right) eigenvector at ks[k]. 

540 ''' 

541 ks = np.atleast_2d(np.asarray(ks, dtype='f8')) 

542 error_handling.ks(ks, self.dim) 

543 out = self._diagonalize(ks, eigenvec) 

544 self.ks = ks 

545 self.en = out[0] if isinstance(out, tuple) else out 

546 steps = np.linalg.norm(np.diff(ks, axis=0), axis=1) 

547 self.ks_dist = np.concatenate([[0.], np.cumsum(steps)])[:len(ks)] 

548 self.nodes = np.array([]) 

549 return out 

550 

551 @overload 

552 def _diagonalize( 

553 self, ks: NDArray[np.float64], eigenvec: Literal[False] = ..., 

554 ) -> NDArray[np.float64]: ... 

555 @overload 

556 def _diagonalize( 

557 self, ks: NDArray[np.float64], eigenvec: Literal[True], 

558 ) -> tuple[NDArray[np.float64], NDArray[np.complex128]]: ... 

559 @overload 

560 def _diagonalize( 

561 self, ks: NDArray[np.float64], eigenvec: bool, 

562 ) -> NDArray[np.float64] | tuple[NDArray[np.float64], NDArray[np.complex128]]: ... 

563 

564 def _diagonalize( 

565 self, ks: NDArray[np.float64], eigenvec: bool = False, 

566 ) -> NDArray[np.float64] | tuple[NDArray[np.float64], NDArray[np.complex128]]: 

567 r''' 

568 Private method. Diagonalize :math:`H(\mathbf{k})` over the k-points 

569 *ks* (shape (nk, dim)), without touching the stored band structure. 

570 ''' 

571 return self._eigs(ks, eigenvec) 

572 

573 def set_workers(self, workers: int) -> None: 

574 r''' 

575 Set the number of threads that diagonalize :math:`H(\mathbf{k})` 

576 over many k-points (*get_bands*, *mesh_bands*, *berry_curvature*, 

577 the Wilson loops, ...). The k-points are split into chunks, each 

578 diagonalized by one stacked LAPACK call, which releases the GIL, so 

579 the chunks run in parallel. *surface_spectral_function* spreads its 

580 k-points (and, with fewer k-points than threads, chunks of its 

581 energies) over the threads in the same way. The results depend on 

582 *workers* only up to rounding (and the phases of the eigenvectors). 

583 

584 Parameters 

585 ---------- 

586 workers : Positive integer 

587 Number of threads (default 1, no threads). 

588 

589 Examples 

590 -------- 

591 Example usage:: 

592 

593 ks.set_workers(4) 

594 curv = ks.berry_curvature(0, nk=400) 

595 ''' 

596 error_handling.positive_int(workers, 'workers') 

597 self.workers = workers 

598 

599 def _hams(self, ks: NDArray[np.float64]) -> NDArray[np.complex128]: 

600 ''' 

601 Private method. *get_ham* at the k-points *ks* (shape (nk, dim)), 

602 shape (nk, norb, norb): one Bloch sum, unless a subclass overrides 

603 *get_ham*, which is then called per k-point (a Floquet model 

604 overrides *_hams* too, see *floquet.DrivenKSpace._hams*). 

605 ''' 

606 if type(self).get_ham is KSpace.get_ham: 

607 return self._bloch_ham(ks @ self.k_basis.T) 

608 return np.array([self.get_ham(k) for k in ks], dtype='c16') 

609 

610 def _overlaps(self, ks: NDArray[np.float64]) -> NDArray[np.complex128]: 

611 ''' 

612 Private method. *get_overlap* at the k-points *ks*, shape (nk, norb, norb). 

613 ''' 

614 return np.eye(self.norb) + self._bloch_sum(self._overlap_hop, ks @ self.k_basis.T)[0] 

615 

616 @overload 

617 def _eigs(self, ks: NDArray[np.float64], eigenvec: Literal[False] = ...) -> NDArray: ... 

618 @overload 

619 def _eigs( 

620 self, ks: NDArray[np.float64], eigenvec: Literal[True], 

621 ) -> tuple[NDArray, NDArray[np.complex128]]: ... 

622 @overload 

623 def _eigs( 

624 self, ks: NDArray[np.float64], eigenvec: bool, 

625 ) -> NDArray | tuple[NDArray, NDArray[np.complex128]]: ... 

626 

627 def _eigs( 

628 self, ks: NDArray[np.float64], eigenvec: bool = False, 

629 ) -> NDArray | tuple[NDArray, NDArray[np.complex128]]: 

630 ''' 

631 Private method. *_eig* at the k-points *ks* (shape (nk, dim)): the 

632 eigenvalues, shape (nk, norb), and the eigenvectors, shape 

633 (nk, norb, norb). The k-points go in chunks of bounded memory, 

634 spread over *workers* threads (see *set_workers*). 

635 ''' 

636 chunk = max(1, min(400000 // self.norb ** 2, -(-len(ks) // self.workers))) 

637 parts = [ks[c0:c0 + chunk] for c0 in range(0, len(ks), chunk)] or [ks] 

638 if self.workers > 1 and len(parts) > 1: 

639 with ThreadPoolExecutor(self.workers) as pool: 

640 out = list(pool.map(lambda part: self._eig_chunk(part, eigenvec), parts)) 

641 else: 

642 out = [self._eig_chunk(part, eigenvec) for part in parts] 

643 if eigenvec: 

644 return np.concatenate([o[0] for o in out]), np.concatenate([o[1] for o in out]) 

645 return np.concatenate(out) 

646 

647 def _eig_chunk( 

648 self, ks: NDArray[np.float64], eigenvec: bool, 

649 ) -> NDArray | tuple[NDArray, NDArray[np.complex128]]: 

650 r''' 

651 Private method. One chunk of *_eigs*: the Hermitian solver when 

652 *is_hermitian*, otherwise the general one, with the eigenvalues 

653 sorted by real part, both stacked over the k-points. With an overlap 

654 (see *set_overlap*), the generalized problem :math:`Hv = ES(\mathbf{k})v`: 

655 the Hermitian one is reduced by the Cholesky factor 

656 :math:`S = LL^\dagger` to :math:`L^{-1}HL^{-\dagger}` (eigenvectors 

657 :math:`L^{-\dagger}v`, normalized to :math:`v^\dagger Sv = 1` as by 

658 *scipy.linalg.eigh*); the non-Hermitian one has no stacked solver, 

659 and is solved one k-point at a time. 

660 ''' 

661 ham = self._hams(ks) 

662 hermitian = self.is_hermitian() 

663 en: NDArray 

664 vn: NDArray 

665 if self._overlap_hop and hermitian: 

666 l_inv = np.linalg.inv(np.linalg.cholesky(self._overlaps(ks))) 

667 l_inv_h = l_inv.conj().transpose(0, 2, 1) 

668 ham = l_inv @ ham @ l_inv_h 

669 if hermitian: 

670 if not eigenvec: 

671 return np.linalg.eigvalsh(ham) 

672 en, vn = np.linalg.eigh(ham) 

673 return en, (l_inv_h @ vn if self._overlap_hop else vn) 

674 if self._overlap_hop: 

675 solved = [LA.eig(h, s) for h, s in zip(ham, self._overlaps(ks))] 

676 en = np.array([w for w, _ in solved], dtype='c16').reshape(len(ks), self.norb) 

677 vn = np.array([v for _, v in solved], dtype='c16').reshape(ham.shape) 

678 elif eigenvec: 

679 en, vn = np.linalg.eig(ham) 

680 else: 

681 en = np.linalg.eigvals(ham) 

682 ind = np.argsort(en.real, axis=1, kind='stable') 

683 if not eigenvec: 

684 return np.take_along_axis(en, ind, axis=1) 

685 return np.take_along_axis(en, ind, axis=1), np.take_along_axis(vn, ind[:, None, :], axis=2) 

686 

687 def is_hermitian(self) -> bool: 

688 ''' 

689 Check whether :math:`H(\\mathbf{k})` is Hermitian: it is, unless an 

690 onsite term is not, or *set_hopping* was called with 

691 ``hermitian=False`` (such a model is always treated as 

692 non-Hermitian, and diagonalized with the general solver). 

693 

694 Returns 

695 ------- 

696 hermitian : Boolean 

697 ''' 

698 return bool(not self._nonreciprocal and np.all(self.onsite.imag == 0.) 

699 and np.array_equal(self._onsite_offdiag, self._onsite_offdiag.conj().T)) 

700 

701 def k_path( 

702 self, points: list[ArrayLike], nk: int, 

703 ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

704 r''' 

705 Build a k-path through a list of high-symmetry points, and get the 

706 associated bands. 

707 

708 Parameters 

709 ---------- 

710 points 

711 List of at least two k-points (each a tuple/list of 

712 *dim* real numbers), e.g. from *high_symmetry_path*. 

713 nk : Positive integer 

714 Number of k-points per path segment. 

715 

716 Returns 

717 ------- 

718 ks_dist : Real ndarray 

719 Cumulative distance along the path, 

720 to be used as the x-axis of a band-structure plot. 

721 en : Real ndarray, shape (len(ks_dist), norb) 

722 Band energies. 

723 ''' 

724 error_handling.k_path_points(points, self.dim) 

725 error_handling.positive_int(nk, 'nk') 

726 ks, _, nodes = _path(points, nk) 

727 self.get_bands(ks) 

728 self.nodes = np.array(nodes) 

729 return self.ks_dist, self.en 

730 

731 def mesh_grid( 

732 self, nk: int | tuple[int, ...], monkhorst_pack: bool = False, 

733 ) -> tuple[list[NDArray[np.float64]], NDArray[np.float64]]: 

734 r''' 

735 Build a uniform grid of fractional coordinates spanning the 

736 Brillouin zone, and the corresponding Cartesian k-points. By 

737 default the grid contains :math:`\Gamma`: :math:`r/q`, 

738 :math:`r = 0, \dots, q-1` (each in [0, 1)). With *monkhorst_pack*, 

739 it is the grid of Monkhorst and Pack (Phys. Rev. B 13, 5188 (1976)), 

740 :math:`(2r - q - 1)/2q`, :math:`r = 1, \dots, q` (each in 

741 (-1/2, 1/2)), symmetric about :math:`\Gamma`: the same points for 

742 odd :math:`q`, shifted by half a step (off :math:`\Gamma` and the 

743 zone boundary) for even :math:`q`. Both integrate exactly every 

744 Fourier component :math:`e^{i\mathbf{k}\cdot\mathbf{R}}` with 

745 :math:`|R_i| < q`. 

746 

747 Parameters 

748 ---------- 

749 nk : Positive integer, or tuple of *dim* positive integers 

750 Number of k-points along each reciprocal lattice vector. 

751 monkhorst_pack : Boolean, default False 

752 If True, the 

753 Monkhorst-Pack grid. 

754 

755 Returns 

756 ------- 

757 fracs 

758 List of *dim* real ndarrays, shape (nk1, nk2) each 

759 (or (nk1,) in 1D): fractional coordinates of the grid. 

760 ks : Real ndarray, shape (nk1*nk2, dim) (or (nk1, dim) in 1D) 

761 ''' 

762 error_handling.nk(nk, self.dim) 

763 error_handling.boolean(monkhorst_pack, 'monkhorst_pack') 

764 if isinstance(nk, int): 

765 nk = (nk,) * self.dim 

766 if monkhorst_pack: 

767 axes = [(2 * np.arange(1, n + 1) - n - 1) / (2 * n) for n in nk] 

768 else: 

769 axes = [np.arange(n) / n for n in nk] 

770 fracs = np.meshgrid(*axes, indexing='ij') 

771 ks = sum(f.ravel()[:, None] * b[None, :] for f, b in zip(fracs, self.rec_vec_k)) 

772 if self.dim == 1: 

773 return [fracs[0]], ks 

774 return list(fracs), ks 

775 

776 def mesh_bands(self, nk: int | tuple[int, ...], monkhorst_pack: bool = False) -> NDArray[np.float64]: 

777 ''' 

778 Diagonalize :math:`H(\\mathbf{k})` over a uniform mesh spanning the 

779 Brillouin zone. Unlike *get_bands*, it leaves the band structure kept 

780 for *plot_bands* untouched. 

781 

782 Parameters 

783 ---------- 

784 nk : Positive integer, or tuple of *dim* positive integers 

785 Number of k-points along each reciprocal lattice vector. 

786 monkhorst_pack : Boolean, default False 

787 If True, the 

788 Monkhorst-Pack grid (see *mesh_grid*). 

789 

790 Returns 

791 ------- 

792 en : Real ndarray, shape (nk1*nk2, norb) 

793 Band energies 

794 over the mesh (flattened). 

795 ''' 

796 _, ks = self.mesh_grid(nk, monkhorst_pack) 

797 return self._diagonalize(ks) 

798 

799 def get_fermi_level( 

800 self, n_electrons: float, nk: int | tuple[int, ...] = 30, temperature: float = 0., 

801 ) -> float: 

802 ''' 

803 Get the Fermi level holding *n_electrons* electrons per unit cell 

804 (one electron per band and k-point), from the bands over a uniform 

805 Brillouin-zone mesh (see *tbkit.occupation.fermi_level*). 

806 

807 Parameters 

808 ---------- 

809 n_electrons : Positive real or zero, at most norb 

810 Electrons per unit cell. 

811 nk : Positive integer, or tuple, default 30 

812 k-mesh. 

813 temperature : Positive real or zero, default 0 

814 

815 Returns 

816 ------- 

817 e_fermi : Real number 

818 ''' 

819 en = self.mesh_bands(nk) 

820 return occupation.fermi_level(en, n_electrons, temperature, 

821 weights=np.full(en.shape, 1. / len(en))) 

822 

823 def berry_curvature( 

824 self, bands: int | list[int], nk: int | tuple[int, int] = 30, 

825 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

826 ) -> NDArray[np.float64]: 

827 r''' 

828 Get the Berry curvature of a group of bands over a uniform 

829 Brillouin-zone mesh, using the gauge-invariant lattice method of 

830 Fukui, Hatsugai and Suzuki (J. Phys. Soc. Jpn. 74, 1674 (2005)): 

831 the flux through each mesh plaquette is minus the phase of the 

832 product of the (Slater-determinant) overlaps between the occupied 

833 subspaces at its four corners, 

834 

835 .. math:: 

836 

837 \Omega_{\square} = -\arg\prod_{\langle\mathbf{k}\mathbf{k}'\rangle\in\partial\square} 

838 \det\langle u(\mathbf{k})|u(\mathbf{k}')\rangle\, , 

839 

840 the corners being visited counterclockwise. This is the discrete 

841 version of :math:`\Omega = \partial_{k_x}A_y - \partial_{k_y}A_x`, 

842 :math:`\mathbf{A} = i\langle u|\nabla_{\mathbf{k}}u\rangle`, and does 

843 not depend on the orientation (handedness) of *prim_vec*. The local 

844 curvature is that of the periodic gauge of *get_ham* (orbitals on the 

845 cell origin), which differs point by point from the one with the 

846 orbital positions included (see *quantum_geometric_tensor*); their 

847 integral, the Chern number, is the same. For a 

848 non-Hermitian :math:`H(\mathbf{k})` (see *set_onsite*), the bands 

849 are ordered by the real part of their energy and the overlaps are 

850 taken between right eigenvectors (the "RR" curvature of 

851 *biorthogonal_berry_curvature*). That ordering follows the bands 

852 continuously only if a real line gap separates *bands* from the 

853 others over the whole zone: without one (an imaginary line gap, 

854 or exceptional points), a ValueError is raised instead of a 

855 meaningless result -- see *biorthogonal_berry_curvature*. 

856 

857 Parameters 

858 ---------- 

859 bands : Positive integer, or list of positive integers 

860 Band 

861 index, or indices of a group of bands (e.g. all occupied bands 

862 below a gap). 

863 nk : Positive integer, or tuple of 2 positive integers, default 30 

864 Number of k-points along each of the two 

865 reciprocal lattice vectors spanning the plane. 

866 plane : Tuple of two distinct integers 

867 Default value (0, 1). 

868 3D lattices only: the reciprocal vectors 

869 :math:`\mathbf{b}_i, \mathbf{b}_j` spanning the plane of the 

870 Brillouin zone. The flux is oriented along the remaining 

871 primitive vector :math:`\mathbf{a}_l` (along +z in 2D). 

872 k_fixed : Real number, default 0 

873 3D lattices only: the 

874 fractional coordinate of the plane along the remaining reciprocal 

875 vector :math:`\mathbf{b}_l` (e.g. :math:`k_z` in units of 

876 :math:`|\mathbf{b}_3|` for ``plane=(0, 1)``). 

877 

878 Returns 

879 ------- 

880 curv : Real ndarray, shape (nk1, nk2) 

881 Berry curvature 

882 (flux through each plaquette, in radians). Summing *curv* and 

883 dividing by :math:`2\pi` gives the Chern number, see 

884 *chern_number*. 

885 ''' 

886 error_handling.dim_min(self.dim, 2) 

887 if isinstance(bands, int): 

888 bands = [bands] 

889 error_handling.band_indices(bands, self.norb) 

890 if isinstance(nk, int): 

891 nk = (nk, nk) 

892 error_handling.nk(nk, 2) 

893 error_handling.plane(plane, self.dim) 

894 error_handling.real_number(k_fixed, 'k_fixed') 

895 n1, n2 = nk 

896 ks = self._plane_mesh(nk, plane, k_fixed) 

897 if not self.is_hermitian(): 

898 # bands labelled by Re E are continuous only across a real line gap 

899 en = self._eigs(ks.reshape(-1, self.dim)) 

900 error_handling.line_gap(en.real, bands, 'real') 

901 error_handling.band_continuity(en.reshape(n1, n2, self.norb), bands) 

902 v1 = self._subspaces(ks.reshape(-1, self.dim), bands).reshape(n1, n2, self.norb, len(bands)) 

903 return self._flux(v1, plane) 

904 

905 def _flux(self, v1: NDArray[np.complex128], plane: tuple[int, int]) -> NDArray[np.float64]: 

906 ''' 

907 Private method. Fukui-Hatsugai-Suzuki flux through each plaquette of 

908 the mesh of *_plane_mesh*, from orthonormal bases *v1* of a subspace 

909 at its nodes (shape (n1, n2, norb, n)), oriented as in *berry_curvature*. 

910 ''' 

911 # the other corners of each plaquette: k + b_i/n1, k + b_i/n1 + b_j/n2, k + b_j/n2 

912 v2 = np.roll(v1, -1, axis=0) 

913 v3 = np.roll(v2, -1, axis=1) 

914 v4 = np.roll(v1, -1, axis=1) 

915 

916 def overlap(a, b): 

917 return np.linalg.det(a.conj().swapaxes(-1, -2) @ b) 

918 link = overlap(v1, v2) * overlap(v2, v3) * overlap(v3, v4) * overlap(v4, v1) 

919 curv = -np.angle(link) 

920 # The corners k, k+b_i/n1, k+b_i/n1+b_j/n2, k+b_j/n2 run 

921 # counterclockwise about the normal only if (b_i x b_j).normal > 0; 

922 # otherwise every flux comes out with the opposite sign. 

923 return self._plane_orientation(plane) * curv 

924 

925 def _plane_mesh( 

926 self, nk: tuple[int, int], plane: tuple[int, int], k_fixed: float, 

927 ) -> NDArray[np.float64]: 

928 ''' 

929 Private method. k-points, shape (n1, n2, dim), of the uniform mesh of 

930 the Brillouin-zone plane spanned by b_i, b_j (plane = (i, j)), at 

931 fractional coordinate k_fixed along the remaining reciprocal vector. 

932 ''' 

933 n1, n2 = nk 

934 i, j = plane 

935 rest = [l for l in range(self.dim) if l not in plane] 

936 fracs = np.zeros((n1, n2, self.dim)) 

937 fracs[:, :, i] = (np.arange(n1) / n1)[:, None] 

938 fracs[:, :, j] = (np.arange(n2) / n2)[None, :] 

939 for l in rest: 

940 fracs[:, :, l] = k_fixed 

941 return fracs @ self.rec_vec_k 

942 

943 def _plane_orientation(self, plane: tuple[int, int]) -> float: 

944 ''' 

945 Private method. +1 if (b_i, b_j) is right-handed about the plane's 

946 normal (+z in 2D; the remaining primitive vector in 3D), else -1. 

947 ''' 

948 b = [np.zeros(3) for _ in range(2)] 

949 for bb, idx in zip(b, plane): 

950 bb[:self.space_dim] = self.rec_vec[idx] 

951 if self.dim == 3: 

952 normal = np.array(self.lat.prim_vec[3 - sum(plane)], dtype='f8') 

953 else: 

954 a = [np.zeros(3), np.zeros(3)] 

955 for aa, vec in zip(a, self.lat.prim_vec): 

956 aa[:self.space_dim] = vec 

957 normal = np.cross(a[0], a[1]) 

958 if abs(normal[2]) > 1e-12: 

959 normal = np.array([0., 0., 1.]) 

960 return float(np.sign(np.dot(np.cross(b[0], b[1]), normal))) 

961 

962 def _subspace(self, k: NDArray[np.float64], bands: list[int]) -> NDArray[np.complex128]: 

963 ''' 

964 Private method. Orthonormal basis (columns) of the eigenvectors of 

965 *bands* at *k* (in the periodic gauge, orbital positions ignored). 

966 With an overlap S(k), the Lowdin-orthonormalized S^(1/2) v. 

967 ''' 

968 return self._subspaces(np.asarray(k, dtype='f8')[None], bands)[0] 

969 

970 def _subspaces(self, ks: NDArray[np.float64], bands: list[int]) -> NDArray[np.complex128]: 

971 ''' 

972 Private method. *_subspace* at the k-points *ks* (shape (nk, dim)), 

973 shape (nk, norb, len(bands)). S^(1/2) comes from the eigenvalues of 

974 the (Hermitian, positive) overlap. 

975 ''' 

976 _, vn = self._eigs(ks, eigenvec=True) 

977 if self._overlap_hop: 

978 sv, us = np.linalg.eigh(self._overlaps(ks)) 

979 vn = (us * np.sqrt(sv)[:, None, :]) @ (us.conj().transpose(0, 2, 1) @ vn) 

980 return vn[:, :, bands] 

981 

982 def biorthogonal_berry_curvature( 

983 self, bands: int | list[int], nk: int | tuple[int, int] = 30, kind: str = 'LR', 

984 gap: str = 'real', plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

985 ) -> NDArray[np.float64]: 

986 r''' 

987 Get the Berry curvature of a group of bands of a (non-Hermitian) 

988 model from its right and left eigenvectors, :math:`H|R_n\rangle = 

989 E_n|R_n\rangle` and :math:`H^\dagger|L_n\rangle = E_n^*|L_n\rangle`, 

990 normalized by :math:`\langle L_m|R_n\rangle = \delta_{mn}` (Shen, Zhen 

991 and Fu, Phys. Rev. Lett. 120, 146402 (2018)). The four connections 

992 

993 .. math:: 

994 

995 \mathbf{A}^{\alpha\beta} = i\langle u^\alpha|\nabla_{\mathbf{k}}u^\beta\rangle\, , 

996 \qquad \alpha, \beta \in \{L, R\}\, , 

997 

998 have different curvatures point by point, but their integrals, the 

999 Chern numbers :math:`C^{LR} = C^{RL} = C^{RR} = C^{LL}`, are the same 

1000 integer. The flux through each plaquette is computed as in 

1001 *berry_curvature* (Fukui-Hatsugai-Suzuki), with the links 

1002 :math:`\det\langle u^\alpha(\mathbf{k})|u^\beta(\mathbf{k}')\rangle` (for 

1003 RR and LL, the phase does not depend on the normalization). For a 

1004 Hermitian model all four are the curvature of *berry_curvature*. 

1005 

1006 The bands are ordered at every k-point by the real part of their 

1007 energy (*gap* = 'real') or by its imaginary part ('imaginary'), and 

1008 *bands* must be separated from the others by a line gap of that kind 

1009 over the whole zone -- a line :math:`\mathrm{Re}\,E = E_0` (or 

1010 :math:`\mathrm{Im}\,E = E_0`) that no band crosses. Otherwise a 

1011 ValueError is raised: the gap closes at exceptional points (see 

1012 *tbkit.exceptional.find_exceptional_points*), where the Chern number 

1013 can change. 

1014 

1015 Parameters 

1016 ---------- 

1017 bands : Positive integer, or list of positive integers 

1018 Band 

1019 indices in the order set by *gap*. 

1020 nk : Positive integer, or tuple of 2 positive integers, default 30 

1021 See *berry_curvature*. 

1022 kind : 'LR', 'RL', 'RR' or 'LL', default 'LR' 

1023 :math:`\alpha\beta` above. 

1024 gap : 'real' or 'imaginary', default 'real' 

1025 Kind of line 

1026 gap, and the order of the bands. 

1027 plane : Tuple of two integers 

1028 Default value (0, 1). 3D only, see 

1029 *berry_curvature*. 

1030 k_fixed : Real number, default 0 

1031 3D only, see *berry_curvature*. 

1032 

1033 Returns 

1034 ------- 

1035 curv : Real ndarray, shape (nk1, nk2) 

1036 Flux through each 

1037 plaquette, in radians (sign convention of *berry_curvature*). 

1038 ''' 

1039 error_handling.dim_min(self.dim, 2) 

1040 if isinstance(bands, int): 

1041 bands = [bands] 

1042 error_handling.band_indices(bands, self.norb) 

1043 if isinstance(nk, int): 

1044 nk = (nk, nk) 

1045 error_handling.nk(nk, 2) 

1046 error_handling.biorthogonal_kind(kind) 

1047 error_handling.gap_kind(gap) 

1048 error_handling.plane(plane, self.dim) 

1049 error_handling.real_number(k_fixed, 'k_fixed') 

1050 error_handling.no_overlap(self._overlap_hop) 

1051 n1, n2 = nk 

1052 ks = self._plane_mesh(nk, plane, k_fixed) 

1053 w, vr = np.linalg.eig(self._hams(ks.reshape(n1 * n2, -1))) 

1054 order = np.argsort(w.real if gap == 'real' else w.imag, axis=1, kind='stable') 

1055 en = np.take_along_axis(w, order, axis=1).reshape(n1, n2, self.norb) 

1056 vr = np.take_along_axis(vr, order[:, None, :], axis=2) 

1057 # biorthonormal: <L_m|R_n> = delta_mn, the rows of R^-1 

1058 right = vr[:, :, bands].reshape(n1, n2, self.norb, len(bands)) 

1059 left = np.linalg.inv(vr).conj().transpose(0, 2, 1)[:, :, bands].reshape(right.shape) 

1060 error_handling.line_gap(en.reshape(-1, self.norb).real if gap == 'real' 

1061 else en.reshape(-1, self.norb).imag, bands, gap) 

1062 error_handling.band_continuity(en, bands) 

1063 bra, ket = {'LR': (left, right), 'RL': (right, left), 

1064 'RR': (right, right), 'LL': (left, left)}[kind] 

1065 # one link per oriented edge, U = det<u^alpha(k)|u^beta(k + dk)>, and 

1066 # its inverse when the edge is run backwards: <L_a|R_b> and <L_b|R_a> 

1067 # are not conjugate, so using both would spoil the exact 

1068 # cancellation of the shared edges (for RR, 1/U has the phase of U*). 

1069 bra_h = bra.conj().swapaxes(-1, -2) 

1070 link = np.stack([np.linalg.det(bra_h @ np.roll(ket, -1, axis=0)), 

1071 np.linalg.det(bra_h @ np.roll(ket, -1, axis=1))]) 

1072 flux = (link[0] * np.roll(link[1], -1, axis=0) 

1073 / (np.roll(link[0], -1, axis=1) * link[1])) 

1074 return self._plane_orientation(plane) * -np.angle(flux) 

1075 

1076 def biorthogonal_chern_number( 

1077 self, bands: int | list[int], nk: int | tuple[int, int] = 30, kind: str = 'LR', 

1078 gap: str = 'real', plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

1079 ) -> float: 

1080 r''' 

1081 Get the Chern number of a line-gapped group of bands of a 

1082 (non-Hermitian) model, :math:`C^{\alpha\beta} = \frac{1}{2\pi}\int_{BZ} 

1083 \Omega^{\alpha\beta}\,d^2k`, the same integer for the four kinds 

1084 LR, RL, RR, LL (see *biorthogonal_berry_curvature*). It equals the 

1085 Hermitian Chern number of *chern_number* as long as the line gap stays 

1086 open. 

1087 

1088 Parameters 

1089 ---------- 

1090 bands 

1091 See *biorthogonal_berry_curvature*. 

1092 nk 

1093 See *biorthogonal_berry_curvature*. 

1094 kind : 'LR', 'RL', 'RR' or 'LL', default 'LR' 

1095 gap : 'real' or 'imaginary', default 'real' 

1096 plane 

1097 See *berry_curvature*. 

1098 k_fixed 

1099 See *berry_curvature*. 

1100 

1101 Returns 

1102 ------- 

1103 chern : Real number, close to an integer 

1104 ''' 

1105 return self.biorthogonal_berry_curvature(bands, nk, kind, gap, plane, k_fixed).sum() / (2*PI) 

1106 

1107 def chern_number( 

1108 self, bands: int | list[int], nk: int | tuple[int, int] = 30, 

1109 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

1110 ) -> float: 

1111 r''' 

1112 Get the Chern number of a group of bands: 

1113 

1114 .. math:: 

1115 

1116 C = \frac{1}{2\pi}\int_{BZ} \Omega(\mathbf{k})\, d^2k 

1117 

1118 an integer (up to the numerical precision set by *nk*) for a group 

1119 of bands that is isolated from the rest of the spectrum by a gap 

1120 everywhere in the Brillouin zone. See *berry_curvature*. For a 

1121 non-Hermitian model, *bands* (ordered by Re E) must be isolated by a 

1122 real line gap, or a ValueError is raised; *biorthogonal_chern_number* 

1123 also handles imaginary line gaps and the left eigenvectors. 

1124 

1125 Parameters 

1126 ---------- 

1127 bands : Positive integer, or list of positive integers 

1128 Band 

1129 index, or indices of a group of bands (e.g. all occupied bands 

1130 below a gap). 

1131 nk : Positive integer, or tuple of 2 positive integers, default 30 

1132 Number of k-points along each reciprocal 

1133 lattice vector. 

1134 plane : Tuple of two integers 

1135 Default value (0, 1). 3D lattices 

1136 only, see *berry_curvature*. 

1137 k_fixed : Real number, default 0 

1138 3D lattices only, see 

1139 *berry_curvature*: the Chern number of the plane at this 

1140 fractional coordinate (it jumps across a Weyl point). 

1141 

1142 Returns 

1143 ------- 

1144 chern : Real number, close to an integer 

1145 ''' 

1146 return self.berry_curvature(bands, nk, plane, k_fixed).sum() / (2*PI) 

1147 

1148 def sector_chern_numbers( 

1149 self, op, bands: int | list[int], nk: int | tuple[int, int] = 30, 

1150 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

1151 ) -> tuple[float, float]: 

1152 r''' 

1153 Get the Chern numbers of the two sectors into which a Hermitian 

1154 operator :math:`O` splits a group of bands: at every k-point, the 

1155 projected operator :math:`P(\mathbf{k})\,O\,P(\mathbf{k})` 

1156 (:math:`P` the projector on *bands*) is diagonalized within the 

1157 bands, and its eigenvectors with positive and negative eigenvalues 

1158 span the two sectors, whose Chern numbers :math:`C_\pm` follow as in 

1159 *berry_curvature*. They are well defined as long as the spectrum of 

1160 :math:`POP` keeps a gap around zero over the whole plane, even if 

1161 :math:`O` does not commute with :math:`H` (Prodan, Phys. Rev. B 80, 

1162 125327 (2009)); otherwise a ValueError is raised. See 

1163 *spin_chern_number* and *mirror_chern_number*. 

1164 

1165 Parameters 

1166 ---------- 

1167 op 

1168 Complex ndarray, shape (norb, norb), or callable of k 

1169 returning one: the Hermitian operator :math:`O`. 

1170 bands : Band index, or list of band indices 

1171 nk : Positive integer, or tuple of 2 positive integers, default 30 

1172 plane : Tuple of two integers 

1173 Default value (0, 1). 3D only, see *berry_curvature*. 

1174 k_fixed : Real number, default 0 

1175 3D only, see *berry_curvature*. 

1176 

1177 Returns 

1178 ------- 

1179 c_plus 

1180 Real number, close to an integer: Chern number 

1181 of the sector with :math:`POP > 0`. 

1182 c_minus : Real number: that of the sector with :math:`POP < 0` 

1183 :math:`C_+ + C_-` is the Chern number of *bands*. 

1184 ''' 

1185 bands, nk, ks = self._check_sectors(bands, nk, plane, k_fixed) 

1186 if callable(op): 

1187 ops = np.array([self._operator(op, k) for k in ks]) 

1188 else: 

1189 ops = self._operator(op, ks[0])[None] 

1190 error_handling.hermitian_operator(ops) 

1191 return self._sector_cherns(ops, bands, nk, ks, plane) 

1192 

1193 def _check_sectors( 

1194 self, bands, nk, plane, k_fixed, 

1195 ) -> tuple[list[int], tuple[int, int], NDArray[np.float64]]: 

1196 ''' 

1197 Private method. Validate the arguments of the sector Chern numbers; 

1198 return them with the k-points of the plane mesh, shape (n1 n2, dim). 

1199 ''' 

1200 error_handling.dim_min(self.dim, 2) 

1201 if isinstance(bands, int): 

1202 bands = [bands] 

1203 error_handling.band_indices(bands, self.norb) 

1204 if isinstance(nk, int): 

1205 nk = (nk, nk) 

1206 error_handling.nk(nk, 2) 

1207 error_handling.plane(plane, self.dim) 

1208 error_handling.real_number(k_fixed, 'k_fixed') 

1209 error_handling.hermitian_kspace(self.is_hermitian(), self._overlap_hop) 

1210 return bands, nk, self._plane_mesh(nk, plane, k_fixed).reshape(-1, self.dim) 

1211 

1212 def _sector_cherns( 

1213 self, ops: NDArray[np.complex128], bands: list[int], nk: tuple[int, int], 

1214 ks: NDArray[np.float64], plane: tuple[int, int], 

1215 ) -> tuple[float, float]: 

1216 ''' 

1217 Private method. *sector_chern_numbers* with the Hermitian operator 

1218 evaluated at the k-points *ks* (shape (n1 n2, norb, norb), or 

1219 (1, norb, norb) if constant). 

1220 ''' 

1221 n1, n2 = nk 

1222 v = self._subspaces(ks, bands) 

1223 w, u = np.linalg.eigh(v.conj().transpose(0, 2, 1) @ ops @ v) 

1224 n_minus = error_handling.projected_gap(w) 

1225 sectors = v @ u 

1226 c_minus = self._flux(sectors[:, :, :n_minus].reshape(n1, n2, self.norb, -1), plane) 

1227 c_plus = self._flux(sectors[:, :, n_minus:].reshape(n1, n2, self.norb, -1), plane) 

1228 return float(c_plus.sum() / (2*PI)), float(c_minus.sum() / (2*PI)) 

1229 

1230 def spin_chern_number( 

1231 self, bands: list[int], nk: int | tuple[int, int] = 30, s_z=None, 

1232 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., 

1233 ) -> float: 

1234 r''' 

1235 Get the spin Chern number of a group of bands (Prodan, Phys. Rev. B 

1236 80, 125327 (2009); Sheng, Weng, Sheng and Haldane, Phys. Rev. Lett. 

1237 97, 036808 (2006)), 

1238 

1239 .. math:: 

1240 

1241 C_s = \frac{C_+ - C_-}{2}\, , 

1242 

1243 with :math:`C_\pm` the Chern numbers of the two sectors of the 

1244 projected spin operator :math:`Ps_zP` (see *sector_chern_numbers*). 

1245 When :math:`s_z` is conserved they are the Chern numbers of the two 

1246 spins; Rashba coupling mixes the spins, but :math:`C_s` stays 

1247 quantized, and unchanged, as long as the spectrum of :math:`Ps_zP` 

1248 keeps a gap around zero. With time reversal, :math:`C_s` modulo 2 is 

1249 the :math:`\mathbb{Z}_2` invariant of *z2_invariant*. 

1250 

1251 Parameters 

1252 ---------- 

1253 bands : List of band indices (e.g. the occupied bands) 

1254 nk : Positive integer, or tuple of 2 positive integers, default 30 

1255 s_z : Complex ndarray, shape (norb, norb), or callable of k 

1256 Default value None: :math:`\sigma_z` on every site of a model 

1257 built with ``spin=True``. The spin operator. 

1258 plane : Tuple of two integers 

1259 Default value (0, 1). 3D only, see *berry_curvature*. 

1260 k_fixed : Real number, default 0 

1261 3D only, see *berry_curvature*. 

1262 

1263 Returns 

1264 ------- 

1265 c_s : Real number, close to an integer 

1266 ''' 

1267 if s_z is None: 

1268 error_handling.spinful(self.spin) 

1269 s_z = np.kron(np.eye(self.norb // 2), PAULI['z']) 

1270 c_plus, c_minus = self.sector_chern_numbers(s_z, bands, nk, plane, k_fixed) 

1271 return (c_plus - c_minus) / 2 

1272 

1273 def mirror_chern_number( 

1274 self, mirror, bands: list[int], nk: int | tuple[int, int] = 30, 

1275 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., tol: float = 1e-8, 

1276 ) -> float: 

1277 r''' 

1278 Get the mirror Chern number of a group of bands on a mirror-invariant 

1279 plane of the Brillouin zone (Teo, Fu and Kane, Phys. Rev. B 78, 

1280 045426 (2008); Hsieh et al., Nat. Commun. 3, 982 (2012)), 

1281 

1282 .. math:: 

1283 

1284 n_M = \frac{C_{+} - C_{-}}{2}\, , 

1285 

1286 with :math:`C_\pm` the Chern numbers of the bands of mirror 

1287 eigenvalue :math:`+i` and :math:`-i` (:math:`M^2 = -1`, spinful 

1288 electrons) or :math:`+1` and :math:`-1` (:math:`M^2 = +1`). The 

1289 mirror must commute with :math:`H(\mathbf{k})` at every point of the 

1290 plane: in 3D, the plane fixed by the reflection (e.g. 

1291 :math:`k_z = 0` or :math:`\pi` for :math:`z\to-z`); in 2D, the whole 

1292 zone, for a reflection of the third axis. A topological crystalline 

1293 insulator such as SnTe has :math:`n_M = 2` and every 

1294 :math:`\mathbb{Z}_2` index zero (:math:`\nu = n_M` modulo 2 on the plane). 

1295 

1296 Parameters 

1297 ---------- 

1298 mirror 

1299 Complex ndarray, shape (norb, norb), or callable of k: 

1300 the unitary mirror operator :math:`M`, with :math:`M^2 = \pm1`. 

1301 bands : List of band indices (e.g. the occupied bands) 

1302 nk : Positive integer, or tuple of 2 positive integers, default 30 

1303 plane : Tuple of two integers 

1304 Default value (0, 1). 3D only, see *berry_curvature*. 

1305 k_fixed : Real number, default 0 

1306 3D only: the 

1307 mirror-invariant plane (0 or 0.5 for a reflection of the remaining axis). 

1308 tol : Positive real, default 1e-8 

1309 Tolerance of the 

1310 checks :math:`M^2 = \pm1` and :math:`[M, H(\mathbf{k})] = 0`. 

1311 

1312 Returns 

1313 ------- 

1314 n_m : Real number, close to an integer 

1315 ''' 

1316 bands, nk, ks = self._check_sectors(bands, nk, plane, k_fixed) 

1317 error_handling.positive_real(tol, 'tol') 

1318 if callable(mirror): 

1319 mats = np.array([self._operator(mirror, k) for k in ks]) 

1320 else: 

1321 mats = self._operator(mirror, ks[0])[None] 

1322 sign = error_handling.mirror_square(mats @ mats, tol) 

1323 hams = self._hams(ks) 

1324 error_handling.commutes(float(np.max(np.abs(mats @ hams - hams @ mats))), tol) 

1325 # Hermitian, with eigenvalue +1 on the sector M = +i (M = +1 if M^2 = 1) 

1326 herm = mats if sign > 0 else -1j * mats 

1327 c_plus, c_minus = self._sector_cherns(herm, bands, nk, ks, plane) 

1328 return (c_plus - c_minus) / 2 

1329 

1330 # ------------------------------------------------------------------ 

1331 # Anomalous and spin Hall conductivities (Kubo formula) 

1332 # ------------------------------------------------------------------ 

1333 

1334 def _check_static(self) -> None: 

1335 ''' 

1336 Private method. Methods that differentiate the stored hoppings call 

1337 it first (a Floquet model overrides it to refuse). 

1338 ''' 

1339 

1340 def _bloch_sum( 

1341 self, hops: list, k_cart: NDArray[np.float64], directions: Sequence[NDArray[np.float64]] = (), 

1342 positions: bool = False, A: NDArray[np.float64] | None = None, 

1343 ) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]: 

1344 r''' 

1345 Private method. The Bloch sum :math:`\sum t\,e^{i\mathbf{k}\cdot\mathbf{d}}` 

1346 of a hopping list over many k-points at once (*k_cart*, shape 

1347 (nk, space_dim)), and its derivatives 

1348 :math:`\sum i(\mathbf{u}\cdot\mathbf{d})\,t\,e^{i\mathbf{k}\cdot\mathbf{d}}` 

1349 along the Cartesian unit vectors *directions*, with 

1350 :math:`\mathbf{d} = \mathbf{R}` (plus :math:`\boldsymbol\tau_j-\boldsymbol\tau_i` 

1351 with *positions*). A vector potential *A* multiplies each term by the 

1352 Peierls phase :math:`e^{i\mathbf{A}\cdot(\mathbf{R}+\boldsymbol\tau_j-\boldsymbol\tau_i)}` 

1353 (see *get_ham_peierls*). Shapes (nk, norb, norb) and 

1354 (len(directions), nk, norb, norb). Every Bloch matrix of the model 

1355 (*get_ham*, *get_overlap*, *get_ham_peierls*, *_bloch_derivatives*) 

1356 is built here. 

1357 

1358 When the hoppings fill at least *group_fill* of the blocks 

1359 :math:`T_{\mathbf{R}}` (one norb x norb matrix per distinct 

1360 :math:`\mathbf{R}`), the sum is one matrix product 

1361 :math:`\sum_{\mathbf{R}} e^{i\mathbf{k}\cdot\mathbf{R}}\,T_{\mathbf{R}}`, 

1362 with the orbital positions as the phases 

1363 :math:`e^{-i\mathbf{k}\cdot\boldsymbol\tau_i}` and 

1364 :math:`e^{i\mathbf{k}\cdot\boldsymbol\tau_j}` of its rows and 

1365 columns. Otherwise, each hopping is summed on its own. 

1366 ''' 

1367 n, nk = self.norb, len(k_cart) 

1368 i = np.array([h[0] for h in hops], dtype=int) 

1369 j = np.array([h[1] for h in hops], dtype=int) 

1370 d = np.array([h[2] for h in hops], dtype='f8').reshape(len(hops), self.space_dim) 

1371 t = np.array([h[3] for h in hops], dtype='c16') 

1372 if positions or A is not None: 

1373 tau = self.orbital_positions() 

1374 bond = d + tau[j] - tau[i] 

1375 if A is not None: 

1376 # the Peierls phase of a bond does not depend on k 

1377 t = t * np.exp(1j * (bond @ A)) 

1378 if len(hops): 

1379 cells, cell = np.unique(d, axis=0, return_inverse=True) 

1380 if len(hops) >= self.group_fill * len(cells) * n * n: 

1381 return self._bloch_grouped(i, j, t, cells, cell.ravel(), k_cart, directions, 

1382 tau if positions else None) 

1383 if positions: 

1384 d = bond 

1385 w = np.exp(1j * (k_cart @ d.T)) * t[None, :] 

1386 # accumulate term m of k-point p in entry (p, i_m, j_m): repeated 

1387 # (i, j) pairs add up, as they must 

1388 index = (np.arange(nk)[:, None] * n * n + i * n + j).ravel() 

1389 

1390 def scatter(v): 

1391 size = nk * n * n 

1392 return (np.bincount(index, v.real.ravel(), size) 

1393 + 1j * np.bincount(index, v.imag.ravel(), size)).reshape(nk, n, n) 

1394 mat = scatter(w) 

1395 dmat = np.array([scatter(w * (1j * (d @ u))[None, :]) for u in directions], 

1396 dtype='c16').reshape(len(directions), nk, n, n) 

1397 return mat, dmat 

1398 

1399 def _bloch_grouped( 

1400 self, i: NDArray[np.int_], j: NDArray[np.int_], t: NDArray[np.complex128], 

1401 cells: NDArray[np.float64], cell: NDArray[np.int_], k_cart: NDArray[np.float64], 

1402 directions: Sequence[NDArray[np.float64]], tau: NDArray[np.float64] | None, 

1403 ) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]: 

1404 r''' 

1405 Private method. *_bloch_sum* as one matrix product over the distinct 

1406 lattice vectors *cells* (hopping m connects to cells[cell[m]]), with 

1407 the orbital positions *tau* in the phases (None: periodic gauge). 

1408 The derivative along :math:`\mathbf{u}` weights each block by 

1409 :math:`i\mathbf{u}\cdot\mathbf{R}`, plus 

1410 :math:`i\mathbf{u}\cdot(\boldsymbol\tau_j-\boldsymbol\tau_i)` with *tau*. 

1411 ''' 

1412 n, nk = self.norb, len(k_cart) 

1413 blocks = np.zeros((len(cells), n * n), dtype='c16') 

1414 np.add.at(blocks, (cell, i * n + j), t) 

1415 phases = np.exp(1j * (k_cart @ cells.T)) 

1416 mat = (phases @ blocks).reshape(nk, n, n) 

1417 dmat = np.empty((len(directions), nk, n, n), dtype='c16') 

1418 for a, u in enumerate(directions): 

1419 dmat[a] = ((phases * (1j * (cells @ u))[None, :]) @ blocks).reshape(nk, n, n) 

1420 if tau is not None: 

1421 x = tau @ u 

1422 dmat[a] += 1j * (x[None, :] - x[:, None])[None] * mat 

1423 if tau is not None: 

1424 orb = np.exp(1j * (k_cart @ tau.T)) 

1425 row, col = orb.conj()[:, :, None], orb[:, None, :] 

1426 mat = row * mat * col 

1427 dmat = row[None] * dmat * col[None] 

1428 return mat, dmat 

1429 

1430 def _bloch_derivatives( 

1431 self, ks: NDArray[np.float64], directions: list[NDArray[np.float64]], positions: bool, 

1432 params: dict | None = None, 

1433 ) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]: 

1434 r''' 

1435 Private method. :math:`H(\mathbf{k})` and its exact derivatives 

1436 :math:`\partial H/\partial k_u` along the Cartesian unit vectors 

1437 *directions*, at the k-points *ks* (shape (nk, dim), coordinates of 

1438 *get_ham*). With an overlap, the Lowdin-orthogonalized 

1439 :math:`H' = S^{-1/2}HS^{-1/2}` (the eigenvectors :math:`S^{1/2}v` of 

1440 the topology tools) and its derivative, with 

1441 :math:`\partial S^{-1/2}` from the Daleckii-Krein formula in the 

1442 eigenbasis :math:`S = \sum_a s_a|a\rangle\langle a|`: 

1443 :math:`\langle a|\partial S^{-1/2}|b\rangle = \langle a|\partial S|b\rangle 

1444 (s_a^{-1/2}-s_b^{-1/2})/(s_a-s_b)`, :math:`-s_a^{-3/2}/2` when :math:`s_a = s_b`. 

1445 The value functions take *params* (default: *self.params*). 

1446 ''' 

1447 k_cart = ks @ self.k_basis.T 

1448 hops = self._hop if params is None else self._hops(params) 

1449 ham, dham = self._bloch_sum(hops, k_cart, directions, positions) 

1450 ham = ham + (np.diag(self.onsite) + self._onsite_offdiag)[None] 

1451 if not self._overlap_hop: 

1452 return ham, dham 

1453 s, ds = self._bloch_sum(self._overlap_hop, k_cart, directions, positions) 

1454 s = s + np.eye(self.norb)[None] 

1455 sv, vs = np.linalg.eigh(s) 

1456 error_handling.positive_overlap(sv.min()) 

1457 r = sv ** -0.5 

1458 diff = sv[:, :, None] - sv[:, None, :] 

1459 close = np.abs(diff) < 1e-10 

1460 lk = np.where(close, -0.5 * sv[:, :, None] ** -1.5, 

1461 (r[:, :, None] - r[:, None, :]) / np.where(close, 1., diff)) 

1462 vh = vs.conj().transpose(0, 2, 1) 

1463 s_inv = (vs * r[:, None, :]) @ vh 

1464 ds_inv = vs @ ((vh @ ds @ vs) * lk) @ vh 

1465 dham = ds_inv @ ham @ s_inv + s_inv @ dham @ s_inv + s_inv @ ham @ ds_inv 

1466 return s_inv @ ham @ s_inv, dham 

1467 

1468 def _kubo( 

1469 self, ks: NDArray[np.float64], e_fermi: NDArray[np.float64], temperature: float, 

1470 directions: list[NDArray[np.float64]], pairs: list[tuple[int, int]], positions: bool, 

1471 spin_op: NDArray[np.complex128] | None, response: str = 'hall', 

1472 ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

1473 r''' 

1474 Private method. The Kubo curvature 

1475 :math:`F(\mathbf{k}) = \sum_{n\neq m}P_{nm}\,\mathrm{Im}[X_{nm}Y_{mn}]/(E_n-E_m)^2` 

1476 at each k-point, Fermi energy and pair (X, Y) of velocities (X the 

1477 spin current :math:`\{s, v\}/2` if *spin_op*), with the weights 

1478 :math:`P_{nm}` of *response* (see *_response_weights*), e.g. 

1479 :math:`P_{nm} = -(f_n-f_m)` for the Hall conductivity 

1480 (:math:`F = \sum_n f_n\Omega_n`). Shape (nk, len(e_fermi), len(pairs)). 

1481 Pairs of degenerate states (equal weights) are skipped, so :math:`F` 

1482 stays finite at band crossings below or above the Fermi level. Also 

1483 returns, per k-point, the occupation-independent size of the 

1484 curvature, :math:`\sum_{n\neq m}|\mathrm{Im}[X_{nm}Y_{mn}]|/(E_n-E_m)^2`, 

1485 which locates the hot spots of the adaptive refinement. 

1486 ''' 

1487 out = np.zeros((len(ks), len(e_fermi), len(pairs))) 

1488 hot = np.zeros(len(ks)) 

1489 chunk = max(1, 400000 // self.norb ** 2) 

1490 for c0 in range(0, len(ks), chunk): 

1491 ham, dham = self._bloch_derivatives(ks[c0:c0 + chunk], directions, positions) 

1492 en, vec = np.linalg.eigh(ham) 

1493 vh = vec.conj().transpose(0, 2, 1) 

1494 vel = [vh @ dh @ vec for dh in dham] 

1495 de = en[:, :, None] - en[:, None, :] 

1496 degenerate = np.abs(de) < 1e-9 

1497 inv2 = np.where(degenerate, 0., 1. / np.where(degenerate, 1., de) ** 2) 

1498 if spin_op is not None: 

1499 s_e = vh @ spin_op @ vec 

1500 for p, (a, b) in enumerate(pairs): 

1501 x = vel[a] if spin_op is None else 0.5 * (s_e @ vel[a] + vel[a] @ s_e) 

1502 w = np.imag(x * vel[b].transpose(0, 2, 1)) * inv2 

1503 hot[c0:c0 + chunk] += np.abs(w).sum(axis=(1, 2)) 

1504 for e, mu in enumerate(e_fermi): 

1505 weights = _response_weights(en, float(mu), temperature, response) 

1506 out[c0:c0 + chunk, e, p] = np.einsum('knm,knm->k', weights, w) 

1507 return out, hot 

1508 

1509 def _hall( 

1510 self, e_fermi, temperature, nk, plane, k_fixed, positions, refine, refine_fraction, 

1511 spin_axis, response='hall', 

1512 ): 

1513 ''' 

1514 Private method. Validate, set the geometry and mesh, integrate the 

1515 Kubo curvature (with the adaptive refinement), for 

1516 *hall_conductivity* (spin_axis None) and *spin_hall_conductivity*, 

1517 and, with the weights of *response*, for *orbital_magnetization*, 

1518 *anomalous_nernst_conductivity* and *thermal_hall_conductivity*. 

1519 ''' 

1520 self._check_static() 

1521 error_handling.dim_min(self.dim, 2) 

1522 error_handling.hermitian_model(self.is_hermitian()) 

1523 error_handling.fermi_energies(e_fermi) 

1524 if response in ('nernst', 'thermal'): 

1525 error_handling.positive_real(temperature, 'temperature') 

1526 else: 

1527 error_handling.positive_real_zero(temperature, 'temperature') 

1528 error_handling.plane(plane, self.dim) 

1529 error_handling.k_fixed_hall(k_fixed, self.dim, spin_axis is None) 

1530 error_handling.boolean(positions, 'positions') 

1531 error_handling.positive_int(refine, 'refine') 

1532 error_handling.refine_fraction(refine_fraction) 

1533 full = self.dim == 3 and k_fixed is None 

1534 axes = [0, 1, 2] if full else list(plane) 

1535 if isinstance(nk, int): 

1536 nk = (nk,) * len(axes) 

1537 error_handling.nk(nk, len(axes)) 

1538 spin_op = None 

1539 if spin_axis is not None: 

1540 error_handling.spinful(self.spin) 

1541 error_handling.spin_axis(spin_axis) 

1542 spin_op = np.kron(np.eye(self.n_sites), PAULI[spin_axis] / 2.) 

1543 # Directions (Cartesian unit vectors) of the velocities, the pairs 

1544 # (u, w) whose curvature Omega_uw = Omega.(u x w) is integrated, and 

1545 # the prefactor turning the mesh average into the conductivity. 

1546 if full: 

1547 directions = list(np.eye(3)) 

1548 pairs = [(1, 2), (2, 0), (0, 1)] 

1549 volume = abs(np.linalg.det(np.array(self.lat.prim_vec, dtype='f8'))) 

1550 prefactor = 2 * PI / volume 

1551 elif self.dim == 3: 

1552 i, j = plane 

1553 a_l = np.array(self.lat.prim_vec[3 - i - j], dtype='f8') 

1554 b_i, b_j = (np.array(self.rec_vec[p], dtype='f8') for p in plane) 

1555 u = b_i / np.linalg.norm(b_i) 

1556 directions = [u, np.cross(a_l / np.linalg.norm(a_l), u)] 

1557 pairs = [(0, 1)] 

1558 prefactor = np.linalg.norm(np.cross(b_i, b_j)) / (2 * PI) 

1559 else: 

1560 directions = [self.k_basis[:, 0], self.k_basis[:, 1]] 

1561 pairs = [(0, 1)] 

1562 det = np.linalg.det(self.rec_vec_k) 

1563 prefactor = self._plane_orientation((0, 1)) * np.sign(det) * abs(det) / (2 * PI) 

1564 e_arr = np.atleast_1d(np.asarray(e_fermi, dtype='f8')).ravel() 

1565 grids = np.meshgrid(*[np.arange(n) / n for n in nk], indexing='ij') 

1566 fracs = np.zeros((grids[0].size, self.dim)) 

1567 for ax, g in zip(axes, grids): 

1568 fracs[:, ax] = g.ravel() 

1569 for ax in range(self.dim): 

1570 if ax not in axes: 

1571 fracs[:, ax] = k_fixed 

1572 args = (e_arr, temperature, directions, pairs, positions, spin_op, response) 

1573 coarse, size = self._kubo(fracs @ self.rec_vec_k, *args) 

1574 total = coarse.sum(axis=0) 

1575 if refine > 1: 

1576 # Wang, Yates, Souza and Vanderbilt (2006): resample the cells 

1577 # where the curvature peaks on a refine^d submesh (midpoints of 

1578 # the sub-cells), and replace their coarse value by the average. 

1579 # The cells are ranked by the size of the curvature whatever the 

1580 # occupations, so that cells straddling the Fermi surface are 

1581 # refined together with their neighbours. 

1582 n_hot = int(np.ceil(refine_fraction * len(fracs))) 

1583 hot = np.argsort(size)[::-1][:n_hot] 

1584 sub = np.array(np.meshgrid(*[(np.arange(refine) + 0.5) / refine - 0.5] * len(axes), 

1585 indexing='ij')).reshape(len(axes), -1).T 

1586 offsets = np.zeros((len(sub), self.dim)) 

1587 for c, (ax, n) in enumerate(zip(axes, nk)): 

1588 offsets[:, ax] = sub[:, c] / n 

1589 fine = (fracs[hot][:, None, :] + offsets[None, :, :]).reshape(-1, self.dim) 

1590 fine_val = self._kubo(fine @ self.rec_vec_k, *args)[0].reshape(n_hot, len(sub), len(e_arr), len(pairs)) 

1591 total = total + (fine_val.mean(axis=1) - coarse[hot]).sum(axis=0) 

1592 sigma = prefactor * total / len(fracs) 

1593 if not full: 

1594 sigma = sigma[:, 0] 

1595 if np.ndim(e_fermi) == 0: 

1596 return sigma[0] if full else float(sigma[0]) 

1597 return sigma.reshape(np.shape(e_fermi) + sigma.shape[1:]) 

1598 

1599 def hall_conductivity( 

1600 self, e_fermi: float | ArrayLike, temperature: float = 0., nk: int | tuple[int, ...] = 60, 

1601 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1602 refine: int = 1, refine_fraction: float = 0.05, 

1603 ) -> float | NDArray[np.float64]: 

1604 r''' 

1605 Get the intrinsic anomalous Hall conductivity at any Fermi level -- 

1606 in a gap, in a band, or at a band crossing -- from the Kubo formula 

1607 over a uniform Brillouin-zone mesh, 

1608 

1609 .. math:: 

1610 

1611 \sigma_{xy} = \frac{e^2}{\hbar}\int_{BZ}\frac{d^dk}{(2\pi)^d}\, 

1612 F(\mathbf{k})\, ,\qquad 

1613 F = -\sum_{n\neq m}(f_n - f_m)\, 

1614 \frac{\mathrm{Im}\left[\langle n|\partial_x H|m\rangle\langle m|\partial_y H|n\rangle\right]} 

1615 {(E_n - E_m)^2} = \sum_n f_n\,\Omega_n\, , 

1616 

1617 with :math:`f_n = f(E_n)` the Fermi-Dirac occupations and 

1618 

1619 .. math:: 

1620 

1621 \Omega_n = -2\,\mathrm{Im}\sum_{m\neq n} 

1622 \frac{\langle n|\partial_x H|m\rangle\langle m|\partial_y H|n\rangle}{(E_n-E_m)^2} 

1623 = -2\,\mathrm{Im}\langle\partial_x u_n|\partial_y u_n\rangle 

1624 

1625 the Berry curvature of band *n* (:math:`\partial_\mu H = \partial H/\partial k_\mu`, 

1626 the convention :math:`\Omega = \partial_xA_y - \partial_yA_x`, 

1627 :math:`\mathbf{A} = i\langle u|\nabla_{\mathbf{k}}u\rangle` of 

1628 *berry_curvature*). The form with :math:`f_n - f_m` stays finite 

1629 where bands touch: degenerate pairs have equal occupations and 

1630 drop out, instead of cancelling between two diverging 

1631 :math:`\Omega_n`. :math:`\partial H/\partial\mathbf{k}` is built 

1632 analytically from the stored hoppings, :math:`\sum_{\mathbf{R}} 

1633 i\mathbf{d}\,t\,e^{i\mathbf{k}\cdot\mathbf{d}}` with the Cartesian bond 

1634 vector :math:`\mathbf{d}`. 

1635 

1636 **Sign.** In a gap, :math:`\sigma_{xy} = C\,e^2/h` with :math:`C` 

1637 the value *chern_number* returns for the occupied bands: this is the 

1638 sign of TKNN (Phys. Rev. Lett. 49, 405 (1982)), whose 

1639 :math:`\sigma_H = (e^2/h)\,\frac{i}{2\pi}\int d^2k\,(\langle\partial_1u|\partial_2u\rangle 

1640 - \langle\partial_2u|\partial_1u\rangle)` has the same integrand, 

1641 :math:`\Omega/2\pi`. The adiabatic (semiclassical) response of a 

1642 charge :math:`q` of either sign, :math:`\dot{\mathbf{r}} = \partial_{\mathbf{k}}E 

1643 - \dot{\mathbf{k}}\times\boldsymbol\Omega` with 

1644 :math:`\hbar\dot{\mathbf{k}} = q\mathbf{E}`, gives the opposite sign 

1645 for the tensor of Ohm's law :math:`j_x = \sigma^{\mathrm{Ohm}}_{xy}E_y`: 

1646 :math:`\sigma^{\mathrm{Ohm}}_{xy} = -(q^2/\hbar)\int F\,d^dk/(2\pi)^d`, 

1647 the convention of Wang, Yates, Souza and Vanderbilt (Phys. Rev. B 

1648 74, 195118 (2006)) and of the Kubo-Bastin formula (*tbkit.kpm.hall_conductivity* 

1649 returns, likewise, minus it). So the value returned is 

1650 :math:`\sigma^{\mathrm{Ohm}}_{yx} = -\sigma^{\mathrm{Ohm}}_{xy}`; e.g. a 

1651 massive Dirac cone :math:`v(k_x\sigma_x + k_y\sigma_y) + m\sigma_z` 

1652 gives :math:`+\frac{e^2}{2h}\frac{m}{|E_F|}` outside its gap (the 

1653 literature's :math:`\sigma_{xy} = -\frac{e^2}{2h}\frac{m}{|E_F|}`), and 

1654 :math:`\frac{e^2}{2h}\mathrm{sgn}(m)` inside. 

1655 

1656 **Orbital positions.** The velocity is :math:`\partial H/\partial\mathbf{k}` 

1657 in the gauge of *positions*: with ``positions=True`` (the default) 

1658 the bond vector is :math:`\mathbf{d} = \mathbf{R} + \boldsymbol\tau_j 

1659 - \boldsymbol\tau_i`, the matrix of the physical velocity 

1660 :math:`i[H, \mathbf{r}]` of a tight-binding model whose position 

1661 operator is diagonal on its orbitals (the one *kpm.hall_conductivity* 

1662 uses on a torus). With ``positions=False``, every orbital sits on 

1663 its cell origin (the periodic gauge of *get_ham*). The two differ by 

1664 :math:`i(E_n-E_m)\langle n|\boldsymbol\tau|m\rangle` in the 

1665 velocity's matrix elements, so the band curvatures differ by 

1666 :math:`\nabla\times\langle n|\boldsymbol\tau|n\rangle`, a total 

1667 derivative: the conductivities agree whenever the bands below 

1668 :math:`E_F` are all full (in a gap, at any temperature well below 

1669 the gap) and differ in general at partial filling, by the circulation 

1670 :math:`\frac{1}{2\pi}\oint_{FS}\langle n|\boldsymbol\tau|n\rangle\cdot d\mathbf{k}` 

1671 around the Fermi surface. The default is the physical one. 

1672 

1673 **Units and dimensions.** In 2D, :math:`\sigma` is in units of 

1674 :math:`e^2/h` (:math:`\frac{1}{2\pi}\int d^2k\,F`). In 3D, with 

1675 *k_fixed* given, the value of the plane (the conductance of that 

1676 slice of the Brillouin zone, in :math:`e^2/h`, oriented like 

1677 *chern_number* along :math:`\mathbf{a}_l`); with ``k_fixed=None``, 

1678 the full 3D conductivity as the Hall (pseudo)vector 

1679 :math:`(\sigma_{yz}, \sigma_{zx}, \sigma_{xy}) = \frac{1}{(2\pi)^2}\int d^3k\,\mathbf{F}` 

1680 in units of :math:`e^2/(h\cdot\mathrm{length})` (length the unit of 

1681 *prim_vec*): :math:`\frac{1}{2\pi}\sum_l\bar C_l\,\mathbf{b}_l`, e.g. 

1682 :math:`C/c` along :math:`z` for a stack of Chern layers :math:`c` apart. 

1683 

1684 **Convergence.** The integrand peaks where bands nearly touch close 

1685 to :math:`E_F` (hot spots), and jumps across the Fermi surface. A 

1686 *temperature* smooths the latter. *refine* resamples the fraction 

1687 *refine_fraction* of mesh cells with the largest :math:`|F|` on a 

1688 finer submesh (*refine* points per direction), in the spirit of the 

1689 adaptive mesh refinement of Wang, Yates, Souza and Vanderbilt 

1690 (2006); the cells are ranked by the size of the curvature whatever 

1691 the occupations. It is meant for metals: in a gap the plain uniform 

1692 mesh already converges exponentially, and refining part of it 

1693 costs a little accuracy (about :math:`4\cdot10^{-4}` for the 

1694 Haldane model on a 60 x 60 mesh). With an overlap (*set_overlap*), :math:`H` is first 

1695 Lowdin-orthogonalized, :math:`S^{-1/2}HS^{-1/2}`, as by the topology 

1696 tools (exact derivatives of :math:`S^{-1/2}` included). 

1697 

1698 Parameters 

1699 ---------- 

1700 e_fermi : Real number, or array of real numbers 

1701 Fermi energies 

1702 (all of them are computed from one diagonalization of the mesh). 

1703 temperature : Positive real or zero, default 0 

1704 In 

1705 energy units (:math:`k_B = 1`). 

1706 nk 

1707 Positive integer, or tuple (two integers for a plane, 

1708 three for the full 3D conductivity). Default value 60. k-mesh. 

1709 plane : Tuple of two integers 

1710 Default value (0, 1). 3D only: 

1711 the reciprocal vectors spanning the plane (see *berry_curvature*). 

1712 k_fixed : Real number or None, default None 

1713 3D only: 

1714 the fractional coordinate of the plane along the remaining 

1715 reciprocal vector; None for the full 3D conductivity. Ignored in 2D. 

1716 positions : Boolean, default True 

1717 Include the orbital 

1718 positions in the bond vectors (see above). 

1719 refine : Positive integer 

1720 Default value 1 (no refinement). 

1721 Submesh points per direction in the refined cells. 

1722 refine_fraction : Real in (0, 1], default 0.05 

1723 Fraction 

1724 of the mesh cells that are refined. 

1725 

1726 Returns 

1727 ------- 

1728 sigma 

1729 Real number (or ndarray shaped like *e_fermi*); in 

1730 3D with ``k_fixed=None``, ndarray of shape (3,) (or 

1731 ``e_fermi.shape + (3,)``). 

1732 

1733 Examples 

1734 -------- 

1735 Example usage:: 

1736 

1737 e_f = np.linspace(-3., 3., 301) 

1738 sigma = hal.hall_conductivity(e_f, nk=120) # plateau at C in the gap 

1739 ''' 

1740 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1741 refine_fraction, None) 

1742 

1743 def spin_hall_conductivity( 

1744 self, e_fermi: float | ArrayLike, temperature: float = 0., nk: int | tuple[int, int] = 60, 

1745 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1746 spin_axis: str = 'z', refine: int = 1, refine_fraction: float = 0.05, 

1747 ) -> float | NDArray[np.float64]: 

1748 r''' 

1749 Get the intrinsic spin Hall conductivity of a spinful model 

1750 (``spin=True``): the Kubo formula of *hall_conductivity* with the 

1751 charge current along :math:`x` replaced by the spin current 

1752 (Sinova et al., Phys. Rev. Lett. 92, 126603 (2004)) 

1753 

1754 .. math:: 

1755 

1756 j^{s}_x = \tfrac12\{s, v_x\}\, ,\qquad s = \tfrac{\hbar}{2}\sigma_{a}\, , 

1757 

1758 i.e. :math:`\sigma^{s}_{xy} = \frac{e}{2\pi}\cdot\frac{1}{2\pi}\int d^2k\,F^s`, 

1759 :math:`F^s = -\sum_{n\neq m}(f_n-f_m)\,\mathrm{Im}[\langle n|j^s_x|m\rangle\langle m|\partial_yH|n\rangle]/(E_n-E_m)^2` 

1760 (:math:`j^s_x` with :math:`s = \sigma_a/2`, in units of :math:`\hbar`). 

1761 The unit is :math:`e/2\pi`: one :math:`e/\hbar` from the electric 

1762 field, one :math:`\hbar/2` from the spin, and :math:`1/2\pi` from the 

1763 Brillouin-zone integral, as for :math:`e^2/h = (e^2/\hbar)/2\pi`. 

1764 When :math:`s_z` is conserved, the two spins decouple and 

1765 :math:`j^s_x = \pm\frac{\hbar}{2}v_x` in each, so in a gap 

1766 :math:`\sigma^s_{xy} = \frac{e}{2\pi}\cdot\frac{C_\uparrow - C_\downarrow}{2}`, 

1767 :math:`C_\sigma` the Chern number of each spin sector (the sign of 

1768 *hall_conductivity* and *chern_number*): :math:`\pm e/2\pi` for the 

1769 Kane-Mele quantum spin Hall insulator (Kane and Mele 2005). Rashba 

1770 coupling breaks the conservation of :math:`s_z`, and the value is 

1771 then no longer quantized (the :math:`\mathbb{Z}_2` invariant, see 

1772 *z2_invariant*, still is). 

1773 

1774 Parameters 

1775 ---------- 

1776 e_fermi 

1777 See *hall_conductivity*. 

1778 temperature 

1779 See *hall_conductivity*. 

1780 nk : Positive integer, or tuple of 2 integers, default 60 

1781 plane 

1782 See *hall_conductivity*. 

1783 k_fixed : Real number or None, default None 

1784 3D only: the 

1785 plane (required in 3D, where only planes are computed). 

1786 positions : Boolean, default True 

1787 See *hall_conductivity*. 

1788 spin_axis : 'x', 'y' or 'z', default 'z' 

1789 Spin component 

1790 :math:`a` carried by the current. 

1791 refine 

1792 See *hall_conductivity*. 

1793 refine_fraction 

1794 See *hall_conductivity*. 

1795 

1796 Returns 

1797 ------- 

1798 sigma_s 

1799 Real number (or ndarray shaped like *e_fermi*), in 

1800 units of :math:`e/2\pi`. 

1801 ''' 

1802 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1803 refine_fraction, spin_axis) 

1804 

1805 # ------------------------------------------------------------------ 

1806 # Berry-phase response beyond the Hall conductivity 

1807 # ------------------------------------------------------------------ 

1808 

1809 def orbital_magnetization( 

1810 self, e_fermi: float | ArrayLike, temperature: float = 0., nk: int | tuple[int, ...] = 60, 

1811 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1812 refine: int = 1, refine_fraction: float = 0.05, 

1813 ) -> float | NDArray[np.float64]: 

1814 r''' 

1815 Get the orbital magnetization in the modern theory (Thonhauser, 

1816 Ceresoli, Vanderbilt and Resta, Phys. Rev. Lett. 95, 137205 (2005); 

1817 Xiao, Shi and Niu, Phys. Rev. Lett. 95, 137204 (2005); Ceresoli et 

1818 al., Phys. Rev. B 74, 024408 (2006)), at any Fermi level and 

1819 temperature: 

1820 

1821 .. math:: 

1822 

1823 M = \frac{e}{h}\,\frac{1}{2\pi}\int_{BZ} d^2k\sum_n\left[f_n\,m_n 

1824 + g_n\,\Omega_n\right],\qquad 

1825 m_n = \mathrm{Im}\langle\partial_xu_n|(H-E_n)|\partial_yu_n\rangle\, , 

1826 

1827 with :math:`g = k_BT\ln(1+e^{-(E-\mu)/k_BT})` (:math:`\mu - E` below 

1828 :math:`\mu` and 0 above, at :math:`T = 0`). The first term is the 

1829 self-rotation of the wavepackets, the second the circulation of 

1830 the Berry-curvature currents at the edges. At :math:`T = 0` both 

1831 combine into :math:`\sum_nf_n\,\mathrm{Im}\langle\partial_xu_n|(H+E_n-2\mu)|\partial_yu_n\rangle`, 

1832 the formula of Thonhauser et al. It is evaluated, like 

1833 *hall_conductivity*, from :math:`\langle n|\partial H|m\rangle` 

1834 (no derivatives of the eigenstates), so it is finite where bands 

1835 touch. 

1836 

1837 **Sign and units.** The sign is that of *hall_conductivity*, so 

1838 that the Streda formula reads :math:`\partial M/\partial\mu = \sigma`: 

1839 in a gap (:math:`T = 0`), :math:`M` is linear in :math:`\mu` with 

1840 slope the Chern number :math:`C`. In 2D, :math:`M` (a magnetic 

1841 moment per area, i.e. a current) is in units of :math:`e/h` times 

1842 the energy unit; with ``k_fixed=None`` in 3D, the vector 

1843 :math:`(M_x, M_y, M_z)` per length. *positions* matters as for 

1844 *hall_conductivity*, and the physical choice is the default. 

1845 

1846 Parameters 

1847 ---------- 

1848 e_fermi 

1849 See *hall_conductivity*. 

1850 temperature 

1851 See *hall_conductivity*. 

1852 nk 

1853 See *hall_conductivity*. 

1854 plane 

1855 See *hall_conductivity*. 

1856 k_fixed 

1857 See *hall_conductivity*. 

1858 positions 

1859 See *hall_conductivity*. 

1860 refine 

1861 See *hall_conductivity*. 

1862 refine_fraction 

1863 See *hall_conductivity*. 

1864 

1865 Returns 

1866 ------- 

1867 M 

1868 Real number (or ndarray shaped like *e_fermi*); in 3D 

1869 with ``k_fixed=None``, ndarray of shape (3,) (or ``e_fermi.shape + (3,)``). 

1870 

1871 Examples 

1872 -------- 

1873 Example usage:: 

1874 

1875 mu = np.linspace(-0.5, 0.5, 11) # in the gap of a Chern insulator 

1876 M = hal.orbital_magnetization(mu) # slope C = hall_conductivity(mu) 

1877 ''' 

1878 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1879 refine_fraction, None, 'magnetization') 

1880 

1881 def anomalous_nernst_conductivity( 

1882 self, e_fermi: float | ArrayLike, temperature: float, nk: int | tuple[int, ...] = 60, 

1883 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1884 refine: int = 1, refine_fraction: float = 0.05, 

1885 ) -> float | NDArray[np.float64]: 

1886 r''' 

1887 Get the intrinsic anomalous Nernst (transverse thermoelectric) 

1888 conductivity :math:`\alpha_{xy}`, :math:`j_x = \alpha_{xy}(-\partial_yT)`, 

1889 from the Berry curvature weighted by the entropy of each state 

1890 (Xiao, Yao, Fang and Niu, Phys. Rev. Lett. 97, 026603 (2006)): 

1891 

1892 .. math:: 

1893 

1894 \alpha_{xy} = \frac{ek_B}{h}\,\frac{1}{2\pi}\int_{BZ}d^2k\sum_n s_n\,\Omega_n\, , 

1895 \qquad s = -f\ln f - (1-f)\ln(1-f)\, . 

1896 

1897 Only the states within a few :math:`k_BT` of :math:`\mu` carry 

1898 entropy, so it vanishes in a gap at low temperature. It obeys the 

1899 Mott relation exactly, 

1900 :math:`\alpha_{xy}(\mu, T) = \frac{1}{eT}\int dE\,(E-\mu)\left(-\frac{\partial f}{\partial E}\right)\sigma_{xy}(E)`, 

1901 with :math:`\sigma_{xy}(E)` the :math:`T = 0` *hall_conductivity* 

1902 at Fermi energy :math:`E`, which fixes the sign. The pair form of 

1903 *hall_conductivity* keeps it finite where bands touch. 

1904 

1905 **Units.** :math:`ek_B/h` in 2D (:math:`k_B = 1`, temperatures in 

1906 energy units), per length for the 3D vector (``k_fixed=None``). 

1907 

1908 Parameters 

1909 ---------- 

1910 e_fermi 

1911 See *hall_conductivity*. 

1912 temperature : Positive real number 

1913 In energy units (:math:`k_B = 1`). 

1914 nk 

1915 See *hall_conductivity*. 

1916 plane 

1917 See *hall_conductivity*. 

1918 k_fixed 

1919 See *hall_conductivity*. 

1920 positions 

1921 See *hall_conductivity*. 

1922 refine 

1923 See *hall_conductivity*. 

1924 refine_fraction 

1925 See *hall_conductivity*. 

1926 

1927 Returns 

1928 ------- 

1929 alpha : Real number (or ndarray, as *hall_conductivity*) 

1930 

1931 Examples 

1932 -------- 

1933 Example usage:: 

1934 

1935 alpha = hal.anomalous_nernst_conductivity(np.linspace(-3, 3, 301), 0.05) 

1936 ''' 

1937 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1938 refine_fraction, None, 'nernst') 

1939 

1940 def thermal_hall_conductivity( 

1941 self, e_fermi: float | ArrayLike, temperature: float, nk: int | tuple[int, ...] = 60, 

1942 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1943 refine: int = 1, refine_fraction: float = 0.05, 

1944 ) -> float | NDArray[np.float64]: 

1945 r''' 

1946 Get the intrinsic thermal Hall conductivity of the electrons 

1947 :math:`\kappa_{xy}`, :math:`j^Q_x = \kappa_{xy}(-\partial_yT)` 

1948 (Qin, Niu and Shi, Phys. Rev. Lett. 107, 236601 (2011)): 

1949 

1950 .. math:: 

1951 

1952 \kappa_{xy} = \frac{k_B^2T}{h}\,\frac{1}{2\pi}\int_{BZ}d^2k 

1953 \sum_n c_2(x_n)\,\Omega_n\, ,\qquad 

1954 c_2(x) = \int_x^\infty y^2\left(-\frac{\partial f}{\partial y}\right)dy\, , 

1955 

1956 :math:`x_n = (E_n-\mu)/k_BT` (:math:`c_2 = \pi^2/3` deep below 

1957 :math:`\mu`, 0 far above). Equivalently, exactly, 

1958 :math:`\kappa_{xy}(\mu, T) = \frac{1}{e^2T}\int dE\,(E-\mu)^2\left(-\frac{\partial f}{\partial E}\right)\sigma_{xy}(E)` 

1959 with the :math:`T = 0` *hall_conductivity*. In a gap at low 

1960 temperature it is the Wiedemann-Franz value 

1961 :math:`\kappa_{xy} = \frac{\pi^2k_B^2}{3e^2}T\sigma_{xy}`: for a Chern 

1962 insulator the quantized :math:`C\,\pi^2k_B^2T/3h`. 

1963 

1964 **Units.** :math:`k_B^2/h` times the temperature (in energy units, 

1965 :math:`k_B = 1`) in 2D, so that :math:`\kappa/T = \pi^2C/3` on a 

1966 Chern plateau; per length for the 3D vector (``k_fixed=None``). 

1967 The sign is that of *hall_conductivity*. 

1968 

1969 Parameters 

1970 ---------- 

1971 e_fermi 

1972 See *hall_conductivity*. 

1973 temperature : Positive real number 

1974 In energy units (:math:`k_B = 1`). 

1975 nk 

1976 See *hall_conductivity*. 

1977 plane 

1978 See *hall_conductivity*. 

1979 k_fixed 

1980 See *hall_conductivity*. 

1981 positions 

1982 See *hall_conductivity*. 

1983 refine 

1984 See *hall_conductivity*. 

1985 refine_fraction 

1986 See *hall_conductivity*. 

1987 

1988 Returns 

1989 ------- 

1990 kappa : Real number (or ndarray, as *hall_conductivity*) 

1991 

1992 Examples 

1993 -------- 

1994 Example usage:: 

1995 

1996 kappa = hal.thermal_hall_conductivity(0., 0.01) # = pi^2/3 C T in the gap 

1997 ''' 

1998 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1999 refine_fraction, None, 'thermal') 

2000 

2001 def _axion_rate( 

2002 self, ks: NDArray[np.float64], bands: list[int], params: dict, param: str, 

2003 positions: bool, 

2004 ) -> float: 

2005 r''' 

2006 Private method. The mesh average, over *ks*, of 

2007 :math:`\mathrm{tr}[F_{\lambda x}F_{yz} + F_{\lambda y}F_{zx} + F_{\lambda z}F_{xy}]` 

2008 for the bands *bands* at the parameters *params*, the derivative 

2009 along *param* by central differences. The non-Abelian curvature is 

2010 :math:`F_{\mu\nu} = i(X_\mu^\dagger X_\nu - X_\nu^\dagger X_\mu)`, 

2011 :math:`(X_\mu)_{lm} = \langle l|\partial_\mu H|m\rangle/(E_m-E_l)` 

2012 (:math:`m` in *bands*, :math:`l` not), i.e. 

2013 :math:`Q|\partial_\mu u_m\rangle` in the eigenbasis: gauge covariant. 

2014 ''' 

2015 value = params[param] 

2016 step = 1e-5 * max(1., abs(value)) 

2017 others = [b for b in range(self.norb) if b not in bands] 

2018 total, gap = 0., np.inf 

2019 chunk = max(1, 400000 // self.norb ** 2) 

2020 for c0 in range(0, len(ks), chunk): 

2021 k = ks[c0:c0 + chunk] 

2022 ham, dham = self._bloch_derivatives(k, list(np.eye(3)), positions, params) 

2023 plus = self._bloch_derivatives(k, [], positions, dict(params, **{param: value + step}))[0] 

2024 minus = self._bloch_derivatives(k, [], positions, dict(params, **{param: value - step}))[0] 

2025 en, vec = np.linalg.eigh(ham) 

2026 occ, emp = vec[:, :, bands], vec[:, :, others] 

2027 de = en[:, others][:, :, None] - en[:, bands][:, None, :] 

2028 gap = min(gap, np.abs(de).min()) 

2029 emp_h = emp.conj().transpose(0, 2, 1) 

2030 x = [-(emp_h @ d @ occ) / de for d in [(plus - minus) / (2 * step), *dham]] 

2031 f = {} 

2032 for a, b in ((0, 1), (0, 2), (0, 3), (1, 2), (2, 3), (3, 1)): 

2033 xa_h = x[a].conj().transpose(0, 2, 1) 

2034 xb_h = x[b].conj().transpose(0, 2, 1) 

2035 f[a, b] = 1j * (xa_h @ x[b] - xb_h @ x[a]) 

2036 trace = (np.einsum('kab,kba->k', f[0, 1], f[2, 3]) + np.einsum('kab,kba->k', f[0, 2], f[3, 1]) 

2037 + np.einsum('kab,kba->k', f[0, 3], f[1, 2])) 

2038 total += trace.real.sum() 

2039 error_handling.path_gap(gap, param, value) 

2040 return total / len(ks) 

2041 

2042 def axion_angle( 

2043 self, bands: list[int], param: str, values: ArrayLike, nk: int | tuple[int, int, int] = 16, 

2044 theta0: float = 0., positions: bool = True, 

2045 ) -> NDArray[np.float64]: 

2046 r''' 

2047 Get the axion angle :math:`\theta` of a 3D insulator along a path of 

2048 a parameter of its value functions (*set_hopping*), from the second 

2049 Chern form (Qi, Hughes and Zhang, Phys. Rev. B 78, 195424 (2008); 

2050 Essin, Moore and Vanderbilt, Phys. Rev. Lett. 102, 146805 (2009)): 

2051 

2052 .. math:: 

2053 

2054 \frac{d\theta}{d\lambda} = \frac{1}{2\pi}\int_{BZ}d^3k\; 

2055 \mathrm{tr}\left[F_{\lambda x}F_{yz} + F_{\lambda y}F_{zx} + F_{\lambda z}F_{xy}\right], 

2056 

2057 :math:`F_{\mu\nu}` the non-Abelian Berry curvature of the bands 

2058 *bands* in :math:`(\lambda, k_x, k_y, k_z)` space, so that 

2059 :math:`\theta(\lambda) = \theta_0 + \int_{\lambda_0}^{\lambda}\frac{d\theta}{d\lambda'}d\lambda'`. 

2060 This is the variation of the Chern-Simons axion coupling, computed 

2061 from gauge-invariant quantities only (no smooth gauge is needed), 

2062 and :math:`\theta_0` is its value at ``values[0]``: e.g. 0 

2063 for a path starting from an atomic insulator. A closed path (a 

2064 pumping cycle) changes :math:`\theta` by :math:`2\pi C_2`, with 

2065 :math:`C_2` the second Chern number of the 4D family. At a point 

2066 with time-reversal or inversion symmetry, :math:`\theta` is 0 or 

2067 :math:`\pi` (mod :math:`2\pi`): :math:`\pi` for a strong topological 

2068 insulator, cf. *z2_indices_3d*. The sign of :math:`\theta` follows 

2069 the orientation :math:`(\lambda, k_x, k_y, k_z)` above. 

2070 

2071 The path must stay gapped: the bands *bands* must be separated 

2072 from the others at every k-point and every value (a ValueError 

2073 names the value where they touch). The integrand is smooth, so the 

2074 uniform k-mesh converges exponentially; the :math:`\lambda` 

2075 integral is a trapezoid over *values* (error :math:`O(\Delta\lambda^2)`). 

2076 :math:`\partial H/\partial\lambda` is a central difference, and 

2077 :math:`\partial H/\partial\mathbf{k}` is exact (see 

2078 *hall_conductivity*, whose *positions* convention applies). 

2079 

2080 Parameters 

2081 ---------- 

2082 bands : List of integers 

2083 The occupied bands (e.g. the lower 

2084 half of the spectrum). 

2085 param : String 

2086 Name of the parameter of the value functions 

2087 that the path varies (see *set_params*; the other parameters keep 

2088 their values in *params*). 

2089 values : 1D array of at least two real numbers, increasing or decreasing 

2090 The parameter values along the path. 

2091 nk : Positive integer, or tuple of 3 positive integers, default 16 

2092 k-mesh. 

2093 theta0 : Real number, default 0 

2094 :math:`\theta` at ``values[0]``. 

2095 positions : Boolean, default True 

2096 See *hall_conductivity*. 

2097 

2098 Returns 

2099 ------- 

2100 theta 

2101 ndarray, shape ``(len(values),)``: :math:`\theta` 

2102 at each value, not reduced modulo :math:`2\pi`. 

2103 

2104 Examples 

2105 -------- 

2106 Example usage:: 

2107 

2108 phi = np.linspace(0., 2 * np.pi, 41) 

2109 theta = ks.axion_angle([0, 1], 'phi', phi) # theta[-1] = 2 pi C2 

2110 ''' 

2111 self._check_static() 

2112 error_handling.dim_exact(self.dim, 3) 

2113 error_handling.hermitian_model(self.is_hermitian()) 

2114 error_handling.band_indices(bands, self.norb) 

2115 error_handling.path_parameter(param) 

2116 error_handling.path_values(values) 

2117 if isinstance(nk, int): 

2118 nk = (nk,) * 3 

2119 error_handling.nk(nk, 3) 

2120 error_handling.real_number(theta0, 'theta0') 

2121 error_handling.boolean(positions, 'positions') 

2122 values = np.asarray(values, dtype='f8') 

2123 fracs = np.array(np.meshgrid(*[np.arange(n) / n for n in nk], indexing='ij')).reshape(3, -1).T 

2124 ks = fracs @ self.rec_vec_k 

2125 volume = abs(np.linalg.det(self.rec_vec_k)) 

2126 rates = [self._axion_rate(ks, bands, dict(self.params, **{param: float(v)}), param, positions) 

2127 for v in values] 

2128 return theta0 + cumulative_trapezoid(np.array(rates) * volume / (2 * PI), values, initial=0.) 

2129 

2130 # ------------------------------------------------------------------ 

2131 # Finite samples and non-Hermitian band theory 

2132 # ------------------------------------------------------------------ 

2133 

2134 def _hop_cells(self) -> list[tuple[int, int, tuple[int, ...], complex]]: 

2135 r''' 

2136 Private method. The stored hoppings with their lattice vector as 

2137 integers :math:`(n_1, ...)` (from :math:`\mathbf{R}\cdot\mathbf{b}_i/2\pi`). 

2138 ''' 

2139 rec = np.array(self.rec_vec, dtype='f8') 

2140 return [(i, j, tuple(int(n) for n in np.rint(rec @ R / (2 * PI))), t) 

2141 for i, j, R, t in self._hop] 

2142 

2143 def _finite_entries( 

2144 self, n_cells: int | tuple[int, ...], periodic: bool, 

2145 ) -> tuple[tuple[int, ...], int, list[tuple[NDArray, NDArray, int, int, tuple, NDArray, complex]]]: 

2146 ''' 

2147 Private method. For each stored hopping, the row and column indices 

2148 it fills in a finite sample of *n_cells* cells (see *finite_ham*), 

2149 with its orbitals, lattice vector (integers and Cartesian) and amplitude. 

2150 ''' 

2151 if isinstance(n_cells, int): 

2152 n_cells = (n_cells,) * self.dim 

2153 n_tot = int(np.prod(n_cells)) 

2154 cells = np.array(np.meshgrid(*[np.arange(n) for n in n_cells], indexing='ij')) 

2155 cells = cells.reshape(self.dim, -1).T # (n_tot, dim) 

2156 strides = np.cumprod((1,) + tuple(n_cells[:-1])) 

2157 index = cells @ strides 

2158 order = np.argsort(index) 

2159 cells = cells[order] 

2160 dims = np.array(n_cells) 

2161 entries = [] 

2162 for (i, j, R, t), (_, _, R_cart, _) in zip(self._hop_cells(), self._hop): 

2163 target = cells + np.array(R)[None, :] 

2164 if periodic: 

2165 target = target % dims 

2166 keep = np.ones(n_tot, bool) 

2167 else: 

2168 keep = np.all((target >= 0) & (target < dims), axis=1) 

2169 rows = np.arange(n_tot)[keep] * self.norb + i 

2170 cols = (target[keep] @ strides) * self.norb + j 

2171 entries.append((rows, cols, i, j, R, R_cart, t)) 

2172 return n_cells, n_tot, entries 

2173 

2174 def finite_ham( 

2175 self, n_cells: int | tuple[int, ...], periodic: bool = False, sparse: bool = False, 

2176 ) -> NDArray[np.complex128] | sp.csr_matrix: 

2177 r''' 

2178 Get the real-space Hamiltonian of a finite sample of 

2179 :math:`N_1\times N_2\times\dots` unit cells of the model -- open 

2180 boundaries (a flake, a finite chain), or periodic ones (a torus). 

2181 Orbital *o* of cell :math:`(n_1, n_2, n_3)` is row 

2182 :math:`((n_3 N_2 + n_2) N_1 + n_1)\,\mathrm{norb} + o`. 

2183 

2184 Parameters 

2185 ---------- 

2186 n_cells : Positive integer, or tuple of *dim* positive integers 

2187 periodic : Boolean, default False 

2188 Periodic boundary 

2189 conditions (the spectrum is then that of *get_bands* on the 

2190 :math:`N_1\times N_2\times\dots` k-mesh), else open ones. 

2191 sparse : Boolean, default False 

2192 Return a sparse CSR 

2193 matrix (for large samples, e.g. with *tbkit.kpm*) instead of a 

2194 dense array. 

2195 

2196 Returns 

2197 ------- 

2198 ham 

2199 Complex ndarray (or CSR matrix), shape (N*norb, N*norb), N the number of cells. 

2200 ''' 

2201 error_handling.nk(n_cells, self.dim) 

2202 error_handling.boolean(periodic, 'periodic') 

2203 error_handling.boolean(sparse, 'sparse') 

2204 _, n_tot, entries = self._finite_entries(n_cells, periodic) 

2205 onsite = np.diag(self.onsite) + self._onsite_offdiag 

2206 if sparse: 

2207 onsite_rows, onsite_cols = np.nonzero(onsite) 

2208 rows = [c * self.norb + onsite_rows for c in range(n_tot)] 

2209 cols = [c * self.norb + onsite_cols for c in range(n_tot)] 

2210 vals = [onsite[onsite_rows, onsite_cols]] * n_tot 

2211 for r, c, _, _, _, _, t in entries: 

2212 rows.append(r) 

2213 cols.append(c) 

2214 vals.append(np.full(len(r), t, dtype='c16')) 

2215 n = n_tot * self.norb 

2216 return sp.coo_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), 

2217 shape=(n, n)).tocsr() 

2218 ham = np.zeros((n_tot * self.norb, n_tot * self.norb), 'c16') 

2219 for cell in range(n_tot): 

2220 ham[cell*self.norb:(cell+1)*self.norb, cell*self.norb:(cell+1)*self.norb] += onsite 

2221 for r, c, _, _, _, _, t in entries: 

2222 np.add.at(ham, (r, c), t) 

2223 return ham 

2224 

2225 def finite_velocity( 

2226 self, n_cells: int | tuple[int, ...], periodic: bool = False, sparse: bool = False, 

2227 ) -> list[NDArray[np.complex128] | sp.csr_matrix]: 

2228 r''' 

2229 Get the velocity operators :math:`\mathbf{v} = i[H, \mathbf{r}]` 

2230 (:math:`\hbar = 1`) of the finite sample of *finite_ham*, from the 

2231 Cartesian bond vectors of the hoppings rather than from the site 

2232 coordinates: 

2233 

2234 .. math:: 

2235 

2236 (v_\alpha)_{ab} = i\,H_{ab}\,d_\alpha\, ,\qquad 

2237 \mathbf{d} = \mathbf{R} + \boldsymbol\tau_j - \boldsymbol\tau_i\, , 

2238 

2239 so that they are well defined on a torus (``periodic=True``), where 

2240 the position operator is not: a bond that wraps around the sample 

2241 keeps its short displacement. They are the real-space counterpart of 

2242 :math:`\partial H/\partial\mathbf{k}` with ``positions=True`` 

2243 (*hall_conductivity*), and the input of *tbkit.kpm.hall_conductivity*. 

2244 

2245 Parameters 

2246 ---------- 

2247 n_cells 

2248 See *finite_ham*. 

2249 periodic : Boolean, default False 

2250 See *finite_ham*. 

2251 sparse : Boolean, default False 

2252 See *finite_ham*. 

2253 

2254 Returns 

2255 ------- 

2256 vel 

2257 List of *space_dim* complex ndarrays (or CSR 

2258 matrices), shape (N*norb, N*norb): :math:`v_x, v_y, (v_z)`. 

2259 ''' 

2260 error_handling.nk(n_cells, self.dim) 

2261 error_handling.boolean(periodic, 'periodic') 

2262 error_handling.boolean(sparse, 'sparse') 

2263 _, n_tot, entries = self._finite_entries(n_cells, periodic) 

2264 tau = self.orbital_positions() 

2265 n = n_tot * self.norb 

2266 vel = [] 

2267 for alpha in range(self.space_dim): 

2268 rows, cols, vals = [np.zeros(0, int)], [np.zeros(0, int)], [np.zeros(0, 'c16')] 

2269 for r, c, i, j, _, R_cart, t in entries: 

2270 d = R_cart[alpha] + tau[j, alpha] - tau[i, alpha] 

2271 rows.append(r) 

2272 cols.append(c) 

2273 vals.append(np.full(len(r), 1j * t * d, dtype='c16')) 

2274 mat = sp.coo_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), 

2275 shape=(n, n)).tocsr() 

2276 vel.append(mat if sparse else mat.toarray()) 

2277 return vel 

2278 

2279 def get_ham_beta(self, beta: complex) -> NDArray[np.complex128]: 

2280 r''' 

2281 Get the non-Bloch Hamiltonian of a 1D model, 

2282 :math:`H(\beta) = \sum_{n} t(n)\, \beta^{n}`: the Bloch Hamiltonian 

2283 continued off the unit circle, :math:`\beta = e^{ik}` with complex 

2284 :math:`k`. On the generalized Brillouin zone (see *gbz*), its 

2285 spectrum is the open-chain spectrum of a non-Hermitian chain. 

2286 

2287 Parameters 

2288 ---------- 

2289 beta : Complex number, nonzero 

2290 

2291 Returns 

2292 ------- 

2293 ham : Complex ndarray, shape (norb, norb) 

2294 ''' 

2295 error_handling.dim_exact(self.dim, 1) 

2296 error_handling.nonzero_number(beta, 'beta') 

2297 ham = np.diag(self.onsite).astype('c16') + self._onsite_offdiag 

2298 for i, j, (n,), t in self._hop_cells(): 

2299 ham[i, j] += t * complex(beta) ** n 

2300 return ham 

2301 

2302 def spectral_winding(self, e_ref: complex = 0., nk: int = 400) -> float: 

2303 r''' 

2304 Get the spectral winding number of a 1D (non-Hermitian) model about 

2305 the reference energy :math:`E_r`, 

2306 

2307 .. math:: 

2308 

2309 W(E_r) = \frac{1}{2\pi i}\oint_0^{2\pi} dk\, 

2310 \partial_k \ln\det\left[H(k) - E_r\right]\, , 

2311 

2312 the number of times the periodic-chain spectrum winds around 

2313 :math:`E_r` in the complex plane. :math:`W \neq 0` for some 

2314 :math:`E_r` is the criterion for the non-Hermitian skin effect: the 

2315 open-chain eigenstates then pile up at one end (Okuma, Kawabata, 

2316 Shiozaki and Sato, Phys. Rev. Lett. 124, 086801 (2020)). 

2317 

2318 Parameters 

2319 ---------- 

2320 e_ref : Complex number, default 0 

2321 Reference energy. 

2322 nk : Positive integer, default 400 

2323 k-points. 

2324 

2325 Returns 

2326 ------- 

2327 W 

2328 Real number, close to an integer (it is ill defined if 

2329 :math:`E_r` lies on the periodic spectrum). 

2330 ''' 

2331 error_handling.dim_exact(self.dim, 1) 

2332 error_handling.number(e_ref, 'e_ref') 

2333 error_handling.positive_int(nk, 'nk') 

2334 ks = np.arange(nk + 1) / nk * self.rec_vec_k[0, 0] 

2335 dets = np.array([np.linalg.det(self.get_ham([k]) - e_ref * np.eye(self.norb)) for k in ks]) 

2336 steps = np.angle(dets[1:] / dets[:-1]) 

2337 return float(np.sum(steps) / (2 * PI)) 

2338 

2339 def gbz(self, energies: ArrayLike) -> NDArray[np.complex128]: 

2340 r''' 

2341 Get the generalized Brillouin zone of a 1D non-Hermitian model 

2342 (Yao and Wang, Phys. Rev. Lett. 121, 086803 (2018); Yokomizo and 

2343 Murakami, Phys. Rev. Lett. 123, 066404 (2019)): for each energy 

2344 :math:`E`, the roots of :math:`\det[H(\beta)-E] = 0` sorted by modulus, 

2345 :math:`|\beta_1|\le\dots\le|\beta_{p+s}|` (:math:`p` the order of its 

2346 pole at :math:`\beta = 0`), and the middle pair 

2347 :math:`\beta_p, \beta_{p+1}`. The open-chain spectrum, in the limit of 

2348 a long chain, is where :math:`|\beta_p| = |\beta_{p+1}|`; those 

2349 :math:`\beta` trace the generalized Brillouin zone, on which the 

2350 non-Bloch bands :math:`H(\beta)` (see *get_ham_beta*) replace the 

2351 Bloch ones. For a Hermitian model it is the unit circle. 

2352 

2353 Parameters 

2354 ---------- 

2355 energies 

2356 Complex array of energies (e.g. open-chain eigenvalues 

2357 from *finite_ham*). 

2358 

2359 Returns 

2360 ------- 

2361 beta : Complex ndarray, shape (len(energies), 2): :math:`\beta_p, \beta_{p+1}` 

2362 ''' 

2363 error_handling.dim_exact(self.dim, 1) 

2364 energies = np.atleast_1d(np.asarray(energies, dtype='c16')) 

2365 error_handling.ndarray_empty(energies, 'energies') 

2366 powers = [n for _, _, (n,), _ in self._hop_cells()] + [0] 

2367 low, high = self.norb * min(powers), self.norb * max(powers) 

2368 n_samples = 2 * (high - low) + 8 

2369 theta = 2 * PI * np.arange(n_samples) / n_samples 

2370 hams = np.array([self.get_ham_beta(np.exp(1j * th)) for th in theta]) 

2371 out = np.zeros((len(energies), 2), 'c16') 

2372 exps = np.arange(low, high + 1) 

2373 for n, e in enumerate(energies): 

2374 dets = np.linalg.det(hams - e * np.eye(self.norb)[None]) 

2375 # Laurent coefficients c_m of det[H(beta) - E] = sum_m c_m beta^m 

2376 coef = np.array([np.mean(dets * np.exp(-1j * m * theta)) for m in exps]) 

2377 nonzero = np.abs(coef) > 1e-12 * np.max(np.abs(coef)) 

2378 first, last = np.argmax(nonzero), len(coef) - 1 - np.argmax(nonzero[::-1]) 

2379 p = -exps[first] 

2380 roots = np.roots(coef[first:last + 1][::-1]) 

2381 roots = roots[np.argsort(np.abs(roots))] 

2382 error_handling.gbz_roots(len(roots), p) 

2383 out[n] = roots[p - 1], roots[p] 

2384 return out 

2385 

2386 # ------------------------------------------------------------------ 

2387 # Berry phases, Wannier centres, Z2, symmetries, quantum geometry 

2388 # ------------------------------------------------------------------ 

2389 

2390 def orbital_positions(self) -> NDArray[np.float64]: 

2391 r''' 

2392 Get the positions :math:`\boldsymbol\tau` of the orbitals within the 

2393 unit cell (each site twice, for its two spin components, if 

2394 ``spin=True``). 

2395 

2396 Returns 

2397 ------- 

2398 tau : Real ndarray, shape (norb, space_dim) 

2399 ''' 

2400 tau = np.array([dic['r0'] for dic in self.lat.unit_cell], dtype='f8') 

2401 return np.repeat(tau, 2, axis=0) if self.spin else tau 

2402 

2403 def _loop( 

2404 self, bands: list[int], nk: int, direction: int, fracs_perp: dict[int, float], 

2405 positions: bool, 

2406 ) -> NDArray[np.complex128]: 

2407 ''' 

2408 Private method. Wilson loop matrix of *bands* along the closed path 

2409 k0 -> k0 + b_direction (nk steps), at fixed fractional coordinates 

2410 *fracs_perp* along the other reciprocal vectors. 

2411 ''' 

2412 frac = np.zeros(self.dim) 

2413 for d, f in fracs_perp.items(): 

2414 frac[d] = f 

2415 b = self.rec_vec_k[direction] 

2416 k0 = frac @ self.rec_vec_k 

2417 # each link carries exp(-i dk.tau): the Bloch sums then include the 

2418 # orbital positions, so that the phases locate the Wannier centres 

2419 # in space rather than on the cell origin. 

2420 dk_space = self.k_basis @ (b / nk) 

2421 phase = np.exp(-1j * self.orbital_positions() @ dk_space) if positions \ 

2422 else np.ones(self.norb) 

2423 vs = self._subspaces(k0 + np.arange(nk)[:, None] * b[None] / nk, bands) 

2424 links = vs.conj().transpose(0, 2, 1) @ (phase[:, None] * np.roll(vs, -1, axis=0)) 

2425 # keep only the unitary part of each link (its singular values 

2426 # tend to 1 as nk grows): the product then stays unitary 

2427 u, _, vh = np.linalg.svd(links) 

2428 wilson = np.eye(len(bands), dtype='c16') 

2429 for unitary in u @ vh: 

2430 wilson = wilson @ unitary 

2431 return wilson 

2432 

2433 def _perp(self, direction: int, k_perp: float | tuple | None) -> dict[int, float]: 

2434 ''' 

2435 Private method. Fractional coordinates of a loop along the other 

2436 reciprocal vectors, from *k_perp* (a number in 2D, a pair in 3D). 

2437 ''' 

2438 others = [d for d in range(self.dim) if d != direction] 

2439 if k_perp is None: 

2440 k_perp = (0.,) * len(others) 

2441 if not isinstance(k_perp, tuple): 

2442 k_perp = (k_perp,) 

2443 error_handling.k_perp(k_perp, len(others)) 

2444 return dict(zip(others, k_perp)) 

2445 

2446 def berry_phase( 

2447 self, bands: int | list[int], nk: int = 100, direction: int = 0, 

2448 k_perp: float | tuple[float, float] | None = None, positions: bool = True, 

2449 ) -> float: 

2450 r''' 

2451 Get the Berry phase of a group of bands along a closed loop across the 

2452 Brillouin zone, :math:`\mathbf{k}\to\mathbf{k}+\mathbf{b}_{direction}`, 

2453 

2454 .. math:: 

2455 

2456 \gamma = \oint \mathbf{A}\cdot d\mathbf{k} = -\arg\det W\, ,\qquad 

2457 W = \prod_{m} \langle u(\mathbf{k}_m)|u(\mathbf{k}_{m+1})\rangle\, , 

2458 

2459 with :math:`\mathbf{A} = i\langle u|\nabla_{\mathbf{k}}u\rangle` (the 

2460 convention of *berry_curvature*). In 1D it is the Zak phase: its 

2461 value :math:`\gamma/2\pi`, modulo 1, is the centre of the band's 

2462 Wannier functions in units of :math:`\mathbf{a}_{direction}` (the 

2463 polarization). 

2464 

2465 Parameters 

2466 ---------- 

2467 bands : Band index, or list of band indices 

2468 nk : Positive integer, default 100 

2469 Number of k-points on the loop. 

2470 direction : Integer, default 0 

2471 Reciprocal vector the loop follows. 

2472 k_perp : Real number (2D) or pair of real numbers (3D) 

2473 Default 

2474 value None (zero). Fractional coordinates of the loop along the 

2475 other reciprocal vectors. 

2476 positions : Boolean, default True 

2477 Include the orbital 

2478 positions in the Bloch functions (the physical choice: the phase 

2479 then locates the Wannier centres in the crystal). If False, all 

2480 orbitals are placed on the cell origin (the periodic gauge of 

2481 *get_ham*); the two differ by :math:`\sum_n\mathbf{b}\cdot\boldsymbol\tau` 

2482 weighted by the orbital content, and agree on quantized 

2483 differences between phases. 

2484 

2485 Returns 

2486 ------- 

2487 gamma : Real number in :math:`(-\pi, \pi]` 

2488 ''' 

2489 bands = self._check_loop(bands, nk, direction, positions) 

2490 wilson = self._loop(bands, nk, direction, self._perp(direction, k_perp), positions) 

2491 return float(-np.angle(np.linalg.det(wilson))) 

2492 

2493 def wannier_centers( 

2494 self, bands: int | list[int], nk: int = 100, direction: int = 0, 

2495 k_perp: float | tuple[float, float] | None = None, positions: bool = True, 

2496 ) -> NDArray[np.float64]: 

2497 r''' 

2498 Get the (hybrid) Wannier centres of a group of bands: minus the 

2499 phases of the eigenvalues of the Wilson loop (see *berry_phase*), 

2500 divided by :math:`2\pi`. They are the positions, in units of 

2501 :math:`\mathbf{a}_{direction}` and modulo 1, of the maximally 

2502 localized Wannier functions along that direction (hybrid: still 

2503 Bloch waves along the others, at *k_perp*). Their sum is 

2504 :math:`\gamma/2\pi`, modulo 1. 

2505 

2506 Parameters 

2507 ---------- 

2508 bands : Band index, or list of band indices 

2509 nk : Positive integer, default 100 

2510 Number of k-points on the loop. 

2511 direction : Integer, default 0 

2512 k_perp 

2513 See *berry_phase*. 

2514 positions : Boolean, default True 

2515 See *berry_phase*. 

2516 

2517 Returns 

2518 ------- 

2519 centers : Real ndarray, sorted, in :math:`[0, 1)` 

2520 ''' 

2521 bands = self._check_loop(bands, nk, direction, positions) 

2522 wilson = self._loop(bands, nk, direction, self._perp(direction, k_perp), positions) 

2523 return np.sort((-np.angle(np.linalg.eigvals(wilson)) / (2*PI)) % 1.) 

2524 

2525 def wannier_flow( 

2526 self, bands: int | list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

2527 flow: int | None = None, k_fixed: float = 0., positions: bool = True, 

2528 k_range: tuple[float, float] = (0., 1.), 

2529 ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

2530 r''' 

2531 Get the flow of the hybrid Wannier centres (see *wannier_centers*) 

2532 along *direction*, as the loop is moved across the Brillouin zone 

2533 along the reciprocal vector *flow*. In a Chern insulator the centres 

2534 wind by :math:`C` cells over a full period; in a 

2535 :math:`\mathbb{Z}_2` topological insulator, the Kramers pairs swap 

2536 partners over half a period (see *z2_invariant*). 

2537 

2538 Parameters 

2539 ---------- 

2540 bands : Band index, or list of band indices 

2541 nk : Positive integer, default 100 

2542 k-points on each loop. 

2543 nk_perp : Positive integer, default 51 

2544 Number of loops. 

2545 direction : Integer, default 0 

2546 Direction of the loops. 

2547 flow : Integer 

2548 Default value None (the first other direction). 

2549 Reciprocal vector along which the loops are moved. 

2550 k_fixed : Real number, default 0 

2551 3D only: fractional 

2552 coordinate along the remaining reciprocal vector. 

2553 positions : Boolean, default True 

2554 See *berry_phase*. 

2555 k_range : Pair of real numbers 

2556 Default value (0, 1). Range of 

2557 the fractional coordinate along *flow* (both ends included). 

2558 

2559 Returns 

2560 ------- 

2561 fracs : Real ndarray, shape (nk_perp,) 

2562 Fractional coordinates along *flow*. 

2563 centers : Real ndarray, shape (nk_perp, len(bands)) 

2564 Sorted centres, in [0, 1). 

2565 ''' 

2566 bands = self._check_loop(bands, nk, direction, positions) 

2567 error_handling.dim_min(self.dim, 2) 

2568 error_handling.positive_int(nk_perp, 'nk_perp') 

2569 others = [d for d in range(self.dim) if d != direction] 

2570 if flow is None: 

2571 flow = others[0] 

2572 error_handling.flow(flow, direction, self.dim) 

2573 error_handling.real_number(k_fixed, 'k_fixed') 

2574 error_handling.lims(k_range) 

2575 fracs = np.linspace(k_range[0], k_range[1], nk_perp) 

2576 centers = np.zeros((nk_perp, len(bands))) 

2577 for n, f in enumerate(fracs): 

2578 perp = {d: (f if d == flow else k_fixed) for d in others} 

2579 wilson = self._loop(bands, nk, direction, perp, positions) 

2580 centers[n] = np.sort((-np.angle(np.linalg.eigvals(wilson)) / (2*PI)) % 1.) 

2581 return fracs, centers 

2582 

2583 def z2_invariant( 

2584 self, bands: list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

2585 flow: int | None = None, k_fixed: float = 0., 

2586 ) -> int: 

2587 r''' 

2588 Get the :math:`\mathbb{Z}_2` invariant of a time-reversal-symmetric 

2589 group of bands (an even number, e.g. all occupied bands of the 

2590 Kane-Mele model) from the flow of its hybrid Wannier centres over 

2591 half the Brillouin zone (Soluyanov and Vanderbilt, Phys. Rev. B 83, 

2592 235401 (2011); Yu, Qi, Bernevig, Fang and Dai, Phys. Rev. B 84, 

2593 075119 (2011)): follow the midpoint of the largest gap between the 

2594 centres, and count, modulo 2, how many centres it jumps over. It is 

2595 odd when the Kramers partners exchange along the flow -- a 

2596 topological insulator -- and even otherwise. 

2597 

2598 Unlike the parity criterion (*parity_z2*), it needs no inversion 

2599 symmetry, so it also works with Rashba coupling. 

2600 

2601 Parameters 

2602 ---------- 

2603 bands : List of band indices (an even number of bands) 

2604 nk : Positive integer, default 100 

2605 k-points on each loop. 

2606 nk_perp : Positive integer, default 51 

2607 Number of loops 

2608 over the half zone (the flow must be resolved finely enough that 

2609 no two centres cross between neighbouring loops unnoticed). 

2610 direction : Integer, default 0 

2611 See *wannier_flow*. 

2612 flow : Integer, default None 

2613 See *wannier_flow*. 

2614 k_fixed : Real number, default 0 

2615 3D only: a time-reversal 

2616 invariant plane has *k_fixed* 0 or 0.5 (the weak and strong 

2617 indices follow from the six such planes). 

2618 

2619 Returns 

2620 ------- 

2621 nu : 0 (trivial) or 1 (topological) 

2622 ''' 

2623 error_handling.even_bands(bands) 

2624 _, centers = self.wannier_flow(bands, nk, nk_perp, direction, flow, k_fixed, 

2625 positions=False, k_range=(0., 0.5)) 

2626 gap = [self._largest_gap(c) for c in centers] 

2627 jumps = sum(self._between(gap[n], gap[n + 1], centers[n + 1]) 

2628 for n in range(len(gap) - 1)) 

2629 return int(jumps % 2) 

2630 

2631 def z2_indices_3d( 

2632 self, bands: list[int], nk: int = 100, nk_perp: int = 51, 

2633 ) -> tuple[int, int, int, int]: 

2634 r''' 

2635 Get the four :math:`\mathbb{Z}_2` indices 

2636 :math:`(\nu_0;\nu_1\nu_2\nu_3)` of a 3D time-reversal-invariant 

2637 insulator (Fu, Kane and Mele, Phys. Rev. Lett. 98, 106803 (2007); 

2638 Moore and Balents, Phys. Rev. B 75, 121306(R) (2007)) from the 

2639 :math:`\mathbb{Z}_2` invariants of its six time-reversal-invariant 

2640 planes :math:`k_i = 0` and :math:`k_i = 1/2` (fractional coordinates 

2641 along :math:`\mathbf{b}_i`), each computed by *z2_invariant*: 

2642 

2643 .. math:: 

2644 

2645 \nu_0 = \nu(k_i = 0) + \nu(k_i = 1/2) \bmod 2\, ,\qquad 

2646 \nu_i = \nu(k_i = 1/2)\, . 

2647 

2648 The strong index :math:`\nu_0` comes out the same for the three 

2649 :math:`i`, which is checked (a ValueError asks for a finer flow 

2650 otherwise). :math:`\nu_0 = 1` is a strong topological insulator; a 

2651 weak one has :math:`\nu_0 = 0` and some :math:`\nu_i = 1`, i.e. 

2652 stacked quantum spin Hall layers normal to 

2653 :math:`\nu_1\mathbf{b}_1 + \nu_2\mathbf{b}_2 + \nu_3\mathbf{b}_3`. 

2654 No inversion symmetry is needed (compare *parity_z2*). 

2655 

2656 Parameters 

2657 ---------- 

2658 bands : List of band indices (an even number of bands) 

2659 nk : Positive integer, default 100 

2660 See *z2_invariant*. 

2661 nk_perp : Positive integer, default 51 

2662 See *z2_invariant*. 

2663 

2664 Returns 

2665 ------- 

2666 indices : Tuple of four integers, 0 or 1: :math:`(\nu_0, \nu_1, \nu_2, \nu_3)` 

2667 ''' 

2668 error_handling.dim_exact(self.dim, 3) 

2669 nu = np.zeros((3, 2), int) 

2670 for i in range(3): 

2671 direction, flow = [d for d in range(3) if d != i] 

2672 for n, k_fixed in enumerate((0., 0.5)): 

2673 nu[i, n] = self.z2_invariant(bands, nk, nk_perp, direction, flow, k_fixed) 

2674 strong = nu.sum(axis=1) % 2 

2675 error_handling.strong_index(strong) 

2676 return int(strong[0]), int(nu[0, 1]), int(nu[1, 1]), int(nu[2, 1]) 

2677 

2678 def entanglement_spectrum( 

2679 self, ks: ArrayLike, region: list[int], bands: int | list[int], 

2680 ) -> NDArray[np.float64]: 

2681 r''' 

2682 Get the k-resolved entanglement spectrum of a group of bands 

2683 (Peschel, J. Phys. A 36, L205 (2003); Li and Haldane, Phys. Rev. Lett. 

2684 101, 010504 (2008); Fidkowski, Phys. Rev. Lett. 104, 130502 (2010)): 

2685 the eigenvalues :math:`\xi_n(\mathbf{k})` of the correlation matrix 

2686 :math:`P(\mathbf{k})` of the filled *bands*, restricted to the 

2687 orbitals of *region*, 

2688 

2689 .. math:: 

2690 

2691 C_A(\mathbf{k}) = \left[P(\mathbf{k})\right]_{A}\, ,\qquad 

2692 P(\mathbf{k}) = \sum_{n\in\mathrm{bands}}|u_n(\mathbf{k})\rangle\langle u_n(\mathbf{k})|\, , 

2693 

2694 between 0 and 1. For a ribbon or a supercell 

2695 (*tbkit.moire.supercell*) cut in two halves across its width, the 

2696 states of the whole are either inside the region 

2697 (:math:`\xi \approx 1`) or outside (:math:`\xi \approx 0`), except 

2698 for those localized at the cuts: a topological phase leaves 

2699 entanglement modes inside :math:`(0, 1)` -- a mode pinned at 1/2 in 

2700 a chiral-symmetric chain, a branch that flows from 0 to 1 across 

2701 the zone in a Chern insulator. The single-particle entanglement 

2702 energies are :math:`\epsilon = \ln[(1-\xi)/\xi]`. 

2703 

2704 Parameters 

2705 ---------- 

2706 ks : ndarray, shape (nk, dim) 

2707 k-points (see *get_bands*). 

2708 region : List of orbital indices (the region :math:`A`) 

2709 bands : Band index, or list of band indices (the filled bands) 

2710 

2711 Returns 

2712 ------- 

2713 xi : Real ndarray, shape (nk, len(region)), sorted ascending at every k 

2714 ''' 

2715 ks = np.atleast_2d(np.asarray(ks, dtype='f8')) 

2716 error_handling.ks(ks, self.dim) 

2717 error_handling.region(region, self.norb) 

2718 if isinstance(bands, int): 

2719 bands = [bands] 

2720 error_handling.band_indices(bands, self.norb) 

2721 error_handling.hermitian_kspace(self.is_hermitian(), self._overlap_hop) 

2722 v = self._subspaces(ks, bands)[:, region, :] 

2723 return np.linalg.eigvalsh(v @ v.conj().transpose(0, 2, 1)) 

2724 

2725 @staticmethod 

2726 def _largest_gap(centers: NDArray[np.float64]) -> float: 

2727 ''' 

2728 Private method. Midpoint of the largest gap between sorted centres on the circle [0, 1). 

2729 ''' 

2730 ext = np.append(centers, centers[0] + 1.) 

2731 n = np.argmax(np.diff(ext)) 

2732 return float((ext[n] + ext[n + 1]) / 2 % 1.) 

2733 

2734 @staticmethod 

2735 def _between(a: float, b: float, centers: NDArray[np.float64]) -> int: 

2736 ''' 

2737 Private method. Number of centres on the shorter arc between a and b. 

2738 ''' 

2739 d = (b - a) % 1. 

2740 start, length = (a, d) if d <= 0.5 else (b, 1. - d) 

2741 return int(np.sum((centers - start) % 1. < length)) 

2742 

2743 def _check_loop(self, bands, nk, direction, positions) -> list[int]: 

2744 ''' 

2745 Private method. Validate the arguments shared by the Wilson-loop methods. 

2746 ''' 

2747 if isinstance(bands, int): 

2748 bands = [bands] 

2749 error_handling.band_indices(bands, self.norb) 

2750 error_handling.positive_int(nk, 'nk') 

2751 error_handling.direction(direction, self.dim) 

2752 error_handling.boolean(positions, 'positions') 

2753 return bands 

2754 

2755 def _trims(self) -> list[NDArray[np.float64]]: 

2756 ''' 

2757 Private method. Time-reversal-invariant momenta: fractional 

2758 coordinates in {0, 1/2}^dim, as k points. 

2759 ''' 

2760 grid = np.array(np.meshgrid(*[[0., 0.5]] * self.dim, indexing='ij')).reshape(self.dim, -1).T 

2761 return [f @ self.rec_vec_k for f in grid] 

2762 

2763 def _operator(self, op, k: NDArray[np.float64]) -> NDArray[np.complex128]: 

2764 ''' 

2765 Private method. Matrix of a symmetry operator at k (op is a matrix, or a callable of k). 

2766 ''' 

2767 mat = np.asarray(op(k) if callable(op) else op, dtype='c16') 

2768 error_handling.operator(mat, self.norb) 

2769 return mat 

2770 

2771 def parity_z2(self, inversion, bands: list[int]) -> int: 

2772 r''' 

2773 Get the :math:`\mathbb{Z}_2` invariant of an inversion- and 

2774 time-reversal-symmetric model from the parity eigenvalues of its 

2775 occupied bands at the time-reversal-invariant momenta 

2776 :math:`\Gamma_i` (Fu and Kane, Phys. Rev. B 76, 045302 (2007)): 

2777 

2778 .. math:: 

2779 

2780 (-1)^\nu = \prod_i \delta_i\, ,\qquad 

2781 \delta_i = \prod_{m=1}^{N} \xi_{2m}(\Gamma_i)\, , 

2782 

2783 one parity :math:`\xi = \pm1` per Kramers pair. In 3D, :math:`\nu` 

2784 is the strong index. 

2785 

2786 Parameters 

2787 ---------- 

2788 inversion 

2789 Complex ndarray, shape (norb, norb), or callable of 

2790 k returning one: the inversion operator :math:`P`, with 

2791 :math:`P H(\mathbf{k}) P^{-1} = H(-\mathbf{k})` (e.g. swapping the 

2792 two sublattices of graphene, and the identity on spin). 

2793 bands : List of band indices (the occupied Kramers pairs) 

2794 

2795 Returns 

2796 ------- 

2797 nu : 0 (trivial) or 1 (topological) 

2798 ''' 

2799 error_handling.even_bands(bands) 

2800 error_handling.band_indices(bands, self.norb) 

2801 product = 1 

2802 for k in self._trims(): 

2803 v = self._subspace(k, bands) 

2804 xi = np.linalg.eigvals(v.conj().T @ self._operator(inversion, k) @ v) 

2805 error_handling.parities(xi) 

2806 n_minus = int(np.sum(xi.real < 0)) 

2807 product *= (-1) ** (n_minus // 2) 

2808 return 0 if product == 1 else 1 

2809 

2810 def symmetry_error( 

2811 self, op, k_map: str = 'minus', antiunitary: bool = False, anti: bool = False, 

2812 nk: int = 5, seed: int = 0, transpose: bool = False, 

2813 ) -> float: 

2814 r''' 

2815 Check a symmetry of the Bloch Hamiltonian, 

2816 

2817 .. math:: 

2818 

2819 H(g\mathbf{k}) = \pm\, U\, \mathcal{K}H(\mathbf{k})\mathcal{K}^{-1} U^\dagger\, , 

2820 

2821 with :math:`g\mathbf{k} = -\mathbf{k}` or :math:`\mathbf{k}`, 

2822 :math:`\mathcal{K}` complex conjugation if *antiunitary*, and the 

2823 minus sign if *anti*: time reversal is antiunitary with 

2824 :math:`\mathbf{k}\to-\mathbf{k}` and the plus sign, particle-hole 

2825 antiunitary with :math:`\mathbf{k}\to-\mathbf{k}` and the minus sign, 

2826 chiral unitary with :math:`\mathbf{k}\to\mathbf{k}` and the minus sign, 

2827 inversion unitary with :math:`\mathbf{k}\to-\mathbf{k}` and the plus 

2828 sign. With *transpose*, :math:`H^T` replaces :math:`\mathcal{K}H\mathcal{K}^{-1} = H^*` 

2829 (and :math:`H^\dagger` with *antiunitary* too): the symmetries 

2830 of non-Hermitian Hamiltonians, where :math:`H^T \neq H^*` (see 

2831 *non_hermitian_class*). 

2832 

2833 Parameters 

2834 ---------- 

2835 op : Complex ndarray, shape (norb, norb), or callable of k: :math:`U` 

2836 k_map : 'minus' or 'identity', default 'minus' 

2837 antiunitary : Boolean, default False 

2838 anti : Boolean, default False 

2839 nk : Positive integer, default 5 

2840 Points per direction of 

2841 the (randomly shifted) test mesh. 

2842 seed : Integer, default 0 

2843 Seed of the random shift. 

2844 transpose : Boolean, default False 

2845 

2846 Returns 

2847 ------- 

2848 error : Real number 

2849 Largest deviation over the mesh (0 for a symmetry). 

2850 ''' 

2851 error_handling.k_map(k_map) 

2852 error_handling.boolean(antiunitary, 'antiunitary') 

2853 error_handling.boolean(anti, 'anti') 

2854 error_handling.positive_int(nk, 'nk') 

2855 error_handling.boolean(transpose, 'transpose') 

2856 rng = np.random.default_rng(seed) 

2857 fracs = np.array(np.meshgrid(*[np.arange(nk) / nk] * self.dim, indexing='ij')).reshape(self.dim, -1).T 

2858 fracs = fracs + rng.uniform(0., 1. / nk, self.dim) 

2859 sign = -1. if anti else 1. 

2860 err = 0. 

2861 for f in fracs: 

2862 k = f @ self.rec_vec_k 

2863 gk = -k if k_map == 'minus' else k 

2864 ham = self.get_ham(k) 

2865 u = self._operator(op, k) 

2866 ham = ham.conj() if antiunitary else ham 

2867 rhs = u @ (ham.T if transpose else ham) @ u.conj().T 

2868 err = max(err, float(np.max(np.abs(self.get_ham(gk) - sign * rhs)))) 

2869 return err 

2870 

2871 def tenfold_class( 

2872 self, time_reversal=None, particle_hole=None, chiral=None, nk: int = 5, tol: float = 1e-8, 

2873 ) -> str: 

2874 r''' 

2875 Get the Altland-Zirnbauer symmetry class (the "tenfold way") of the 

2876 model from its time-reversal :math:`T = U_T\mathcal{K}`, particle-hole 

2877 :math:`C = U_C\mathcal{K}` and chiral :math:`S = U_S` symmetries. Each 

2878 given operator is first checked (see *symmetry_error*) to be a 

2879 symmetry; the class then follows from :math:`T^2 = U_TU_T^* = \pm1`, 

2880 :math:`C^2 = \pm1`, and the presence of :math:`S`. Any two of the 

2881 three imply the third (:math:`S = TC`), which need not be given. 

2882 

2883 Parameters 

2884 ---------- 

2885 time_reversal : Matrix :math:`U_T`, or None (absent) 

2886 particle_hole : Matrix :math:`U_C`, or None (absent) 

2887 chiral : Matrix :math:`U_S`, or None (absent) 

2888 nk : Positive integer, default 5 

2889 See *symmetry_error*. 

2890 tol : Positive real, default 1e-8 

2891 Tolerance of the checks. 

2892 

2893 Returns 

2894 ------- 

2895 name : String: 'A', 'AIII', 'AI', 'BDI', 'D', 'DIII', 'AII', 'CII', 'C' or 'CI' 

2896 ''' 

2897 error_handling.positive_real(tol, 'tol') 

2898 k0 = np.zeros(self.dim) 

2899 for op, k_map, antiu, anti in ((time_reversal, 'minus', True, False), 

2900 (particle_hole, 'minus', True, True), 

2901 (chiral, 'identity', False, True)): 

2902 if op is not None: 

2903 error_handling.is_symmetry(self.symmetry_error(op, k_map, antiu, anti, nk), tol) 

2904 u_t = None if time_reversal is None else self._operator(time_reversal, k0) 

2905 u_c = None if particle_hole is None else self._operator(particle_hole, k0) 

2906 # two of the three imply the third: C = S T, T = S C 

2907 if chiral is not None and (u_t is None) != (u_c is None): 

2908 u_s = self._operator(chiral, k0) 

2909 if u_t is not None: 

2910 u_c = u_s @ u_t 

2911 elif u_c is not None: 

2912 u_t = u_s @ u_c 

2913 squares = [] 

2914 for u in (u_t, u_c): 

2915 if u is None: 

2916 squares.append(0) 

2917 continue 

2918 square = u @ u.conj() 

2919 error_handling.square(square, tol) 

2920 squares.append(int(np.sign(square[0, 0].real))) 

2921 has_s = chiral is not None or (u_t is not None and u_c is not None) 

2922 table = {(0, 0, False): 'A', (0, 0, True): 'AIII', (1, 0, False): 'AI', 

2923 (1, 1, True): 'BDI', (0, 1, False): 'D', (-1, 1, True): 'DIII', 

2924 (-1, 0, False): 'AII', (-1, -1, True): 'CII', (0, -1, False): 'C', 

2925 (1, -1, True): 'CI'} 

2926 return table[(squares[0], squares[1], has_s)] 

2927 

2928 def non_hermitian_class( 

2929 self, time_reversal=None, particle_hole=None, chiral=None, time_reversal_dagger=None, 

2930 particle_hole_dagger=None, sublattice=None, pseudo_hermiticity=None, canonical: bool = False, 

2931 nk: int = 5, tol: float = 1e-8, 

2932 ) -> str: 

2933 r''' 

2934 Get the non-Hermitian symmetry class (the "38-fold way", Kawabata, 

2935 Shiozaki, Ueda and Sato, Phys. Rev. X 9, 041015 (2019); Zhou and Lee, 

2936 Phys. Rev. B 99, 235112 (2019)) of the model. A non-Hermitian 

2937 Hamiltonian has :math:`H^* \neq H^T`, so each symmetry of the tenfold 

2938 way (see *tenfold_class*) splits in two: 

2939 

2940 .. math:: 

2941 

2942 \text{TRS: } U_T H^*(\mathbf{k}) U_T^\dagger = H(-\mathbf{k}),\qquad 

2943 \text{TRS}^\dagger\text{: } U_{C_+} H^T(\mathbf{k}) U_{C_+}^\dagger = H(-\mathbf{k}), 

2944 

2945 \text{PHS: } U_C H^T(\mathbf{k}) U_C^\dagger = -H(-\mathbf{k}),\qquad 

2946 \text{PHS}^\dagger\text{: } U_{T_-} H^*(\mathbf{k}) U_{T_-}^\dagger = -H(-\mathbf{k}), 

2947 

2948 \text{CS: } U_\Gamma H^\dagger(\mathbf{k}) U_\Gamma^\dagger = -H(\mathbf{k}),\qquad 

2949 \text{SLS: } U_S H(\mathbf{k}) U_S^\dagger = -H(\mathbf{k}),\qquad 

2950 \text{pH: } U_\eta H^\dagger(\mathbf{k}) U_\eta^\dagger = H(\mathbf{k}). 

2951 

2952 Each given operator is first checked (see *symmetry_error*) to be a 

2953 symmetry. The operators are then closed under products (two 

2954 symmetries imply their product, which need not be given, as 

2955 :math:`S = TC` in the tenfold way); a product of two operators of 

2956 the same kind must agree with the given one up to a phase (no 

2957 unitary symmetry left: block-diagonalize it first). The class 

2958 follows from the kinds present, the squares :math:`UU^* = \pm1` of 

2959 the four antiunitary kinds, and the commutation of 

2960 :math:`S` (or :math:`\eta`) with them: :math:`U_SU = \epsilon\,UU_S^*` 

2961 gives the subscript :math:`\epsilon` (for classes with TRS and PHS, 

2962 the first refers to TRS, the second to PHS; in class AIII it is 

2963 :math:`U_\Gamma U_S = \epsilon\,U_SU_\Gamma`). 

2964 

2965 The name is that of the tables of Kawabata et al.: an AZ class 

2966 ('AI', ...), an AZ\ :math:`^\dagger` class ('AI†', ...), an AZ class 

2967 with SLS ('A + S', 'BDI + S-+', ...) when :math:`S` is present, or 

2968 with pseudo-Hermiticity ('A + η', 'D + η+', ...) when :math:`\eta` 

2969 is. These 54 names describe 38 classes: :math:`H \to iH` turns TRS 

2970 into PHS\ :math:`^\dagger` and CS into pH, keeping the topology but 

2971 exchanging real and imaginary line gaps, so that D† is AI, C† is AII, 

2972 AII + S- is AI + S-, DIII + S-+ is BDI + S-+, A + η is AIII, and so 

2973 on (their Tables XII and XIV). Since the tables differ for such 

2974 partners, the name keeps the symmetries as given unless *canonical*. 

2975 

2976 Parameters 

2977 ---------- 

2978 time_reversal : Matrix :math:`U_T` (TRS), or None (absent) 

2979 particle_hole : Matrix :math:`U_C` (PHS), or None (absent) 

2980 chiral : Matrix :math:`U_\Gamma` (CS), or None (absent) 

2981 time_reversal_dagger : Matrix :math:`U_{C_+}` (TRS†), or None (absent) 

2982 particle_hole_dagger : Matrix :math:`U_{T_-}` (PHS†), or None (absent) 

2983 sublattice : Matrix :math:`U_S` (SLS), or None (absent) 

2984 pseudo_hermiticity : Matrix :math:`U_\eta` (pH), or None (absent) 

2985 canonical : Boolean, default False 

2986 Return the representative of 

2987 the 38 classes instead (AZ names before AZ† ones, η classes as 

2988 their equivalents without η). 

2989 nk : Positive integer, default 5 

2990 See *symmetry_error*. 

2991 tol : Positive real, default 1e-8 

2992 Tolerance of the checks. 

2993 

2994 Returns 

2995 ------- 

2996 name : String, for instance 'A', 'AI†', 'AIII + S-', 'BDI + S+-' or 'C + η+' 

2997 

2998 Notes 

2999 ----- 

3000 For a Hermitian model :math:`H^* = H^T` and :math:`H^\dagger = H`, 

3001 so TRS is also TRS†, PHS also PHS†, CS also SLS, and :math:`\eta = 1` 

3002 is a pseudo-Hermiticity: a Hermitian class BDI is 'BDI + S++' once 

3003 all of them are given. 

3004 ''' 

3005 error_handling.positive_real(tol, 'tol') 

3006 error_handling.boolean(canonical, 'canonical') 

3007 k0 = np.zeros(self.dim) 

3008 given = {} 

3009 ops = (time_reversal, particle_hole, chiral, time_reversal_dagger, particle_hole_dagger, 

3010 sublattice, pseudo_hermiticity) 

3011 for op, kind in zip(ops, _NH_KINDS): 

3012 if op is not None: 

3013 conj, transpose, minus = kind 

3014 k_map = 'minus' if conj != transpose else 'identity' 

3015 err = self.symmetry_error(op, k_map, bool(conj), bool(minus), nk, transpose=bool(transpose)) 

3016 error_handling.is_symmetry(err, tol) 

3017 given[kind] = self._operator(op, k0) 

3018 group = self._symmetry_group(given, tol) 

3019 squares = {} 

3020 for kind, u in group.items(): 

3021 if kind[0] != kind[1]: 

3022 square = u @ u.conj() 

3023 error_handling.square(square, tol) 

3024 squares[kind] = int(np.sign(square[0, 0].real)) 

3025 t_p, c_m, c_p, t_m = (squares.get(kind, 0) for kind in ((1, 0, 0), (0, 1, 1), (0, 1, 0), (1, 0, 1))) 

3026 az = {(1, 1): 'BDI', (-1, 1): 'DIII', (-1, -1): 'CII', (1, -1): 'CI', (1, 0): 'AI', 

3027 (-1, 0): 'AII', (0, 1): 'D', (0, -1): 'C'} 

3028 

3029 def sign(eps): 

3030 return '+' if eps > 0 else '-' 

3031 

3032 if (0, 0, 1) in group: # SLS 

3033 if not squares: 

3034 if (1, 1, 1) not in group: 

3035 name = 'A + S' 

3036 else: 

3037 u_g, u_s = group[(1, 1, 1)], group[(0, 0, 1)] 

3038 eps = np.trace((u_s @ u_g).conj().T @ u_g @ u_s).real 

3039 name = 'AIII + S' + sign(eps) 

3040 elif not (c_p or c_m): 

3041 name = az[(t_p, 0)] + ' + S' + sign(t_p * t_m) 

3042 elif not (t_p or t_m): 

3043 name = az[(0, c_m)] + ' + S' + sign(c_p * c_m) 

3044 else: 

3045 name = az[(t_p, c_m)] + ' + S' + sign(t_p * t_m) + sign(c_p * c_m) 

3046 elif (1, 1, 0) in group: # pseudo-Hermiticity 

3047 if not squares: 

3048 name = 'A + η' 

3049 elif t_p: 

3050 name = az[(t_p, 0)] + ' + η' + sign(t_p * c_p) 

3051 else: 

3052 name = az[(0, c_m)] + ' + η' + sign(t_m * c_m) 

3053 elif t_p or c_m: 

3054 name = az[(t_p, c_m)] 

3055 elif c_p or t_m: 

3056 name = az[(c_p, t_m)] + '†' 

3057 else: 

3058 name = 'AIII' if (1, 1, 1) in group else 'A' 

3059 return _NH_CANONICAL.get(name, name) if canonical else name 

3060 

3061 def _symmetry_group(self, given: dict, tol: float) -> dict: 

3062 ''' 

3063 Private method. Close the symmetry operators *given* ({kind: matrix}, 

3064 kind = (conjugation, transposition, minus sign), see *_NH_KINDS*) 

3065 under products, checking that two products of the same kind agree up 

3066 to a phase. Unitary kinds come back normalized to :math:`U^2 = 1`. 

3067 ''' 

3068 group = {(0, 0, 0): np.eye(self.norb, dtype='c16')} 

3069 todo = list(given.items()) 

3070 while todo: 

3071 kind, u = todo.pop() 

3072 if kind in group: 

3073 error_handling.symmetry_algebra(group[kind], u, tol) 

3074 continue 

3075 group[kind] = u 

3076 for other, v in list(group.items()): 

3077 product = tuple(a ^ b for a, b in zip(kind, other)) 

3078 # first one, then the other: U_2 f(U_1), f = conjugation for the antiunitary kinds 

3079 todo.append((product, v @ (u.conj() if other[0] != other[1] else u))) 

3080 todo.append((product, u @ (v.conj() if kind[0] != kind[1] else v))) 

3081 for kind, u in group.items(): 

3082 if kind[0] == kind[1]: 

3083 group[kind] = u / np.sqrt((u @ u)[0, 0]) 

3084 return group 

3085 

3086 def quantum_geometric_tensor( 

3087 self, bands: int | list[int], k: ArrayLike, dk: float = 1e-4, positions: bool = True, 

3088 ) -> NDArray[np.complex128]: 

3089 r''' 

3090 Get the quantum geometric tensor of a group of bands at :math:`\mathbf{k}`, 

3091 

3092 .. math:: 

3093 

3094 Q_{\mu\nu} = \mathrm{Tr}\left[P\,\partial_\mu P\,\partial_\nu P\right] 

3095 = \sum_{n\in\mathrm{bands}}\langle\partial_\mu u_n|(1-P)|\partial_\nu u_n\rangle\, , 

3096 

3097 with :math:`P(\mathbf{k})` the projector on the bands (gauge 

3098 invariant, so degeneracies within the group are harmless), from 

3099 central differences. Many k-points are evaluated at once, with one 

3100 batched diagonalization (see *set_workers*). Its real 

3101 part is the quantum metric :math:`g_{\mu\nu}` (Provost and Vallee, 

3102 1980), minus twice its imaginary part the Berry curvature 

3103 :math:`\Omega_{\mu\nu} = -2\,\mathrm{Im}\,Q_{\mu\nu}` (the convention of 

3104 *berry_curvature*, per unit k-area here). They obey 

3105 :math:`\sqrt{\det g} \ge |\Omega_{xy}|/2` in 2D. 

3106 

3107 Parameters 

3108 ---------- 

3109 bands : Band index, or list of band indices 

3110 k : k point (see *get_ham*), or real array of shape (nk, dim) 

3111 One or many k-points. 

3112 dk : Positive real, default 1e-4 

3113 Finite-difference step. 

3114 positions : Boolean, default True 

3115 Include the orbital 

3116 positions in the Bloch functions. The local metric and curvature 

3117 depend on this choice (the Berry curvature of *berry_curvature* is 

3118 that of ``positions=False``); integrals such as the Chern number 

3119 do not. 

3120 

3121 Returns 

3122 ------- 

3123 Q : Complex ndarray, shape (dim, dim), in the k coordinates of *get_ham* 

3124 Shape (nk, dim, dim) for many k-points. 

3125 ''' 

3126 if isinstance(bands, int): 

3127 bands = [bands] 

3128 error_handling.band_indices(bands, self.norb) 

3129 error_handling.k_points(k, 'k', self.dim) 

3130 error_handling.positive_real(dk, 'dk') 

3131 error_handling.boolean(positions, 'positions') 

3132 k = np.asarray(k, dtype='f8') 

3133 ks = np.atleast_2d(k) 

3134 # k, then k + dk e_mu and k - dk e_mu for every mu 

3135 shifts = np.concatenate([np.zeros((1, self.dim)), dk * np.eye(self.dim), -dk * np.eye(self.dim)]) 

3136 kk = (ks[:, None, :] + shifts[None]).reshape(-1, self.dim) 

3137 v = self._subspaces(kk, bands) 

3138 if positions: 

3139 v = np.exp(-1j * (kk @ self.k_basis.T) @ self.orbital_positions().T)[:, :, None] * v 

3140 proj = (v @ v.conj().transpose(0, 2, 1)).reshape(len(ks), 1 + 2 * self.dim, self.norb, self.norb) 

3141 dp = (proj[:, 1:1 + self.dim] - proj[:, 1 + self.dim:]) / (2 * dk) 

3142 q = np.einsum('kab,kmbc,knca->kmn', proj[:, 0], dp, dp) 

3143 return q if k.ndim == 2 else q[0] 

3144 

3145 def surface_spectral_function( 

3146 self, ks: ArrayLike, energies: ArrayLike, direction: int, side: int = 1, 

3147 eta: float = 1e-2, bulk: bool = False, max_iter: int = 10000, 

3148 ) -> NDArray[np.float64]: 

3149 r''' 

3150 Get the spectral function of the surface of a semi-infinite crystal, 

3151 

3152 .. math:: 

3153 

3154 A_s(\mathbf{k}_\parallel, E) = -\frac{1}{\pi}\, 

3155 \mathrm{Im}\,\mathrm{Tr}\, G_{00}(\mathbf{k}_\parallel, E + i\eta)\, , 

3156 

3157 with :math:`G_{00}` the Green's function of the outermost unit cell: 

3158 the crystal fills the cells :math:`n\,\mathbf{a}_d`, 

3159 :math:`n = 0, 1, 2, \dots` along the primitive vector 

3160 :math:`\mathbf{a}_d` (``lat.prim_vec[direction]``), times *side*, and 

3161 is periodic along the others. It comes from the Lopez Sancho-Rubio 

3162 decimation (see *transport.surface_green*) of the chain of principal 

3163 layers (as many cells as the longest hopping along 

3164 :math:`\mathbf{a}_d` spans), so the crystal is truly semi-infinite: no 

3165 finite slab, and no states of the opposite surface. It is the 

3166 tight-binding picture of an ARPES map -- surface states (edge states, 

3167 in 2D; the end state of a 1D chain) show up as sharp lines inside the 

3168 bulk gaps. With *bulk*, the same quantity for a unit cell deep inside 

3169 the crystal: the bulk bands projected on the surface Brillouin zone. 

3170 

3171 Parameters 

3172 ---------- 

3173 ks : Real array, shape (nk, dim) 

3174 k-points, in the coordinates 

3175 of *get_ham* (e.g. from a *k_path* of the surface Brillouin zone). 

3176 Only :math:`\mathbf{k}_\parallel` matters: the result does not 

3177 depend on the component along the reciprocal vector 

3178 :math:`\mathbf{b}_d`. 

3179 energies : Real number or real array 

3180 Energies :math:`E`. 

3181 direction : Integer, between 0 and dim-1 

3182 The primitive vector 

3183 normal to the surface (the one along which the crystal is 

3184 semi-infinite). 

3185 side : +1 or -1, default 1 

3186 The crystal extends along 

3187 :math:`+\mathbf{a}_d` (the surface faces :math:`-\mathbf{a}_d`) or 

3188 along :math:`-\mathbf{a}_d` (the opposite surface). 

3189 eta : Positive real, default 1e-2 

3190 Broadening: the 

3191 width of the lines, and the decay length (about :math:`v/\eta` 

3192 cells) over which the bulk is resolved. 

3193 bulk : Boolean, default False 

3194 If True, the spectral 

3195 function of a bulk unit cell instead of the surface one. 

3196 max_iter : Positive integer, default 10000 

3197 Maximum 

3198 number of decimation steps. 

3199 

3200 Returns 

3201 ------- 

3202 A : Real ndarray, shape (nk, len(energies)) 

3203 

3204 Examples 

3205 -------- 

3206 Example usage:: 

3207 

3208 # the (001) surface of a 3D model, along a path of the surface zone 

3209 A = ks.surface_spectral_function(k_path, np.linspace(-1, 1, 201), direction=2) 

3210 ''' 

3211 error_handling.no_overlap(self._overlap_hop) 

3212 ks = np.atleast_2d(np.asarray(ks, dtype='f8')) 

3213 error_handling.ks(ks, self.dim) 

3214 error_handling.frequencies(energies, 'energies') 

3215 energies = np.atleast_1d(np.asarray(energies, dtype='f8')) 

3216 error_handling.direction(direction, self.dim) 

3217 error_handling.surface_side(side) 

3218 error_handling.positive_real(eta, 'eta') 

3219 error_handling.boolean(bulk, 'bulk') 

3220 error_handling.positive_int(max_iter, 'max_iter') 

3221 # H_m(k): the hoppings from a cell to the cell m a_d away, with the 

3222 # phase of their in-plane part only (independent of k along b_d) 

3223 a_dir = np.array(self.lat.prim_vec[direction], dtype='f8') 

3224 layers: dict[int, list] = {} 

3225 for (i, j, cell, t), (_, _, R, _) in zip(self._hop_cells(), self._hop): 

3226 layers.setdefault(cell[direction], []).append((i, j, R - cell[direction] * a_dir, t)) 

3227 k_cart = ks @ self.k_basis.T 

3228 blocks: dict[int, NDArray] = {m: self._bloch_sum(hops, k_cart)[0] for m, hops in layers.items()} 

3229 blocks[0] = blocks.get(0, 0.) + (np.diag(self.onsite) + self._onsite_offdiag)[None] 

3230 width = max([abs(m) for m in layers] + [1]) 

3231 norb, nk = self.norb, len(ks) 

3232 

3233 def layer(shift): 

3234 # block (p, q) couples cell p of a principal layer to cell q of 

3235 # the layer *shift* cells further into the crystal 

3236 out = np.zeros((nk, width, norb, width, norb), 'c16') 

3237 for p in range(width): 

3238 for q in range(width): 

3239 m = side * (q - p + shift) 

3240 if m in blocks: 

3241 out[:, p, :, q, :] = blocks[m] 

3242 return out.reshape(nk, width * norb, width * norb) 

3243 h0, alpha, beta = layer(0), layer(width), layer(-width) 

3244 # one task per k-point and chunk of energies, enough tasks for every thread 

3245 n_e = max(1, min(len(energies), -(-self.workers // nk))) 

3246 tasks = [(n, part) for n in range(nk) for part in np.array_split(np.arange(len(energies)), n_e)] 

3247 

3248 def task(n_part): 

3249 n, part = n_part 

3250 gs, gb = _green_surface_bulk(energies[part] + 1j * eta, h0[n], alpha[n], beta[n], 1e-12, 

3251 max_iter, 'surface_spectral_function') 

3252 g = gb if bulk else gs 

3253 return -np.trace(g[:, :norb, :norb], axis1=1, axis2=2).imag / PI 

3254 if self.workers > 1 and len(tasks) > 1: 

3255 with ThreadPoolExecutor(min(self.workers, len(tasks))) as pool: 

3256 out = list(pool.map(task, tasks)) 

3257 else: 

3258 out = [task(t) for t in tasks] 

3259 spec = np.empty((nk, len(energies))) 

3260 for (n, part), a in zip(tasks, out): 

3261 spec[n, part] = a 

3262 return spec 

3263 

3264 # ------------------------------------------------------------------ 

3265 # Projections, constant-energy contours and plot helpers 

3266 # ------------------------------------------------------------------ 

3267 

3268 def spin_operator(self, axis: str) -> NDArray[np.complex128]: 

3269 r''' 

3270 Get the Pauli matrix :math:`\sigma_{axis}` acting on the spin of 

3271 every site, :math:`\mathbb{1}_{sites}\otimes\sigma_{axis}`, for a model 

3272 built with ``spin=True`` (for *band_weights* and *spin_texture*). 

3273 

3274 Parameters 

3275 ---------- 

3276 axis : 'x', 'y' or 'z' 

3277 

3278 Returns 

3279 ------- 

3280 op : Complex ndarray, shape (norb, norb) 

3281 ''' 

3282 error_handling.spinful(self.spin) 

3283 error_handling.spin_axis(axis) 

3284 return np.kron(np.eye(self.n_sites), PAULI[axis]) 

3285 

3286 def _projector(self, projector) -> NDArray[np.complex128]: 

3287 ''' 

3288 Private method. The (norb, norb) operator of *band_weights* from an 

3289 orbital index, a list of them, a sublattice tag or a matrix. 

3290 ''' 

3291 error_handling.projector(projector, self.norb, self.tags) 

3292 if isinstance(projector, np.ndarray): 

3293 return projector.astype('c16') 

3294 if isinstance(projector, str): 

3295 sites = np.nonzero(self.tags == projector)[0] 

3296 projector = np.concatenate([2 * sites, 2 * sites + 1]) if self.spin else sites 

3297 diag = np.zeros(self.norb) 

3298 diag[projector] = 1. 

3299 return np.diag(diag).astype('c16') 

3300 

3301 def _expectations(self, ks: NDArray[np.float64], ops: list) -> NDArray[np.float64]: 

3302 r''' 

3303 Private method. :math:`\mathrm{Re}\langle u_n(\mathbf{k})|O|u_n(\mathbf{k})\rangle 

3304 /\langle u_n|u_n\rangle` for each operator of *ops*, band and k-point: 

3305 shape (len(ops), nk, norb), from one diagonalization. Within a group 

3306 of degenerate bands (energies within 1e-8 of each other, relative to 

3307 the largest one), each band gets the average over the group: the 

3308 trace of O over the degenerate subspace divided by its dimension, 

3309 which does not depend on the eigenvectors the solver returns. 

3310 ''' 

3311 en, vn = self._eigs(ks, eigenvec=True) 

3312 norm = np.sum(np.abs(vn) ** 2, axis=1) 

3313 values = np.array([np.einsum('kin,ij,kjn->kn', vn.conj(), op, vn).real / norm for op in ops]) 

3314 # label the degenerate groups (consecutive bands, sorted by energy) 

3315 tol = 1e-8 * max(1., np.abs(en).max()) 

3316 new_group = np.concatenate([np.ones((len(en), 1), bool), 

3317 np.abs(np.diff(en, axis=1)) > tol], axis=1) 

3318 groups = np.cumsum(new_group.ravel()) - 1 

3319 counts = np.bincount(groups) 

3320 return np.array([(np.bincount(groups, v.ravel()) / counts)[groups].reshape(v.shape) 

3321 for v in values]) 

3322 

3323 def band_weights(self, projector, ks: ArrayLike | None = None) -> NDArray[np.float64]: 

3324 r''' 

3325 Get the weight of each band on a set of orbitals, or the expectation 

3326 value of an operator, for "fat band" plots (see *plot_bands*): 

3327 

3328 .. math:: 

3329 

3330 w_n(\mathbf{k}) = \langle u_n(\mathbf{k})|P|u_n(\mathbf{k})\rangle\, , 

3331 

3332 with :math:`P` the projector on the chosen orbitals (a weight 

3333 between 0 and 1; the weights on all the sublattices add up to 1), or 

3334 any Hermitian operator, e.g. ``ks.spin_operator('z')`` for the spin 

3335 polarization (between -1 and 1). Within a group of degenerate bands 

3336 (e.g. Kramers pairs with inversion and time reversal), the weight of 

3337 a single eigenvector depends on the choice of eigenvectors; each band 

3338 of the group gets instead the average over the group, which does not 

3339 (a Kramers pair has zero spin polarization). 

3340 For a non-Hermitian model, the right eigenvectors are used; with an 

3341 overlap (*set_overlap*), :math:`v^\dagger Pv/v^\dagger v`. 

3342 

3343 Parameters 

3344 ---------- 

3345 projector 

3346 Orbital index, list of orbital indices, sublattice 

3347 tag (all the orbitals, both spins, of the sites with that tag), 

3348 or a (norb, norb) Hermitian matrix. 

3349 ks : Real array, shape (nk, dim) 

3350 Default value None: the 

3351 k-points of the last *get_bands* or *k_path*. 

3352 

3353 Returns 

3354 ------- 

3355 weights : Real ndarray, shape (nk, norb), ordered as the bands 

3356 

3357 Examples 

3358 -------- 

3359 Example usage:: 

3360 

3361 ks.k_path(points, nk=100) 

3362 ks.plot_bands(weights=ks.band_weights('a')) 

3363 ''' 

3364 op = self._projector(projector) 

3365 if ks is None: 

3366 error_handling.empty_ndarray(self.en, 'get_bands or k_path') 

3367 ks = self.ks 

3368 ks = np.atleast_2d(np.asarray(ks, dtype='f8')) 

3369 error_handling.ks(ks, self.dim) 

3370 return self._expectations(ks, [op])[0] 

3371 

3372 def spin_texture(self, ks: ArrayLike, band: int) -> NDArray[np.float64]: 

3373 r''' 

3374 Get the spin expectation values 

3375 :math:`\langle\boldsymbol\sigma\rangle_n(\mathbf{k}) = 

3376 \langle u_n|\boldsymbol\sigma|u_n\rangle` of a band, for a model built 

3377 with ``spin=True``. Their length is 1 if the band is fully polarized, 

3378 less if spin-orbit coupling entangles the spin with the orbitals. 

3379 Within a group of degenerate bands, each band gets the average spin 

3380 of the group (see *band_weights*): zero for a Kramers pair, with both 

3381 inversion and time reversal. 

3382 

3383 Parameters 

3384 ---------- 

3385 ks : Real array, shape (nk, dim) 

3386 k-points. 

3387 band : Integer 

3388 Band index. 

3389 

3390 Returns 

3391 ------- 

3392 spin 

3393 Real ndarray, shape (nk, 3): 

3394 :math:`(\langle\sigma_x\rangle, \langle\sigma_y\rangle, \langle\sigma_z\rangle)`. 

3395 ''' 

3396 error_handling.spinful(self.spin) 

3397 error_handling.band_index(band, self.norb) 

3398 ks = np.atleast_2d(np.asarray(ks, dtype='f8')) 

3399 error_handling.ks(ks, self.dim) 

3400 ops = [self.spin_operator(a) for a in 'xyz'] 

3401 return self._expectations(ks, ops)[:, :, band].T 

3402 

3403 def _fold(self, elements: NDArray[np.float64]) -> NDArray[np.float64]: 

3404 ''' 

3405 Private method. Bring segments or triangles (fractional coordinates, 

3406 shape (n, nv, dim)) into the first Brillouin zone (the Wigner-Seitz 

3407 cell of the reciprocal lattice, by their centres), in the k 

3408 coordinates of *get_ham*. 

3409 ''' 

3410 elements = elements - np.floor(elements.mean(axis=1) + 0.5)[:, None, :] 

3411 cart = elements @ self.rec_vec_k 

3412 shifts = np.array(list(product((-1, 0, 1), repeat=self.dim))) @ self.rec_vec_k 

3413 dist = np.linalg.norm(cart.mean(axis=1)[:, None, :] - shifts[None], axis=2) 

3414 return cart - shifts[np.argmin(dist, axis=1)][:, None, :] 

3415 

3416 def fermi_surface( 

3417 self, energy: float = 0., nk: int | tuple[int, ...] | None = None, 

3418 bands: int | list[int] | None = None, 

3419 ) -> list[NDArray[np.float64]]: 

3420 r''' 

3421 Get the constant-energy surfaces :math:`E_n(\mathbf{k}) = E` (the 

3422 Fermi surface at :math:`E = E_F`): lines in a 2D Brillouin zone, 

3423 surfaces in 3D. The bands are interpolated linearly in the triangles 

3424 (tetrahedra in 3D) of a uniform mesh, the same decomposition as 

3425 *dos.tetrahedron_dos*, and the level set of each simplex is a segment 

3426 (a triangle or a quadrilateral, split in two, in 3D): marching 

3427 triangles and tetrahedra, exact for the interpolated bands. Each 

3428 piece is brought into the first Brillouin zone, the Wigner-Seitz 

3429 cell around :math:`\Gamma`. For a non-Hermitian model, the real part 

3430 of the energies is used. 

3431 

3432 Parameters 

3433 ---------- 

3434 energy : Real number, default 0 

3435 The energy :math:`E`. 

3436 nk : Positive integer, or tuple of *dim* positive integers, default None: 100 in 2D, 40 in 3D 

3437 Number of k-points along 

3438 each reciprocal lattice vector. 

3439 bands : Band index, or list of band indices 

3440 Default value 

3441 None: all bands. 

3442 

3443 Returns 

3444 ------- 

3445 surfaces 

3446 List, one entry per band of *bands*: a real 

3447 ndarray of shape (n, 2, 2) in 2D (n segments, each two k-points) 

3448 or (n, 3, 3) in 3D (n triangles, each three k-points), in the 

3449 k coordinates of *get_ham*. Empty (n = 0) if the band does not 

3450 cross *energy*. 

3451 

3452 Examples 

3453 -------- 

3454 Example usage:: 

3455 

3456 # the Fermi surface of the half-filled square lattice 

3457 segments = sq.fermi_surface(0.)[0] 

3458 ''' 

3459 error_handling.dim_min(self.dim, 2) 

3460 error_handling.real_number(energy, 'energy') 

3461 if nk is None: 

3462 nk = 100 if self.dim == 2 else 40 

3463 error_handling.nk(nk, self.dim) 

3464 if isinstance(nk, int): 

3465 nk = (nk,) * self.dim 

3466 if bands is None: 

3467 bands = list(range(self.norb)) 

3468 elif isinstance(bands, int): 

3469 bands = [bands] 

3470 error_handling.band_indices(bands, self.norb) 

3471 _, ks = self.mesh_grid(nk) 

3472 en = self._eigs(ks).real.reshape(*nk, self.norb) 

3473 return [self._fold(_level_set(en[..., n], energy)) for n in bands] 

3474 

3475 def _k_labels(self) -> list[str]: 

3476 ''' 

3477 Private method. Axis labels of the k coordinates of *get_ham*: 

3478 '$k_x$' for a coordinate along the Cartesian x axis (the slab of a 

3479 3D model along x and z gives '$k_x$', '$k_z$'), '$k_1$', ... otherwise. 

3480 ''' 

3481 labels = [] 

3482 for n, col in enumerate(self.k_basis.T): 

3483 axis = np.nonzero(np.isclose(np.abs(col), 1.))[0] 

3484 labels.append('$k_{}$'.format('xyz'[axis[0]] if len(axis) else n + 1)) 

3485 return labels 

3486 

3487 def _zone_axes(self, ax, fs: float) -> None: 

3488 ''' 

3489 Private method. Draw the first Brillouin zone of a 2D model on *ax*, 

3490 and label and scale its axes. 

3491 ''' 

3492 zone = _zone_polygon(self.rec_vec_k) 

3493 ax.plot(*np.vstack([zone, zone[:1]]).T, 'k', lw=1) 

3494 ax.set_aspect('equal') 

3495 pad = 0.05 * np.ptp(zone, axis=0).max() 

3496 ax.set_xlim(zone[:, 0].min() - pad, zone[:, 0].max() + pad) 

3497 ax.set_ylim(zone[:, 1].min() - pad, zone[:, 1].max() + pad) 

3498 ax.set_xlabel(self._k_labels()[0], fontsize=fs) 

3499 ax.set_ylabel(self._k_labels()[1], fontsize=fs) 

3500 

3501 def plot_fermi_surface( 

3502 self, energy: float = 0., nk: int | tuple[int, ...] | None = None, 

3503 bands: int | list[int] | None = None, lw: float = 2., alpha: float = 0.6, 

3504 fs: float = 20, figsize: tuple[float, float] | None = None, 

3505 ) -> Figure: 

3506 ''' 

3507 Plot the constant-energy contours of *fermi_surface* (one color per 

3508 band, with a legend if several bands cross the energy): lines inside 

3509 the first Brillouin zone (drawn in black) in 2D, surfaces in 3D. 

3510 

3511 Parameters 

3512 ---------- 

3513 energy : Real number, default 0 

3514 The energy. 

3515 nk 

3516 See *fermi_surface*. 

3517 bands 

3518 See *fermi_surface*. 

3519 lw : Positive number, default 2 

3520 Linewidth (2D). 

3521 alpha : Real number in (0, 1], default 0.6 

3522 Opacity of 

3523 the surfaces (3D). 

3524 fs : Positive number, default 20 

3525 Fontsize. 

3526 figsize : Tuple, default None 

3527 Figure size. 

3528 

3529 Returns 

3530 ------- 

3531 fig : Figure 

3532 ''' 

3533 error_handling.positive_real(lw, 'lw') 

3534 error_handling.positive_real(alpha, 'alpha') 

3535 error_handling.positive_real(fs, 'fs') 

3536 error_handling.tuple_2elem(figsize, 'figsize') 

3537 surfaces = self.fermi_surface(energy, nk, bands) 

3538 bands = list(range(self.norb)) if bands is None else np.atleast_1d(bands).tolist() 

3539 fig = plt.figure(figsize=figsize) 

3540 if self.dim == 2: 

3541 ax = fig.add_subplot() 

3542 for n, (band, segs) in enumerate(zip(bands, surfaces)): 

3543 if len(segs): 

3544 ax.add_collection(LineCollection(list(segs), colors='C{}'.format(n % 10), lw=lw, 

3545 label='band {}'.format(band))) 

3546 self._zone_axes(ax, fs) 

3547 else: 

3548 ax = fig.add_subplot(projection='3d') 

3549 for n, (band, tris) in enumerate(zip(bands, surfaces)): 

3550 if len(tris): 

3551 ax.add_collection3d(Poly3DCollection(tris, facecolor='C{}'.format(n % 10), 

3552 alpha=min(alpha, 1.), edgecolor='none', 

3553 label='band {}'.format(band))) 

3554 extent = np.abs(np.concatenate([t.reshape(-1, 3) for t in surfaces] + 

3555 [self.rec_vec_k / 2])).max() 

3556 ax.set(xlim=(-extent, extent), ylim=(-extent, extent), zlim=(-extent, extent)) 

3557 ax.set_box_aspect((1, 1, 1)) 

3558 ax.set_xlabel(self._k_labels()[0], fontsize=fs) 

3559 ax.set_ylabel(self._k_labels()[1], fontsize=fs) 

3560 ax.set_zlabel(self._k_labels()[2], fontsize=fs) 

3561 if sum(len(s) > 0 for s in surfaces) > 1: 

3562 ax.legend(fontsize=0.6 * fs) 

3563 ax.set_title('$E = {:g}$'.format(energy), fontsize=fs) 

3564 plt.draw() 

3565 return fig 

3566 

3567 def plot_spin_texture( 

3568 self, band: int, energy: float = 0., nk: int = 100, n_arrows: int = 40, 

3569 cmap: str = 'RdBu_r', lw: float = 1., fs: float = 20, 

3570 figsize: tuple[float, float] | None = None, 

3571 ) -> Figure: 

3572 r''' 

3573 Plot the spin texture of a band of a 2D spinful model on its 

3574 constant-energy contour (see *fermi_surface*), zoomed on the 

3575 contour: arrows of 

3576 :math:`(\langle\sigma_x\rangle, \langle\sigma_y\rangle)` along the 

3577 contour, colored by :math:`\langle\sigma_z\rangle` (see *spin_texture*). 

3578 Rashba coupling, for instance, locks the spin perpendicular to 

3579 :math:`\mathbf{k}`, winding once around the contour. 

3580 

3581 Parameters 

3582 ---------- 

3583 band : Integer 

3584 Band index. 

3585 energy : Real number, default 0 

3586 Energy of the contour. 

3587 nk : Positive integer, default 100 

3588 Mesh of the contour. 

3589 n_arrows : Positive integer, default 40 

3590 Approximate 

3591 number of arrows, evenly spaced along the contour. A fully 

3592 polarized in-plane spin is drawn 80% as long as their spacing. 

3593 cmap 

3594 Default value 'RdBu_r'. Colormap of :math:`\langle\sigma_z\rangle`. 

3595 lw : Positive number, default 1 

3596 Linewidth of the contour. 

3597 fs : Positive number, default 20 

3598 Fontsize. 

3599 figsize : Tuple, default None 

3600 Figure size. 

3601 

3602 Returns 

3603 ------- 

3604 fig : Figure 

3605 ''' 

3606 error_handling.dim_2(self.dim) 

3607 error_handling.spinful(self.spin) 

3608 error_handling.band_index(band, self.norb) 

3609 error_handling.positive_int(nk, 'nk') 

3610 error_handling.positive_int(n_arrows, 'n_arrows') 

3611 error_handling.positive_real(lw, 'lw') 

3612 error_handling.positive_real(fs, 'fs') 

3613 error_handling.tuple_2elem(figsize, 'figsize') 

3614 segs = self.fermi_surface(energy, nk, [band])[0] 

3615 error_handling.contour_found(len(segs), energy) 

3616 # arrows at segment midpoints, at least one contour length / n_arrows apart 

3617 mids = segs.mean(axis=1) 

3618 spacing = np.linalg.norm(segs[:, 1] - segs[:, 0], axis=1).sum() / n_arrows 

3619 keep = [0] 

3620 for i in range(1, len(mids)): 

3621 if np.min(np.linalg.norm(mids[keep] - mids[i], axis=1)) >= spacing: 

3622 keep.append(i) 

3623 spin = self.spin_texture(mids[keep], band) 

3624 fig, ax = plt.subplots(figsize=figsize) 

3625 ax.add_collection(LineCollection(list(segs), colors='0.6', lw=lw)) 

3626 arrows = ax.quiver(mids[keep, 0], mids[keep, 1], spin[:, 0], spin[:, 1], spin[:, 2], 

3627 cmap=cmap, norm=Normalize(-1., 1.), pivot='mid', edgecolor='k', 

3628 linewidth=0.5, angles='xy', scale_units='xy', scale=1.25 / spacing) 

3629 fig.colorbar(arrows, ax=ax).set_label(r'$\langle\sigma_z\rangle$', fontsize=fs) 

3630 self._zone_axes(ax, fs) 

3631 # zoom on the contour (within the zone) 

3632 low, high = segs.reshape(-1, 2).min(axis=0), segs.reshape(-1, 2).max(axis=0) 

3633 pad = 0.1 * (high - low).max() + spacing 

3634 ax.set_xlim(max(ax.get_xlim()[0], low[0] - pad), min(ax.get_xlim()[1], high[0] + pad)) 

3635 ax.set_ylim(max(ax.get_ylim()[0], low[1] - pad), min(ax.get_ylim()[1], high[1] + pad)) 

3636 ax.set_title('Spin texture of band {}, $E = {:g}$'.format(band, energy), fontsize=fs) 

3637 plt.draw() 

3638 return fig 

3639 

3640 def plot_berry_curvature( 

3641 self, bands: int | list[int], nk: int | tuple[int, int] = 60, 

3642 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., cmap: str = 'RdBu_r', 

3643 fs: float = 20, figsize: tuple[float, float] | None = None, 

3644 ) -> Figure: 

3645 r''' 

3646 Plot the Berry curvature of *berry_curvature* as a density, 

3647 :math:`\Omega(\mathbf{k})` = flux of each plaquette / its area. In 

3648 2D, the plaquettes are brought into the first Brillouin zone (drawn 

3649 in black), in the k coordinates of *get_ham*. In 3D, they fill the 

3650 parallelogram spanned by the two reciprocal vectors of *plane*, in 

3651 orthonormal coordinates of the plane (:math:`k_1` along 

3652 :math:`\mathbf{b}_i`). The title gives the Chern number, the total 

3653 flux over :math:`2\pi`. 

3654 

3655 Parameters 

3656 ---------- 

3657 bands 

3658 See *berry_curvature*. 

3659 nk 

3660 Default value 60. See *berry_curvature*. 

3661 plane 

3662 See *berry_curvature*. 

3663 k_fixed 

3664 See *berry_curvature*. 

3665 cmap 

3666 Default value 'RdBu_r'. Colormap, centred on zero. 

3667 fs : Positive number, default 20 

3668 Fontsize. 

3669 figsize : Tuple, default None 

3670 Figure size. 

3671 

3672 Returns 

3673 ------- 

3674 fig : Figure 

3675 ''' 

3676 error_handling.positive_real(fs, 'fs') 

3677 error_handling.tuple_2elem(figsize, 'figsize') 

3678 curv = self.berry_curvature(bands, nk, plane, k_fixed) 

3679 n1, n2 = curv.shape 

3680 # the corners of each plaquette, counterclockwise, in fractional coordinates 

3681 f1, f2 = np.meshgrid(np.arange(n1) / n1, np.arange(n2) / n2, indexing='ij') 

3682 corner = np.stack([f1.ravel(), f2.ravel()], axis=-1)[:, None, :] 

3683 quads = corner + np.array([[0., 0.], [1. / n1, 0.], [1. / n1, 1. / n2], [0., 1. / n2]]) 

3684 b_i, b_j = self.rec_vec_k[plane[0]], self.rec_vec_k[plane[1]] 

3685 if self.dim == 2: 

3686 quads = self._fold(quads) 

3687 else: 

3688 # orthonormal coordinates of the plane, the first along b_i 

3689 u = b_i / np.linalg.norm(b_i) 

3690 v = b_j - (b_j @ u) * u 

3691 b_i, b_j = np.array([b_i @ u, 0.]), np.array([b_j @ u, b_j @ v / np.linalg.norm(v)]) 

3692 quads = quads @ np.array([b_i, b_j]) 

3693 area = abs(b_i[0] * b_j[1] - b_i[1] * b_j[0]) / (n1 * n2) 

3694 omega = curv.ravel() / area 

3695 vmax = np.abs(omega).max() or 1. 

3696 fig, ax = plt.subplots(figsize=figsize) 

3697 patches = PolyCollection(quads, array=omega, cmap=cmap, norm=Normalize(-vmax, vmax), 

3698 edgecolors='face', linewidths=0.2) 

3699 ax.add_collection(patches) 

3700 fig.colorbar(patches, ax=ax).set_label(r'$\Omega(\mathbf{k})$', fontsize=fs) 

3701 if self.dim == 2: 

3702 self._zone_axes(ax, fs) 

3703 else: 

3704 ax.autoscale_view() 

3705 ax.set_aspect('equal') 

3706 ax.set_xlabel('$k_1$', fontsize=fs) 

3707 ax.set_ylabel('$k_2$', fontsize=fs) 

3708 ax.set_title('Berry curvature, $C = {:.3f}$'.format(curv.sum() / (2 * PI)), fontsize=fs) 

3709 fig.set_layout_engine('tight') 

3710 plt.draw() 

3711 return fig 

3712 

3713 def plot_wannier_flow( 

3714 self, bands: int | list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

3715 flow: int | None = None, k_fixed: float = 0., positions: bool = True, 

3716 k_range: tuple[float, float] = (0., 1.), ms: float = 4., c: str = 'b', fs: float = 20, 

3717 figsize: tuple[float, float] | None = None, 

3718 ) -> Figure: 

3719 r''' 

3720 Plot the flow of the hybrid Wannier centres (the Wilson-loop 

3721 spectrum) of *wannier_flow*: the centres along *direction*, in 

3722 units of :math:`\mathbf{a}_{direction}`, as the loop moves along 

3723 the reciprocal vector *flow*. They wind :math:`C` times in a Chern 

3724 insulator, and the Kramers pairs swap partners in a 

3725 :math:`\mathbb{Z}_2` topological insulator. 

3726 

3727 Parameters 

3728 ---------- 

3729 bands 

3730 See *wannier_flow*. 

3731 nk 

3732 See *wannier_flow*. 

3733 nk_perp 

3734 See *wannier_flow*. 

3735 direction 

3736 See *wannier_flow*. 

3737 flow 

3738 See *wannier_flow*. 

3739 k_fixed 

3740 See *wannier_flow*. 

3741 positions 

3742 See *wannier_flow*. 

3743 k_range 

3744 See *wannier_flow*. 

3745 ms : Positive number, default 4 

3746 Marker size. 

3747 c 

3748 Default value 'b'. Marker color. 

3749 fs : Positive number, default 20 

3750 Fontsize. 

3751 figsize : Tuple, default None 

3752 Figure size. 

3753 

3754 Returns 

3755 ------- 

3756 fig : Figure 

3757 ''' 

3758 error_handling.positive_real(ms, 'ms') 

3759 error_handling.positive_real(fs, 'fs') 

3760 error_handling.tuple_2elem(figsize, 'figsize') 

3761 fracs, centers = self.wannier_flow(bands, nk, nk_perp, direction, flow, k_fixed, 

3762 positions, k_range) 

3763 if flow is None: 

3764 flow = [d for d in range(self.dim) if d != direction][0] 

3765 fig, ax = plt.subplots(figsize=figsize) 

3766 ax.plot(fracs, centers, 'o', c=c, ms=ms) 

3767 ax.set_xlim(fracs[0], fracs[-1]) 

3768 ax.set_ylim(0., 1.) 

3769 ax.set_xlabel(r'$k_{0}/|\mathbf{{b}}_{0}|$'.format(flow + 1), fontsize=fs) 

3770 ax.set_ylabel(r'Wannier centres ($\mathbf{{a}}_{}$)'.format(direction + 1), fontsize=fs) 

3771 fig.set_layout_engine('tight') 

3772 plt.draw() 

3773 return fig 

3774 

3775 def plot_surface_spectral_function( 

3776 self, points: list[ArrayLike], energies: ArrayLike, direction: int, nk: int = 60, 

3777 side: int = 1, eta: float = 1e-2, bulk: bool = False, 

3778 node_labels: list[str] | None = None, log: bool = True, cmap: str = 'magma', 

3779 fs: float = 20, figsize: tuple[float, float] | None = None, 

3780 ) -> Figure: 

3781 r''' 

3782 Plot the surface spectral function of *surface_spectral_function* 

3783 along a path through the surface Brillouin zone (built as in 

3784 *k_path*), as an ARPES-like map of :math:`E` against 

3785 :math:`\mathbf{k}_\parallel`. Surface states are the sharp lines in 

3786 the bulk gaps. 

3787 

3788 Parameters 

3789 ---------- 

3790 points : List of at least two k-points, the nodes of the path (see *k_path*) 

3791 energies : Real array 

3792 Energies :math:`E`. 

3793 direction 

3794 See *surface_spectral_function*. 

3795 nk : Positive integer, default 60 

3796 Number of k-points per path segment. 

3797 side 

3798 See *surface_spectral_function*. 

3799 eta 

3800 See *surface_spectral_function*. 

3801 bulk 

3802 See *surface_spectral_function*. 

3803 node_labels : List of strings, default None 

3804 Labels of *points*. 

3805 log : Boolean, default True 

3806 Logarithmic color scale, over 

3807 four decades below the maximum (the surface states are much 

3808 sharper than the bulk continuum). 

3809 cmap 

3810 Default value 'magma'. Colormap. 

3811 fs : Positive number, default 20 

3812 Fontsize. 

3813 figsize : Tuple, default None 

3814 Figure size. 

3815 

3816 Returns 

3817 ------- 

3818 fig : Figure 

3819 ''' 

3820 error_handling.k_path_points(points, self.dim) 

3821 error_handling.positive_int(nk, 'nk') 

3822 error_handling.boolean(log, 'log') 

3823 error_handling.positive_real(fs, 'fs') 

3824 error_handling.tuple_2elem(figsize, 'figsize') 

3825 if node_labels is not None: 

3826 error_handling.ndarray(np.array(node_labels), 'node_labels', len(points)) 

3827 ks, dist, nodes = _path(points, nk) 

3828 spec = self.surface_spectral_function(ks, energies, direction, side, eta, bulk) 

3829 energies = np.atleast_1d(np.asarray(energies, dtype='f8')) 

3830 vmax = spec.max() if spec.max() > 0 else 1. 

3831 norm = LogNorm(vmax * 1e-4, vmax) if log else Normalize(0., vmax) 

3832 fig, ax = plt.subplots(figsize=figsize) 

3833 mesh = ax.pcolormesh(dist, energies, np.maximum(spec, vmax * 1e-4).T, cmap=cmap, 

3834 norm=norm, shading='nearest') 

3835 fig.colorbar(mesh, ax=ax).set_label(r'$A(\mathbf{k}_\parallel, E)$', fontsize=fs) 

3836 for node in nodes: 

3837 ax.axvline(node, color='w', lw=0.5) 

3838 if node_labels is not None: 

3839 ax.set_xticks(nodes) 

3840 ax.set_xticklabels(node_labels, fontsize=fs) 

3841 ax.set_ylabel('$E$', fontsize=fs) 

3842 ax.set_title('Bulk' if bulk else 'Surface', fontsize=fs) 

3843 fig.set_layout_engine('tight') 

3844 plt.draw() 

3845 return fig 

3846 

3847 def plot_dos( 

3848 self, 

3849 nk: int | tuple[int, ...] = 30, 

3850 broadening: float = 0.05, 

3851 kernel: str = 'gaussian', 

3852 e_grid: ArrayLike | None = None, 

3853 fs: float = 20, 

3854 lw: float = 2., 

3855 figsize: tuple[float, float] | None = None, 

3856 ) -> Figure: 

3857 ''' 

3858 Plot the density of states, obtained by diagonalizing 

3859 :math:`H(\\mathbf{k})` over a uniform Brillouin-zone mesh: broadened 

3860 (see *tbkit.dos.density_of_states*), or by the linear tetrahedron 

3861 method (``kernel='tetrahedron'``, see *tbkit.dos.tetrahedron_dos*), 

3862 which needs no broadening and keeps band edges and van Hove 

3863 singularities sharp. 

3864 

3865 Parameters 

3866 ---------- 

3867 nk : Positive integer, or tuple of *dim* positive integers, default 30 

3868 Number of k-points along each reciprocal 

3869 lattice vector. 

3870 broadening : Positive real number, default 0.05 

3871 Kernel 

3872 width (unused by the tetrahedron method). 

3873 kernel : String, default 'gaussian' 

3874 'gaussian', 

3875 'lorentzian' or 'tetrahedron'. 

3876 e_grid : Real ndarray, default None 

3877 Energies at which to 

3878 evaluate the density of states. 

3879 fs : Positive number, default 20 

3880 Fontsize. 

3881 lw : Positive number, default 2 

3882 Linewidth. 

3883 figsize : Tuple, default None 

3884 Figure size. 

3885 

3886 Returns 

3887 ------- 

3888 fig : Figure 

3889 ''' 

3890 error_handling.positive_real(fs, 'fs') 

3891 error_handling.positive_real(lw, 'lw') 

3892 error_handling.tuple_2elem(figsize, 'figsize') 

3893 error_handling.kspace_dos_kernel(kernel) 

3894 en = self.mesh_bands(nk) 

3895 if kernel != 'tetrahedron': 

3896 return dos._plot_density_of_states(en, e_grid, broadening, kernel, fs, lw, figsize) 

3897 nk = (nk,) * self.dim if isinstance(nk, int) else nk 

3898 e_grid, rho = dos.tetrahedron_dos(en.reshape(*nk, self.norb), e_grid) 

3899 return dos._plot_dos_curve(e_grid, rho, fs, lw, figsize) 

3900 

3901 def plot_bands( 

3902 self, 

3903 node_labels: list[str] | None = None, 

3904 fs: float = 20, 

3905 lw: float = 2., 

3906 ms: float = 0., 

3907 c: str = 'b', 

3908 lims: tuple[float, float] | None = None, 

3909 figsize: tuple[float, float] | None = None, 

3910 weights: ArrayLike | None = None, 

3911 style: str = 'color', 

3912 cmap: str | None = None, 

3913 ) -> Figure: 

3914 ''' 

3915 Plot the band structure computed by *k_path* or *get_bands*, against 

3916 the cumulative distance along the k-points (vertical lines mark the 

3917 nodes of a *k_path*). For a non-Hermitian model, the real part of 

3918 the energies is plotted. With *weights* (e.g. from *band_weights*: 

3919 an orbital, sublattice or spin projection), a "fat band" plot: the 

3920 bands are colored by the weights (with a colorbar), or drawn with 

3921 dots sized by their absolute value. 

3922 

3923 Parameters 

3924 ---------- 

3925 node_labels : List of strings, default None 

3926 Labels of the 

3927 high-symmetry points passed to *k_path*. 

3928 fs : Positive number, default 20 

3929 Fontsize. 

3930 lw : Positive number, default 2 

3931 Linewidth. 

3932 ms : Positive number, default 0 

3933 Marker size. 

3934 c 

3935 Default value 'b'. Line color. 

3936 lims : List, default None 

3937 Energy plot limits. 

3938 figsize : Tuple, default None 

3939 Figure size. 

3940 weights : Real array, shape (nk, norb), default None 

3941 One weight per k-point and band, e.g. from *band_weights*. 

3942 style : String, default 'color' 

3943 'color': the bands 

3944 colored by *weights*; 'size': dots of size proportional to 

3945 :math:`|w|` (*ms*, or 8 if *ms* is 0, for the largest) on thin 

3946 lines of color *c*. 

3947 cmap 

3948 Default value None ('viridis', or 'RdBu_r', centred on 

3949 zero, if some weights are negative). Colormap of *style* 'color'. 

3950 

3951 Returns 

3952 ------- 

3953 fig : Figure 

3954 ''' 

3955 error_handling.empty_ndarray(self.en, 'get_bands or k_path') 

3956 error_handling.positive_real(fs, 'fs') 

3957 error_handling.positive_real(lw, 'lw') 

3958 error_handling.lims(lims) 

3959 error_handling.tuple_2elem(figsize, 'figsize') 

3960 fig, ax = plt.subplots(figsize=figsize) 

3961 if weights is None: 

3962 for n in range(self.norb): 

3963 ax.plot(self.ks_dist, self.en[:, n].real, c=c, lw=lw, marker='o', ms=ms) 

3964 else: 

3965 self._plot_fat_bands(fig, ax, weights, style, cmap, c, lw, ms) 

3966 for node in self.nodes: 

3967 ax.axvline(node, color='k', lw=0.5) 

3968 ax.set_xlim(self.ks_dist[0], self.ks_dist[-1]) 

3969 if lims is not None: 

3970 ax.set_ylim(lims) 

3971 if node_labels is not None: 

3972 error_handling.ndarray(np.array(node_labels), 'node_labels', len(self.nodes)) 

3973 ax.set_xticks(self.nodes) 

3974 ax.set_xticklabels(node_labels, fontsize=fs) 

3975 ax.set_ylabel('$E$', fontsize=fs) 

3976 for label in ax.yaxis.get_majorticklabels(): 

3977 label.set_fontsize(fs) 

3978 fig.set_layout_engine('tight') 

3979 plt.draw() 

3980 return fig 

3981 

3982 def _plot_fat_bands(self, fig, ax, weights, style, cmap, c, lw, ms) -> None: 

3983 ''' 

3984 Private method. The bands of *plot_bands* colored (or with dots 

3985 sized) by *weights*. 

3986 ''' 

3987 error_handling.band_weights(weights, self.en.shape) 

3988 error_handling.weight_style(style) 

3989 weights = np.asarray(weights, dtype='f8') 

3990 signed = weights.min() < 0 

3991 vmax = np.abs(weights).max() or 1. 

3992 if style == 'size': 

3993 size = (ms or 8.) ** 2 

3994 for n in range(self.norb): 

3995 ax.plot(self.ks_dist, self.en[:, n].real, c=c, lw=lw / 4) 

3996 ax.scatter(self.ks_dist, self.en[:, n].real, s=size * np.abs(weights[:, n]) / vmax, 

3997 c=c, lw=0) 

3998 return 

3999 norm = Normalize(-vmax if signed else 0., vmax) 

4000 cmap = cmap or ('RdBu_r' if signed else 'viridis') 

4001 for n in range(self.norb): 

4002 pts = np.stack([self.ks_dist, self.en[:, n].real], axis=-1) 

4003 lines = LineCollection(list(np.stack([pts[:-1], pts[1:]], axis=1)), cmap=cmap, norm=norm, lw=lw) 

4004 lines.set_array((weights[:-1, n] + weights[1:, n]) / 2) 

4005 ax.add_collection(lines) 

4006 ax.autoscale_view() 

4007 fig.colorbar(lines, ax=ax) 

4008 

4009 def show(self) -> None: 

4010 ''' 

4011 Emulate Matplotlib method plt.show(). 

4012 ''' 

4013 plt.show() 

4014 

4015 

4016def ribbon( 

4017 lat: Lattice, 

4018 list_hop: list[dict], 

4019 width: int, 

4020 direction: int = 1, 

4021 onsite: dict | None = None, 

4022 spin: bool = False, 

4023) -> KSpace: 

4024 r''' 

4025 Cut a ribbon out of a 2D periodic model: periodic along one primitive 

4026 vector, finite (open boundary, *width* unit cells) along the other. 

4027 This is the standard way to see edge states in a band structure (e.g. 

4028 the zero-energy edge band of a zigzag graphene ribbon, or the helical 

4029 edge states of a Kane-Mele ribbon). Cut out of a 3D model, it gives a 

4030 slab: periodic along the two other primitive vectors, the way to see 

4031 surface states (e.g. the Fermi arcs of a Weyl semimetal). 

4032 

4033 Parameters 

4034 ---------- 

4035 lat 

4036 **Lattice** class instance (2D or 3D, i.e. two or three primitive 

4037 vectors). Only *unit_cell* and *prim_vec* are used. 

4038 list_hop 

4039 List of dictionaries, in the same format passed to 

4040 *KSpace.set_hopping* -- the hoppings of the periodic (2D) model 

4041 that the ribbon is cut from. 

4042 width : Positive integer 

4043 Number of unit cells across the ribbon. 

4044 direction : 0 or 1 (or 2, in 3D), default 1 

4045 Which primitive 

4046 vector (``lat.prim_vec[direction]``) becomes finite; the others stay 

4047 periodic. 

4048 onsite : Dictionary, default None 

4049 Onsite energies, in the 

4050 same format passed to *KSpace.set_onsite* -- applied identically 

4051 on every row of the ribbon. 

4052 spin : Boolean, default False 

4053 See *KSpace*. 

4054 

4055 Returns 

4056 ------- 

4057 rib 

4058 **KSpace** instance, 1D-periodic (2D-periodic for a slab), with 

4059 ``width * len(lat.unit_cell)`` sites (each site of *lat*, 

4060 repeated once per row across the ribbon; row *w*'s copy of site 

4061 *i* is orbital ``w*len(lat.unit_cell) + i``). 

4062 

4063 Examples 

4064 -------- 

4065 Example usage:: 

4066 

4067 # zigzag graphene ribbon, 20 unit cells wide 

4068 list_hop = [{'i': 0, 'j': 1, 'R': (0, 0), 't': 1.}, 

4069 {'i': 0, 'j': 1, 'R': (-1, 0), 't': 1.}, 

4070 {'i': 0, 'j': 1, 'R': (0, -1), 't': 1.}] 

4071 rib = ribbon(lat, list_hop, width=20) 

4072 ''' 

4073 error_handling.lat(lat) 

4074 error_handling.dim_min(len(lat.prim_vec), 2) 

4075 error_handling.positive_int(width, 'width') 

4076 error_handling.direction(direction, len(lat.prim_vec)) 

4077 periodic = [d for d in range(len(lat.prim_vec)) if d != direction] 

4078 n_sites = len(lat.unit_cell) 

4079 a_dir = np.array(lat.prim_vec[direction]) 

4080 new_unit_cell = [] 

4081 for w in range(width): 

4082 for dic in lat.unit_cell: 

4083 r0 = np.array(dic['r0']) + w*a_dir 

4084 new_unit_cell.append({'tag': dic['tag'], 'r0': tuple(float(c) for c in r0)}) 

4085 new_lat = Lattice(unit_cell=new_unit_cell, prim_vec=[lat.prim_vec[d] for d in periodic]) 

4086 rib = KSpace(new_lat, spin=spin) 

4087 new_list_hop = [] 

4088 for dic in list_hop: 

4089 w2_shift = dic['R'][direction] 

4090 n_periodic = tuple(dic['R'][d] for d in periodic) 

4091 for w in range(width): 

4092 w2 = w + w2_shift 

4093 if 0 <= w2 < width: 

4094 new_list_hop.append({'i': w*n_sites + dic['i'], 

4095 'j': w2*n_sites + dic['j'], 

4096 'R': n_periodic, 

4097 't': dic['t']}) 

4098 rib.set_hopping(new_list_hop) 

4099 if onsite is not None: 

4100 rib.set_onsite(onsite) 

4101 return rib 

4102 

4103 

4104def magnetic_supercell( 

4105 lat: Lattice, 

4106 list_hop: list[dict], 

4107 p: int, 

4108 q: int, 

4109 onsite: dict | None = None, 

4110 spin: bool = False, 

4111) -> KSpace: 

4112 r''' 

4113 Thread a uniform magnetic field through a 2D periodic model, with a 

4114 rational flux :math:`\alpha = p/q` flux quanta per unit cell, and return 

4115 the Bloch Hamiltonian of its magnetic unit cell: *q* cells along 

4116 :math:`\mathbf{a}_1`. Each hopping gets its Peierls phase (see 

4117 *System.set_peierls_phase*) 

4118 

4119 .. math:: 

4120 

4121 \phi_{ij} = \frac{2\pi}{\Phi_0}\int_{\mathbf{r}_i}^{\mathbf{r}_j}\mathbf{A}\cdot d\mathbf{l} 

4122 = 2\pi\alpha\,\frac{s_{1,i}+s_{1,j}}{2}\,(s_{2,j}-s_{2,i}) 

4123 

4124 in the gauge :math:`\mathbf{A} = \alpha\Phi_0\, s_1\nabla s_2`, with 

4125 :math:`(s_1, s_2)` the fractional coordinates of a point along 

4126 :math:`(\mathbf{a}_1, \mathbf{a}_2)`, periodic along :math:`\mathbf{a}_2`. 

4127 A translation by :math:`q\mathbf{a}_1` is a symmetry only up to the 

4128 gauge transformation :math:`e^{-2\pi i p s_2}`, which the hoppings 

4129 crossing the magnetic cell boundary carry. The bands of the result are 

4130 the Hofstadter subbands, and their *chern_number* the TKNN integers. 

4131 

4132 Parameters 

4133 ---------- 

4134 lat : **Lattice** class instance, 2D (two primitive vectors in the plane) 

4135 list_hop 

4136 List of dictionaries, the hoppings of the field-free 

4137 model, in the format of *KSpace.set_hopping*. 

4138 p : Integer 

4139 Numerator of the flux per unit cell. 

4140 q : Positive integer 

4141 Denominator of the flux per unit cell: the 

4142 magnetic cell has *q* unit cells. 

4143 onsite : Dictionary, default None 

4144 Onsite energies, in the 

4145 format of *KSpace.set_onsite*, applied to every copy. 

4146 spin : Boolean, default False 

4147 See *KSpace*. 

4148 

4149 Returns 

4150 ------- 

4151 mag 

4152 **KSpace** instance, with ``q * len(lat.unit_cell)`` 

4153 sites: copy *m* of site *i* is site ``m*len(lat.unit_cell) + i``, 

4154 displaced by :math:`m\mathbf{a}_1`. 

4155 

4156 Examples 

4157 -------- 

4158 Example usage:: 

4159 

4160 # square lattice, flux 1/3 per plaquette: three Hofstadter bands 

4161 mag = magnetic_supercell(lattices.square(), list_hop, p=1, q=3) 

4162 chern = [mag.chern_number(bands=[n]) for n in range(3)] 

4163 ''' 

4164 error_handling.lat(lat) 

4165 error_handling.planar_cell(lat.prim_vec) 

4166 error_handling.set_hopping_kspace(list_hop, len(lat.unit_cell), 2, spin) 

4167 error_handling.integer(p, 'p') 

4168 error_handling.positive_int(q, 'q') 

4169 alpha = p / q 

4170 n_sites = len(lat.unit_cell) 

4171 a = np.array(lat.prim_vec, dtype='f8') 

4172 frac = np.linalg.solve(a.T, np.array([dic['r0'] for dic in lat.unit_cell], dtype='f8').T).T 

4173 new_unit_cell = [] 

4174 for m in range(q): 

4175 for dic in lat.unit_cell: 

4176 r0 = np.array(dic['r0'], dtype='f8') + m * a[0] 

4177 new_unit_cell.append({'tag': dic['tag'], 'r0': (float(r0[0]), float(r0[1]))}) 

4178 new_lat = Lattice(unit_cell=new_unit_cell, prim_vec=[tuple(q * a[0]), lat.prim_vec[1]]) 

4179 mag = KSpace(new_lat, spin=spin) 

4180 new_list_hop = [] 

4181 for dic in list_hop: 

4182 n1, n2 = dic['R'] 

4183 for m in range(q): 

4184 s1_i, s2_i = m + frac[dic['i'], 0], frac[dic['i'], 1] 

4185 s1_j, s2_j = m + n1 + frac[dic['j'], 0], n2 + frac[dic['j'], 1] 

4186 big, m_j = divmod(m + n1, q) 

4187 phase = 2 * PI * alpha * 0.5 * (s1_i + s1_j) * (s2_j - s2_i) - 2 * PI * p * big * s2_j 

4188 new_list_hop.append({'i': m * n_sites + dic['i'], 'j': m_j * n_sites + dic['j'], 

4189 'R': (big, n2), 't': dic['t'] * np.exp(1j * phase)}) 

4190 mag.set_hopping(new_list_hop) 

4191 if onsite is not None: 

4192 mag.set_onsite(onsite) 

4193 return mag 

4194 

4195 

4196################################# 

4197# K-PATHS AND CONSTANT-ENERGY CONTOURS 

4198################################# 

4199 

4200 

4201def _response_weights( 

4202 en: NDArray[np.float64], mu: float, temperature: float, response: str, 

4203) -> NDArray[np.float64]: 

4204 r''' 

4205 Private. The weights :math:`P_{nm}` of the Kubo pair sum of 

4206 *KSpace._kubo*, shape (nk, n, n), from the bands *en* (nk, n). With 

4207 :math:`x_n = (E_n-\mu)/T`, the conductivities are 

4208 :math:`\sum_n w_n\Omega_n`, i.e. :math:`P_{nm} = -(w_n - w_m)`, with 

4209 :math:`w = f` ('hall'), the entropy per state 

4210 :math:`s = -f\ln f-(1-f)\ln(1-f)` ('nernst') and 

4211 :math:`c_2/T = T\int_x^\infty y^2(-f'(y))\,dy` ('thermal'). The orbital 

4212 magnetization ('magnetization'), 

4213 :math:`\sum_n[f_n\,\mathrm{Im}\langle\partial_xu_n|(H-E_n)|\partial_yu_n\rangle + g_n\Omega_n]` 

4214 with :math:`g = T\ln(1+e^{-x})` (:math:`\max(\mu-E, 0)` at :math:`T = 0`), 

4215 has :math:`P_{nm} = \frac12(f_n+f_m)(E_m-E_n) - (g_n-g_m)`. 

4216 ''' 

4217 if response == 'magnetization': 

4218 f = occupation.fermi_dirac(en, mu, temperature) 

4219 if temperature == 0: 

4220 g = np.maximum(mu - en, 0.) 

4221 else: 

4222 g = temperature * np.logaddexp(0., -(en - mu) / temperature) 

4223 return 0.5 * (f[:, :, None] + f[:, None, :]) * (en[:, None, :] - en[:, :, None]) \ 

4224 - (g[:, :, None] - g[:, None, :]) 

4225 if response == 'hall': 

4226 w = occupation.fermi_dirac(en, mu, temperature) 

4227 else: 

4228 a = np.abs(en - mu) / temperature 

4229 if response == 'nernst': 

4230 w = np.log1p(np.exp(-a)) + a * expit(-a) 

4231 else: 

4232 # int_a^inf y^2 (-f') dy = a^2 f(a) + 2 [a ln(1 + e^-a) - Li2(-e^-a)] 

4233 # for a >= 0, and pi^2/3 minus it below the Fermi level 

4234 # (Li2(z) = spence(1 - z) in scipy's convention) 

4235 c2 = a ** 2 * expit(-a) + 2 * (a * np.log1p(np.exp(-a)) - spence(1 + np.exp(-a))) 

4236 w = temperature * np.where(en >= mu, c2, PI ** 2 / 3 - c2) 

4237 return -(w[:, :, None] - w[:, None, :]) 

4238 

4239 

4240def _path( 

4241 points: list[ArrayLike], nk: int, 

4242) -> tuple[NDArray[np.float64], NDArray[np.float64], list[float]]: 

4243 ''' 

4244 Private function. The k-points of a path through *points* (*nk* per 

4245 segment, the last point included), their cumulative distance, and the 

4246 positions of the nodes along it. 

4247 ''' 

4248 pts = np.atleast_2d(np.asarray(points, dtype='f8')) 

4249 ks = np.concatenate([np.linspace(pts[i], pts[i+1], nk, endpoint=False) 

4250 for i in range(len(pts) - 1)] + [pts[-1:]]) 

4251 dist = np.concatenate([[0.], np.cumsum(np.linalg.norm(np.diff(ks, axis=0), axis=1))]) 

4252 return ks, dist, dist[::nk][:len(pts)-1].tolist() + [dist[-1]] 

4253 

4254 

4255def _marching_table(dim: int) -> dict[int, list[list[tuple[int, int]]]]: 

4256 ''' 

4257 Private function. For each pattern of the vertices of a triangle 

4258 (*dim* = 2) or tetrahedron (*dim* = 3) above a level (bit m set if 

4259 vertex m is above), the pieces of the level set, as lists of the edges 

4260 (a, b) that they cross: one segment in 2D; one triangle, or two 

4261 triangles forming a quadrilateral when two vertices are above, in 3D. 

4262 ''' 

4263 table = {} 

4264 for pattern in range(1, 2 ** (dim + 1) - 1): 

4265 up = [m for m in range(dim + 1) if pattern >> m & 1] 

4266 down = [m for m in range(dim + 1) if not pattern >> m & 1] 

4267 if dim == 3 and len(up) == 2: 

4268 (a, b), (c, d) = up, down 

4269 table[pattern] = [[(a, c), (a, d), (b, d)], [(a, c), (b, d), (b, c)]] 

4270 else: 

4271 table[pattern] = [[(a, b) for a in up for b in down]] 

4272 return table 

4273 

4274 

4275def _level_set(values: NDArray[np.float64], level: float) -> NDArray[np.float64]: 

4276 ''' 

4277 Private function. The level set ``values == level`` of a function on a 

4278 periodic 2D or 3D mesh (*values* of shape (n1, n2) or (n1, n2, n3)), 

4279 linear in each simplex of *dos._mesh_simplices*: segments (shape 

4280 (n, 2, 2)) or triangles (shape (n, 3, 3)), in fractional coordinates 

4281 (not folded: between 0 and 1 + 1/n_d). 

4282 ''' 

4283 shape, dim = values.shape, values.ndim 

4284 offsets = dos._mesh_simplices(dim) 

4285 vals = dos._simplex_values(values - level, offsets).reshape(dim + 1, -1) 

4286 pattern = sum((vals[m] > 0).astype(int) << m for m in range(dim + 1)) 

4287 pieces = [np.empty((0, dim, dim))] 

4288 for pat, polygons in _marching_table(dim).items(): 

4289 sel = np.nonzero(pattern == pat)[0] 

4290 if not len(sel): 

4291 continue 

4292 simplex, cell = np.divmod(sel, int(np.prod(shape))) 

4293 corner = np.stack(np.unravel_index(cell, shape), axis=-1) 

4294 pos = corner[None] + offsets[simplex].transpose(1, 0, 2) 

4295 v = vals[:, sel] 

4296 for polygon in polygons: 

4297 # the crossing on edge (a, b): v[a] > 0 >= v[b] or the reverse 

4298 pieces.append(np.stack([pos[a] + (v[a] / (v[a] - v[b]))[:, None] * (pos[b] - pos[a]) 

4299 for a, b in polygon], axis=1)) 

4300 return np.concatenate(pieces) / np.array(shape) 

4301 

4302 

4303def _zone_polygon(rec: NDArray[np.float64]) -> NDArray[np.float64]: 

4304 ''' 

4305 Private function. Vertices, counterclockwise, of the first Brillouin 

4306 zone (Wigner-Seitz cell) of the 2D reciprocal lattice with basis *rec* 

4307 (rows): the points at least as close to the origin as to any other 

4308 reciprocal lattice vector, where two of the bisectors meet. 

4309 ''' 

4310 gs = np.array([n @ rec for n in product(range(-2, 3), repeat=2) if any(n)]) 

4311 half = np.sum(gs ** 2, axis=1) / 2 

4312 tol = 1e-9 * half.max() 

4313 found = [] 

4314 for i in range(len(gs)): 

4315 for j in range(i + 1, len(gs)): 

4316 mat = gs[[i, j]] 

4317 if abs(np.linalg.det(mat)) > tol: 

4318 k = np.linalg.solve(mat, half[[i, j]]) 

4319 if np.all(gs @ k <= half + tol): 

4320 found.append(k) 

4321 verts = np.unique(np.round(np.array(found), 9), axis=0) 

4322 return verts[np.argsort(np.arctan2(verts[:, 1], verts[:, 0]))] 

4323 

4324 

4325def _gauss_reduce(u: NDArray[np.float64], v: NDArray[np.float64]): 

4326 ''' 

4327 Private function. Gauss-reduced basis of a 2D lattice: the two 

4328 shortest independent vectors, |u| <= |v|, at an obtuse (or right) 

4329 angle, so that the edges of the Wigner-Seitz cell are normal to 

4330 u, v and u + v. 

4331 ''' 

4332 while True: 

4333 if u @ u > v @ v: 

4334 u, v = v, u 

4335 m = np.round(u @ v / (u @ u)) 

4336 if m == 0: 

4337 break 

4338 v = v - m * u 

4339 return (u, v - u) if u @ v > 0 else (u, v) 

4340 

4341 

4342def _successive_minima(vecs: NDArray[np.float64]): 

4343 ''' 

4344 Private function. The shortest lattice vectors of a 3D lattice with 

4345 basis *vecs* (rows): the three successive minima, the shells of 

4346 lattice vectors sorted by length (list of arrays), and their lengths. 

4347 ''' 

4348 pts = np.array([n @ vecs for n in product(range(-3, 4), repeat=3) if any(n)]) 

4349 lengths = np.linalg.norm(pts, axis=1) 

4350 # by length, then (among equal lengths) the most positive first: x, then y, then z 

4351 order = np.lexsort((-pts[:, 2], -pts[:, 1], -pts[:, 0], np.round(lengths / lengths.min(), 6))) 

4352 pts, lengths = pts[order], lengths[order] 

4353 minima: list = [] 

4354 for p in pts: 

4355 if np.linalg.matrix_rank(np.array(minima + [p]), tol=1e-8 * lengths[0]) == len(minima) + 1: 

4356 minima.append(p) 

4357 if len(minima) == 3: 

4358 break 

4359 bounds = np.nonzero(np.diff(lengths) > 1e-6 * lengths[0])[0] + 1 

4360 return np.array(minima), np.split(pts, bounds), [s[0] for s in np.split(lengths, bounds)] 

4361 

4362 

4363def _orthogonal_axes(shell: NDArray[np.float64]) -> NDArray[np.float64]: 

4364 ''' 

4365 Private function. Three mutually orthogonal unit vectors among the 

4366 vectors of *shell* (the conventional cubic axes). 

4367 ''' 

4368 x = shell[0] / np.linalg.norm(shell[0]) 

4369 y = next(s for s in shell if abs(s @ x) < 1e-6 * np.linalg.norm(s)) 

4370 y = y / np.linalg.norm(y) 

4371 return np.array([x, y, np.cross(x, y)]) 

4372 

4373 

4374def cubic_path( 

4375 points: dict[str, ArrayLike], names: list[str], a: float = 1., axes: ArrayLike | None = None, 

4376) -> tuple[list[NDArray[np.float64]], list[str]]: 

4377 r''' 

4378 Build a path through k-points given in the conventional coordinates of 

4379 a cubic crystal: in units of :math:`2\pi/a` along the cubic axes, with 

4380 :math:`a` the side of the conventional cube, as tables of high-symmetry 

4381 points list them (e.g. :math:`X = (0, 1, 0)` and 

4382 :math:`L = (\tfrac12, \tfrac12, \tfrac12)` for fcc). *high_symmetry_path* 

4383 builds the standard simple cubic, fcc and bcc paths with it; use it 

4384 for another path, or other points, of the same lattices. 

4385 

4386 Parameters 

4387 ---------- 

4388 points : Dictionary 

4389 key: label, val: tuple of 3 real numbers, 

4390 the point in units of :math:`2\pi/a`. 

4391 names 

4392 List of at least two labels (keys of *points*), the 

4393 nodes of the path in order. 

4394 a : Positive real, default 1 

4395 Side of the conventional cube. 

4396 axes : Real array, shape (3, 3) 

4397 Default value None: the 

4398 Cartesian axes. Rows: the three orthonormal cubic axes, in the 

4399 coordinates of *KSpace.get_ham* (for a crystal whose cube is rotated). 

4400 

4401 Returns 

4402 ------- 

4403 points : List of real ndarrays, the k-points, for *KSpace.k_path* 

4404 labels : List of strings, *names*, for *node_labels* 

4405 

4406 Examples 

4407 -------- 

4408 Example usage:: 

4409 

4410 # fcc, along Gamma - X - U and K - Gamma - L 

4411 pts = {'G': (0, 0, 0), 'X': (0, 1, 0), 'U': (.25, 1, .25), 

4412 'K': (.75, .75, 0), 'L': (.5, .5, .5)} 

4413 points, labels = cubic_path(pts, ['G', 'X', 'U'], a=3.6) 

4414 ''' 

4415 error_handling.cubic_path(points, names, axes) 

4416 error_handling.positive_real(a, 'a') 

4417 axes = np.eye(3) if axes is None else np.asarray(axes, dtype='f8') 

4418 return [2 * PI / a * np.asarray(points[n], dtype='f8') @ axes for n in names], list(names) 

4419 

4420 

4421def _bravais_3d(vecs: NDArray[np.float64]): 

4422 ''' 

4423 Private function. The points and labels of the high-symmetry path of a 

4424 3D lattice (basis *vecs*, rows), or (None, None) if it is not one of 

4425 the simple cubic, fcc, bcc, simple tetragonal or hexagonal lattices. 

4426 ''' 

4427 minima, shells, radii = _successive_minima(vecs) 

4428 lengths = np.linalg.norm(minima, axis=1) 

4429 pairs = [(0, 1), (0, 2), (1, 2)] 

4430 cosine = {p: abs(minima[p[0]] @ minima[p[1]]) / (lengths[p[0]] * lengths[p[1]]) for p in pairs} 

4431 equal = {p: abs(lengths[p[0]] - lengths[p[1]]) < 1e-6 * lengths[2] for p in pairs} 

4432 right = {p: cosine[p] < 1e-6 for p in pairs} 

4433 G = r'$\Gamma$' 

4434 if all(right.values()): 

4435 if all(equal.values()): # simple cubic 

4436 return cubic_path({G: (0, 0, 0), 'X': (0, .5, 0), 'M': (.5, .5, 0), 'R': (.5, .5, .5)}, 

4437 [G, 'X', 'M', G, 'R', 'X'], lengths[0], minima / lengths[:, None]) 

4438 if sum(equal.values()) != 1: # orthorhombic 

4439 return None, None 

4440 # simple tetragonal: a, a, c 

4441 (i, j), = [p for p in pairs if equal[p]] 

4442 k = 3 - i - j 

4443 a, c = lengths[i], lengths[k] 

4444 axes = np.array([minima[i] / a, minima[j] / a, minima[k] / c]) 

4445 pts = {G: (0, 0, 0), 'X': (0, .5, 0), 'M': (.5, .5, 0), 'Z': (0, 0, .5), 

4446 'R': (0, .5, .5), 'A': (.5, .5, .5)} 

4447 names = [G, 'X', 'M', G, 'Z', 'R', 'A', 'Z'] 

4448 scale = 2 * PI * np.array([1 / a, 1 / a, 1 / c]) 

4449 return [(scale * pts[n]) @ axes for n in names], names 

4450 for (i, j) in pairs: 

4451 k = 3 - i - j 

4452 if equal[(i, j)] and abs(cosine[(i, j)] - .5) < 1e-6 and \ 

4453 all(right[p] for p in pairs if p != (i, j)): 

4454 # hexagonal: reciprocal vectors of (a1, a2, c), in-plane ones at 120 degrees 

4455 rec = 2 * PI * np.linalg.inv(minima[[i, j, k]]).T 

4456 b1, b2 = _gauss_reduce(rec[0], rec[1]) 

4457 M, K, A = b1 / 2, (2 * b1 + b2) / 3, rec[2] / 2 

4458 hex_pts = {G: np.zeros(3), 'M': M, 'K': K, 'A': A, 'L': M + A, 'H': K + A} 

4459 names = [G, 'M', 'K', G, 'A', 'L', 'H', 'A'] 

4460 return [hex_pts[n] for n in names], names 

4461 if not all(equal.values()): 

4462 return None, None 

4463 if len(shells[0]) == 12: # fcc: 12 nearest neighbours at a/sqrt(2), then 6 at a 

4464 return cubic_path({G: (0, 0, 0), 'X': (0, 1, 0), 'W': (.5, 1, 0), 'K': (.75, .75, 0), 

4465 'L': (.5, .5, .5), 'U': (.25, 1, .25)}, 

4466 [G, 'X', 'W', 'K', G, 'L', 'U', 'W', 'L', 'K'], 

4467 radii[1], _orthogonal_axes(shells[1])) 

4468 if len(shells[0]) == 8 and all(abs(c - 1 / 3) < 1e-6 for c in cosine.values()): 

4469 # bcc: 8 nearest neighbours at a sqrt(3)/2, then 6 at a 

4470 return cubic_path({G: (0, 0, 0), 'H': (0, 0, 1), 'N': (.5, 0, .5), 'P': (.5, .5, .5)}, 

4471 [G, 'H', 'N', G, 'P', 'H'], radii[1], _orthogonal_axes(shells[1])) 

4472 return None, None 

4473 

4474 

4475def high_symmetry_path(lat: Lattice) -> tuple[list[NDArray[np.float64]], list[str]]: 

4476 r''' 

4477 Get a standard path through the high-symmetry points of the Brillouin 

4478 zone of a lattice, for *KSpace.k_path* and the labels of 

4479 *KSpace.plot_bands*. The Bravais lattice is recognized from 

4480 *lat.prim_vec*, whatever the choice of primitive vectors and the 

4481 orientation of the lattice: 

4482 

4483 * 1D: :math:`-X, \Gamma, X` (the whole zone, :math:`X = \mathbf{b}/2`). 

4484 * 2D (the five Bravais lattices): square :math:`\Gamma X M \Gamma`; 

4485 rectangular :math:`\Gamma X S Y \Gamma`; hexagonal 

4486 :math:`\Gamma M K \Gamma`; centred rectangular and oblique 

4487 :math:`\Gamma X H C H_1 Y \Gamma`, along the edges of the 

4488 zone: :math:`X, Y, C` are the midpoints of the edges normal to the 

4489 reciprocal vectors :math:`\mathbf{b}_1, \mathbf{b}_2, 

4490 \mathbf{b}_1+\mathbf{b}_2` of the reduced basis (with 

4491 :math:`|\mathbf{b}_1| \le |\mathbf{b}_2|`), :math:`H` and 

4492 :math:`H_1` the corners between them. 

4493 * 3D: simple cubic :math:`\Gamma X M \Gamma R X`; fcc 

4494 :math:`\Gamma X W K \Gamma L U W L K`; bcc :math:`\Gamma H N \Gamma P H`; 

4495 simple tetragonal :math:`\Gamma X M \Gamma Z R A Z`; hexagonal 

4496 :math:`\Gamma M K \Gamma A L H A`. These are the paths of Setyawan 

4497 and Curtarolo (Comput. Mater. Sci. 49, 299 (2010)), up to their 

4498 first discontinuity (a path here is one continuous line). 

4499 

4500 The other 3D lattices (orthorhombic, monoclinic, body-centred 

4501 tetragonal, rhombohedral, ...) raise a ValueError: give their points 

4502 to *k_path* directly. 

4503 

4504 Parameters 

4505 ---------- 

4506 lat : **lattice** class instance (only *prim_vec* is used) 

4507 

4508 Returns 

4509 ------- 

4510 points 

4511 List of real ndarrays, the k-points, in the 

4512 coordinates of *KSpace.get_ham*. 

4513 labels : List of strings (LaTeX), for *node_labels* 

4514 

4515 Examples 

4516 -------- 

4517 Example usage:: 

4518 

4519 points, labels = high_symmetry_path(lat) 

4520 ks.k_path(points, nk=60) 

4521 ks.plot_bands(node_labels=labels) 

4522 ''' 

4523 ks = KSpace(lat) 

4524 vecs = np.array(lat.prim_vec, dtype='f8') @ ks.k_basis # in the k coordinates 

4525 G = r'$\Gamma$' 

4526 if ks.dim == 1: 

4527 x = PI / vecs[0] 

4528 return [-x, np.zeros(1), x], ['$-X$', G, 'X'] 

4529 if ks.dim == 3: 

4530 points, labels = _bravais_3d(vecs) 

4531 error_handling.bravais_lattice(labels, 3) 

4532 return points, labels 

4533 rec = 2 * PI * np.linalg.inv(vecs).T 

4534 b1, b2 = _gauss_reduce(rec[0], rec[1]) 

4535 l1, l2, l3 = (np.linalg.norm(b) for b in (b1, b2, b1 + b2)) 

4536 close = lambda a, b: abs(a - b) < 1e-6 * max(abs(a), abs(b)) 

4537 cosine = b1 @ b2 / (l1 * l2) 

4538 if abs(cosine) < 1e-6: 

4539 if close(l1, l2): 

4540 return [np.zeros(2), b1 / 2, (b1 + b2) / 2, np.zeros(2)], [G, 'X', 'M', G] 

4541 return [np.zeros(2), b1 / 2, (b1 + b2) / 2, b2 / 2, np.zeros(2)], [G, 'X', 'S', 'Y', G] 

4542 if close(l1, l2) and close(cosine, -.5): 

4543 return [np.zeros(2), b1 / 2, (2 * b1 + b2) / 3, np.zeros(2)], [G, 'M', 'K', G] 

4544 # centred rectangular and oblique: around half of the hexagonal zone 

4545 b3 = b1 + b2 

4546 corner = lambda u, v: np.linalg.solve(np.array([u, v]), np.array([u @ u, v @ v]) / 2) 

4547 return ([np.zeros(2), b1 / 2, corner(b1, b3), b3 / 2, corner(b2, b3), b2 / 2, np.zeros(2)], 

4548 [G, 'X', 'H', 'C', '$H_1$', 'Y', G])