Coverage for tbkit/kspace.py: 100%

1376 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 06:13 +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 

40################################# 

41# CLASS KSPACE 

42################################# 

43 

44 

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

46 r''' 

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

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

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

50 

51 :param prim_vec: List of one/two/three tuples. Primitive vectors (see class **lattice**). 

52 

53 :returns: 

54 * **rec_vec** -- List of one/two/three tuples. Reciprocal vectors, with 

55 as many components as the primitive vectors. 

56 ''' 

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

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

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

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

61 if len(prim_vec) == 1: 

62 ax, ay = prim_vec[0] 

63 norm2 = ax ** 2 + ay ** 2 

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

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

66 area = a1x * a2y - a1y * a2x 

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

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

69 return [b1, b2] 

70 

71 

72class KSpace(): 

73 r''' 

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

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

76 

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

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

79 

80 .. math:: 

81 

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

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

84 

85 :param lat: **lattice** class instance. Only *unit_cell* and *prim_vec* 

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

87 :param spin: Boolean. Default value False. If True, every site of 

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

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

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

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

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

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

94 

95 Example usage:: 

96 

97 # graphene, nearest-neighbor hopping t 

98 DX, DY = 0.5 * 3 ** 0.5, 0.5 

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

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

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

102 gra = KSpace(lat) 

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

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

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

106 ''' 

107 

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

109 

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

111 error_handling.lat(lat) 

112 error_handling.boolean(spin, 'spin') 

113 error_handling.independent(lat.prim_vec) 

114 self.lat = lat 

115 self.dim = len(lat.prim_vec) 

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

117 self.spin = spin 

118 self.n_sites = len(lat.unit_cell) 

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

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

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

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

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

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

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

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

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

128 self._hop_values: list = [] 

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

130 self.params: dict = {} 

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

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

133 self.rec_vec = reciprocal_vectors(lat.prim_vec) 

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

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

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

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

138 if self.dim == self.space_dim: 

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

140 elif self.dim == 1: 

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

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

143 else: 

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

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

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

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

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

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

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

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

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

153 

154 @property 

155 def _hop(self) -> list: 

156 ''' 

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

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

159 Assigning it replaces both. 

160 ''' 

161 return self._hops(self.params) 

162 

163 @_hop.setter 

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

165 self._hop_const = list(hops) 

166 self._hop_values = [] 

167 

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

169 ''' 

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

171 ''' 

172 hops = list(self._hop_const) 

173 if not self._hop_values: 

174 return hops 

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

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

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

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

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

180 if self.spin: 

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

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

183 else: 

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

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

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

187 if hermitian: 

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

189 return hops 

190 

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

192 ''' 

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

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

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

196 

197 :param params: Parameter values, added to (or replacing those of) *params*. 

198 

199 Example usage:: 

200 

201 ks.set_params(m=0.5) 

202 ks.chern_number() 

203 ''' 

204 self.params.update(params) 

205 

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

207 ''' 

208 Set the onsite energies, by sublattice tag. 

209 

210 :param dict_onsite: Dictionary. key: tag, val: onsite energy 

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

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

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

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

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

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

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

218 

219 Example usage:: 

220 

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

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

223 # splitting along z on 'b': 

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

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

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

227 ''' 

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

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

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

231 if self.spin: 

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

233 block = val * PAULI['0'] 

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

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

236 else: 

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

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

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

240 for site in sites: 

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

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

243 else: 

244 self.onsite[sites] = val 

245 

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

247 r''' 

248 Set the hoppings between orbitals of the unit cell. 

249 

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

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

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

253 

254 :param list_hop: List of dictionaries with keys ('i', 'j', 'R', 't'): 

255 

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

257 (following the order of *unit_cell*). 

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

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

260 two sites. 

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

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

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

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

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

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

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

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

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

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

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

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

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

274 or one per bond). 

275 

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

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

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

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

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

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

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

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

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

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

286 :param hermitian: Boolean. Default value True. If False, the Hermitian 

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

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

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

290 explicitly, with its own amplitude. 

291 

292 Example usage:: 

293 

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

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

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

297 # square lattice, anisotropic nearest neighbours 

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

299 

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

301 # and its right neighbor: 

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

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

304 # spin-flip term of strength alpha: 

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

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

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

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

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

310 ''' 

311 error_handling.boolean(hermitian, 'hermitian') 

312 if error_handling.hopping_form(list_hop): 

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

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

315 if not hermitian: 

316 self._nonreciprocal = True 

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

318 for dic in list_hop: 

319 R_cart = np.zeros(self.space_dim) 

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

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

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

323 if callable(t): 

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

325 elif self.spin: 

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

327 for a in range(2): 

328 for b in range(2): 

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

330 if hermitian: 

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

332 else: 

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

334 if hermitian: 

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

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

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

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

339 

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

341 r''' 

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

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

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

345 

346 .. math:: 

347 

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

349 

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

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

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

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

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

355 

356 :param list_hop: List of dictionaries ('i', 'j', 'R', 't'), or by 

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

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

359 ''' 

360 if error_handling.hopping_form(list_hop): 

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

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

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

364 self._hop = [] 

365 self.set_hopping(list_hop) 

366 self._overlap_hop += self._hop_const 

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

368 

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

370 r''' 

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

372 orthogonal basis), see *set_overlap*. 

373 

374 :param k: k point (see *get_ham*). 

375 

376 :returns: 

377 * **S** -- Complex ndarray, shape (norb, norb). 

378 ''' 

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

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

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

382 

383 def clear_hopping(self) -> None: 

384 ''' 

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

386 ''' 

387 self._hop = [] 

388 self._nonreciprocal = False 

389 self._overlap_hop = [] 

390 

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

392 r''' 

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

394 

395 :param k: Tuple/list/ndarray of *dim* real numbers. When the lattice 

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

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

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

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

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

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

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

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

404 these coordinates. 

405 :param params: Values of the parameters of the value functions of 

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

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

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

409 

410 :returns: 

411 * **ham** -- Complex ndarray, shape (norb, norb). 

412 

413 Example usage:: 

414 

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

416 ''' 

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

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

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

420 

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

422 r''' 

423 Get the Bloch Hamiltonian in a uniform vector potential 

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

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

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

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

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

429 (see *tbkit.floquet*). 

430 

431 :param k: k point (see *get_ham*). 

432 :param A: Tuple/ndarray of space_dim real numbers. Vector potential. 

433 

434 :returns: 

435 * **ham** -- Complex ndarray, shape (norb, norb). 

436 ''' 

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

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

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

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

441 

442 def _bloch_ham( 

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

444 ) -> NDArray[np.complex128]: 

445 ''' 

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

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

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

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

450 ''' 

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

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

453 

454 def get_bands( 

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

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

457 r''' 

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

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

460 between consecutive k-points). 

461 

462 :param ks: ndarray, shape (nk, dim). k-points. 

463 :param eigenvec: Boolean. Default value False. If True, also return 

464 the eigenvectors. 

465 

466 :returns: 

467 * **en** -- Real ndarray, shape (nk, norb). Band energies, sorted ascending. 

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

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

470 * **vn** -- Complex ndarray, shape (nk, norb, norb), only if *eigenvec* is True. 

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

472 ''' 

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

474 error_handling.ks(ks, self.dim) 

475 out = self._diagonalize(ks, eigenvec) 

476 self.ks = ks 

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

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

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

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

481 return out 

482 

483 @overload 

484 def _diagonalize( 

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

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

487 @overload 

488 def _diagonalize( 

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

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

491 @overload 

492 def _diagonalize( 

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

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

495 

496 def _diagonalize( 

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

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

499 r''' 

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

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

502 ''' 

503 return self._eigs(ks, eigenvec) 

504 

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

506 r''' 

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

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

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

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

511 the chunks run in parallel. The results depend on *workers* only up 

512 to rounding (and the phases of the eigenvectors). 

513 

514 :param workers: Positive integer. Number of threads (default 1, no threads). 

515 

516 Example usage:: 

517 

518 ks.set_workers(4) 

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

520 ''' 

521 error_handling.positive_int(workers, 'workers') 

522 self.workers = workers 

523 

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

525 ''' 

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

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

528 *get_ham* (a Floquet model), which is then called per k-point. 

529 ''' 

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

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

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

533 

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

535 ''' 

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

537 ''' 

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

539 

540 @overload 

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

542 @overload 

543 def _eigs( 

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

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

546 @overload 

547 def _eigs( 

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

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

550 

551 def _eigs( 

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

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

554 ''' 

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

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

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

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

559 ''' 

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

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

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

563 with ThreadPoolExecutor(self.workers) as pool: 

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

565 else: 

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

567 if eigenvec: 

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

569 return np.concatenate(out) 

570 

571 def _eig_chunk( 

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

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

574 r''' 

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

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

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

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

579 the Hermitian one is reduced by the Cholesky factor 

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

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

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

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

584 ''' 

585 ham = self._hams(ks) 

586 hermitian = self.is_hermitian() 

587 en: NDArray 

588 vn: NDArray 

589 if self._overlap_hop and hermitian: 

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

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

592 ham = l_inv @ ham @ l_inv_h 

593 if hermitian: 

594 if not eigenvec: 

595 return np.linalg.eigvalsh(ham) 

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

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

598 if self._overlap_hop: 

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

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

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

602 elif eigenvec: 

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

604 else: 

605 en = np.linalg.eigvals(ham) 

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

607 if not eigenvec: 

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

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

610 

611 def is_hermitian(self) -> bool: 

612 ''' 

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

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

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

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

617 

618 :returns: 

619 * **hermitian** -- Boolean. 

620 ''' 

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

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

623 

624 def k_path( 

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

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

627 r''' 

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

629 associated bands. 

630 

631 :param points: List of at least two k-points (each a tuple/list of 

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

633 :param nk: Positive integer. Number of k-points per path segment. 

634 

635 :returns: 

636 * **ks_dist** -- Real ndarray. Cumulative distance along the path, 

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

638 * **en** -- Real ndarray, shape (len(ks_dist), norb). Band energies. 

639 ''' 

640 error_handling.k_path_points(points, self.dim) 

641 error_handling.positive_int(nk, 'nk') 

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

643 self.get_bands(ks) 

644 self.nodes = np.array(nodes) 

645 return self.ks_dist, self.en 

646 

647 def mesh_grid( 

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

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

650 r''' 

651 Build a uniform grid of fractional coordinates spanning the 

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

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

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

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

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

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

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

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

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

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

662 

663 :param nk: Positive integer, or tuple of *dim* positive integers. 

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

665 :param monkhorst_pack: Boolean. Default value False. If True, the 

666 Monkhorst-Pack grid. 

667 

668 :returns: 

669 * **fracs** -- List of *dim* real ndarrays, shape (nk1, nk2) each 

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

671 * **ks** -- Real ndarray, shape (nk1*nk2, dim) (or (nk1, dim) in 1D). 

672 ''' 

673 error_handling.nk(nk, self.dim) 

674 error_handling.boolean(monkhorst_pack, 'monkhorst_pack') 

675 if isinstance(nk, int): 

676 nk = (nk,) * self.dim 

677 if monkhorst_pack: 

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

679 else: 

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

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

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

683 if self.dim == 1: 

684 return [fracs[0]], ks 

685 return list(fracs), ks 

686 

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

688 ''' 

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

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

691 for *plot_bands* untouched. 

692 

693 :param nk: Positive integer, or tuple of *dim* positive integers. 

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

695 :param monkhorst_pack: Boolean. Default value False. If True, the 

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

697 

698 :returns: 

699 * **en** -- Real ndarray, shape (nk1*nk2, norb). Band energies 

700 over the mesh (flattened). 

701 ''' 

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

703 return self._diagonalize(ks) 

704 

705 def get_fermi_level( 

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

707 ) -> float: 

708 ''' 

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

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

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

712 

713 :param n_electrons: Positive real or zero, at most norb. Electrons per unit cell. 

714 :param nk: Positive integer, or tuple. Default value 30. k-mesh. 

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

716 

717 :returns: 

718 * **e_fermi** -- Real number. 

719 ''' 

720 en = self.mesh_bands(nk) 

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

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

723 

724 def berry_curvature( 

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

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

727 ) -> NDArray[np.float64]: 

728 r''' 

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

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

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

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

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

734 subspaces at its four corners, 

735 

736 .. math:: 

737 

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

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

740 

741 the corners being visited counterclockwise. This is the discrete 

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

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

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

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

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

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

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

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

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

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

752 *biorthogonal_berry_curvature*). That ordering follows the bands 

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

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

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

756 meaningless result -- see *biorthogonal_berry_curvature*. 

757 

758 :param bands: Positive integer, or list of positive integers. Band 

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

760 below a gap). 

761 :param nk: Positive integer, or tuple of 2 positive integers. 

762 Default value 30. Number of k-points along each of the two 

763 reciprocal lattice vectors spanning the plane. 

764 :param plane: Tuple of two distinct integers. Default value (0, 1). 

765 3D lattices only: the reciprocal vectors 

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

767 Brillouin zone. The flux is oriented along the remaining 

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

769 :param k_fixed: Real number. Default value 0. 3D lattices only: the 

770 fractional coordinate of the plane along the remaining reciprocal 

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

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

773 

774 :returns: 

775 * **curv** -- Real ndarray, shape (nk1, nk2). Berry curvature 

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

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

778 *chern_number*. 

779 ''' 

780 error_handling.dim_min(self.dim, 2) 

781 if isinstance(bands, int): 

782 bands = [bands] 

783 error_handling.band_indices(bands, self.norb) 

784 if isinstance(nk, int): 

785 nk = (nk, nk) 

786 error_handling.nk(nk, 2) 

787 error_handling.plane(plane, self.dim) 

788 error_handling.real_number(k_fixed, 'k_fixed') 

789 n1, n2 = nk 

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

791 if not self.is_hermitian(): 

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

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

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

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

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

797 return self._flux(v1, plane) 

798 

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

800 ''' 

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

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

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

804 ''' 

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

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

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

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

809 

810 def overlap(a, b): 

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

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

813 curv = -np.angle(link) 

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

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

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

817 return self._plane_orientation(plane) * curv 

818 

819 def _plane_mesh( 

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

821 ) -> NDArray[np.float64]: 

822 ''' 

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

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

825 fractional coordinate k_fixed along the remaining reciprocal vector. 

826 ''' 

827 n1, n2 = nk 

828 i, j = plane 

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

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

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

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

833 for l in rest: 

834 fracs[:, :, l] = k_fixed 

835 return fracs @ self.rec_vec_k 

836 

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

838 ''' 

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

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

841 ''' 

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

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

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

845 if self.dim == 3: 

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

847 else: 

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

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

850 aa[:self.space_dim] = vec 

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

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

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

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

855 

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

857 ''' 

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

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

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

861 ''' 

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

863 

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

865 ''' 

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

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

868 the (Hermitian, positive) overlap. 

869 ''' 

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

871 if self._overlap_hop: 

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

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

874 return vn[:, :, bands] 

875 

876 def biorthogonal_berry_curvature( 

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

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

879 ) -> NDArray[np.float64]: 

880 r''' 

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

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

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

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

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

886 

887 .. math:: 

888 

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

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

891 

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

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

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

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

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

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

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

899 

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

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

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

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

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

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

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

907 can change. 

908 

909 :param bands: Positive integer, or list of positive integers. Band 

910 indices in the order set by *gap*. 

911 :param nk: Positive integer, or tuple of 2 positive integers. 

912 Default value 30. See *berry_curvature*. 

913 :param kind: 'LR', 'RL', 'RR' or 'LL'. Default value 'LR'. 

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

915 :param gap: 'real' or 'imaginary'. Default value 'real'. Kind of line 

916 gap, and the order of the bands. 

917 :param plane: Tuple of two integers. Default value (0, 1). 3D only, see 

918 *berry_curvature*. 

919 :param k_fixed: Real number. Default value 0. 3D only, see *berry_curvature*. 

920 

921 :returns: 

922 * **curv** -- Real ndarray, shape (nk1, nk2). Flux through each 

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

924 ''' 

925 error_handling.dim_min(self.dim, 2) 

926 if isinstance(bands, int): 

927 bands = [bands] 

928 error_handling.band_indices(bands, self.norb) 

929 if isinstance(nk, int): 

930 nk = (nk, nk) 

931 error_handling.nk(nk, 2) 

932 error_handling.biorthogonal_kind(kind) 

933 error_handling.gap_kind(gap) 

934 error_handling.plane(plane, self.dim) 

935 error_handling.real_number(k_fixed, 'k_fixed') 

936 error_handling.no_overlap(self._overlap_hop) 

937 n1, n2 = nk 

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

939 right = np.zeros((n1, n2, self.norb, len(bands)), 'c16') 

940 left = np.zeros_like(right) 

941 en = np.zeros((n1, n2, self.norb), 'c16') 

942 for i1 in range(n1): 

943 for i2 in range(n2): 

944 w, vl, vr = LA.eig(self.get_ham(ks[i1, i2]), left=True, right=True) 

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

946 en[i1, i2] = w[order] 

947 r, l = vr[:, order][:, bands], vl[:, order][:, bands] 

948 # biorthonormal: <L_m|R_n> = delta_mn 

949 right[i1, i2] = r 

950 left[i1, i2] = l @ np.linalg.inv(l.conj().T @ r).conj().T 

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

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

953 error_handling.band_continuity(en, bands) 

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

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

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

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

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

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

960 link = np.zeros((2, n1, n2), 'c16') 

961 for i1 in range(n1): 

962 for i2 in range(n2): 

963 link[0, i1, i2] = np.linalg.det(bra[i1, i2].conj().T @ ket[(i1 + 1) % n1, i2]) 

964 link[1, i1, i2] = np.linalg.det(bra[i1, i2].conj().T @ ket[i1, (i2 + 1) % n2]) 

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

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

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

968 

969 def biorthogonal_chern_number( 

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

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

972 ) -> float: 

973 r''' 

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

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

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

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

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

979 open. 

980 

981 :param bands: See *biorthogonal_berry_curvature*. 

982 :param nk: See *biorthogonal_berry_curvature*. 

983 :param kind: 'LR', 'RL', 'RR' or 'LL'. Default value 'LR'. 

984 :param gap: 'real' or 'imaginary'. Default value 'real'. 

985 :param plane: See *berry_curvature*. 

986 :param k_fixed: See *berry_curvature*. 

987 

988 :returns: 

989 * **chern** -- Real number, close to an integer. 

990 ''' 

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

992 

993 def chern_number( 

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

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

996 ) -> float: 

997 r''' 

998 Get the Chern number of a group of bands: 

999 

1000 .. math:: 

1001 

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

1003 

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

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

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

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

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

1009 also handles imaginary line gaps and the left eigenvectors. 

1010 

1011 :param bands: Positive integer, or list of positive integers. Band 

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

1013 below a gap). 

1014 :param nk: Positive integer, or tuple of 2 positive integers. 

1015 Default value 30. Number of k-points along each reciprocal 

1016 lattice vector. 

1017 :param plane: Tuple of two integers. Default value (0, 1). 3D lattices 

1018 only, see *berry_curvature*. 

1019 :param k_fixed: Real number. Default value 0. 3D lattices only, see 

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

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

1022 

1023 :returns: 

1024 * **chern** -- Real number, close to an integer. 

1025 ''' 

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

1027 

1028 def sector_chern_numbers( 

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

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

1031 ) -> tuple[float, float]: 

1032 r''' 

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

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

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

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

1037 bands, and its eigenvectors with positive and negative eigenvalues 

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

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

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

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

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

1043 *spin_chern_number* and *mirror_chern_number*. 

1044 

1045 :param op: Complex ndarray, shape (norb, norb), or callable of k 

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

1047 :param bands: Band index, or list of band indices. 

1048 :param nk: Positive integer, or tuple of 2 positive integers. Default value 30. 

1049 :param plane: Tuple of two integers. Default value (0, 1). 3D only, see *berry_curvature*. 

1050 :param k_fixed: Real number. Default value 0. 3D only, see *berry_curvature*. 

1051 

1052 :returns: 

1053 * **c_plus** -- Real number, close to an integer: Chern number 

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

1055 * **c_minus** -- Real number: that of the sector with :math:`POP < 0`. 

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

1057 ''' 

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

1059 if callable(op): 

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

1061 else: 

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

1063 error_handling.hermitian_operator(ops) 

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

1065 

1066 def _check_sectors( 

1067 self, bands, nk, plane, k_fixed, 

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

1069 ''' 

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

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

1072 ''' 

1073 error_handling.dim_min(self.dim, 2) 

1074 if isinstance(bands, int): 

1075 bands = [bands] 

1076 error_handling.band_indices(bands, self.norb) 

1077 if isinstance(nk, int): 

1078 nk = (nk, nk) 

1079 error_handling.nk(nk, 2) 

1080 error_handling.plane(plane, self.dim) 

1081 error_handling.real_number(k_fixed, 'k_fixed') 

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

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

1084 

1085 def _sector_cherns( 

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

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

1088 ) -> tuple[float, float]: 

1089 ''' 

1090 Private method. *sector_chern_numbers* with the Hermitian operator 

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

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

1093 ''' 

1094 n1, n2 = nk 

1095 v = self._subspaces(ks, bands) 

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

1097 n_minus = error_handling.projected_gap(w) 

1098 sectors = v @ u 

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

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

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

1102 

1103 def spin_chern_number( 

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

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

1106 ) -> float: 

1107 r''' 

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

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

1110 97, 036808 (2006)), 

1111 

1112 .. math:: 

1113 

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

1115 

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

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

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

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

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

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

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

1123 

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

1125 :param nk: Positive integer, or tuple of 2 positive integers. Default value 30. 

1126 :param s_z: Complex ndarray, shape (norb, norb), or callable of k. 

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

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

1129 :param plane: Tuple of two integers. Default value (0, 1). 3D only, see *berry_curvature*. 

1130 :param k_fixed: Real number. Default value 0. 3D only, see *berry_curvature*. 

1131 

1132 :returns: 

1133 * **c_s** -- Real number, close to an integer. 

1134 ''' 

1135 if s_z is None: 

1136 error_handling.spinful(self.spin) 

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

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

1139 return (c_plus - c_minus) / 2 

1140 

1141 def mirror_chern_number( 

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

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

1144 ) -> float: 

1145 r''' 

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

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

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

1149 

1150 .. math:: 

1151 

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

1153 

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

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

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

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

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

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

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

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

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

1163 

1164 :param mirror: Complex ndarray, shape (norb, norb), or callable of k: 

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

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

1167 :param nk: Positive integer, or tuple of 2 positive integers. Default value 30. 

1168 :param plane: Tuple of two integers. Default value (0, 1). 3D only, see *berry_curvature*. 

1169 :param k_fixed: Real number. Default value 0. 3D only: the 

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

1171 :param tol: Positive real. Default value 1e-8. Tolerance of the 

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

1173 

1174 :returns: 

1175 * **n_m** -- Real number, close to an integer. 

1176 ''' 

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

1178 error_handling.positive_real(tol, 'tol') 

1179 if callable(mirror): 

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

1181 else: 

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

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

1184 hams = self._hams(ks) 

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

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

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

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

1189 return (c_plus - c_minus) / 2 

1190 

1191 # ------------------------------------------------------------------ 

1192 # Anomalous and spin Hall conductivities (Kubo formula) 

1193 # ------------------------------------------------------------------ 

1194 

1195 def _check_static(self) -> None: 

1196 ''' 

1197 Private method. Methods that differentiate the stored hoppings call 

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

1199 ''' 

1200 

1201 def _bloch_sum( 

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

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

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

1205 r''' 

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

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

1208 (nk, space_dim)), and its derivatives 

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

1210 along the Cartesian unit vectors *directions*, with 

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

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

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

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

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

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

1217 is built here. 

1218 ''' 

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

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

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

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

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

1224 if positions or A is not None: 

1225 tau = self.orbital_positions() 

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

1227 if positions: 

1228 d = bond 

1229 phase = k_cart @ d.T 

1230 if A is not None: 

1231 phase = phase + bond @ A 

1232 w = np.exp(1j * phase) * t[None, :] 

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

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

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

1236 

1237 def scatter(v): 

1238 size = nk * n * n 

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

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

1241 mat = scatter(w) 

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

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

1244 return mat, dmat 

1245 

1246 def _bloch_derivatives( 

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

1248 params: dict | None = None, 

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

1250 r''' 

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

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

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

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

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

1256 the topology tools) and its derivative, with 

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

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

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

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

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

1262 ''' 

1263 k_cart = ks @ self.k_basis.T 

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

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

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

1267 if not self._overlap_hop: 

1268 return ham, dham 

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

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

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

1272 error_handling.positive_overlap(sv.min()) 

1273 r = sv ** -0.5 

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

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

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

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

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

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

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

1281 dham = ds_inv @ ham @ s_inv + s_inv @ dham @ s_inv + s_inv @ ham @ ds_inv 

1282 return s_inv @ ham @ s_inv, dham 

1283 

1284 def _kubo( 

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

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

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

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

1289 r''' 

1290 Private method. The Kubo curvature 

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

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

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

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

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

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

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

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

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

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

1301 which locates the hot spots of the adaptive refinement. 

1302 ''' 

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

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

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

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

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

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

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

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

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

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

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

1314 if spin_op is not None: 

1315 s_e = vh @ spin_op @ vec 

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

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

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

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

1320 for e, mu in enumerate(e_fermi): 

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

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

1323 return out, hot 

1324 

1325 def _hall( 

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

1327 spin_axis, response='hall', 

1328 ): 

1329 ''' 

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

1331 Kubo curvature (with the adaptive refinement), for 

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

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

1334 *anomalous_nernst_conductivity* and *thermal_hall_conductivity*. 

1335 ''' 

1336 self._check_static() 

1337 error_handling.dim_min(self.dim, 2) 

1338 error_handling.hermitian_model(self.is_hermitian()) 

1339 error_handling.fermi_energies(e_fermi) 

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

1341 error_handling.positive_real(temperature, 'temperature') 

1342 else: 

1343 error_handling.positive_real_zero(temperature, 'temperature') 

1344 error_handling.plane(plane, self.dim) 

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

1346 error_handling.boolean(positions, 'positions') 

1347 error_handling.positive_int(refine, 'refine') 

1348 error_handling.refine_fraction(refine_fraction) 

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

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

1351 if isinstance(nk, int): 

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

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

1354 spin_op = None 

1355 if spin_axis is not None: 

1356 error_handling.spinful(self.spin) 

1357 error_handling.spin_axis(spin_axis) 

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

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

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

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

1362 if full: 

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

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

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

1366 prefactor = 2 * PI / volume 

1367 elif self.dim == 3: 

1368 i, j = plane 

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

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

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

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

1373 pairs = [(0, 1)] 

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

1375 else: 

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

1377 pairs = [(0, 1)] 

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

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

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

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

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

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

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

1385 for ax in range(self.dim): 

1386 if ax not in axes: 

1387 fracs[:, ax] = k_fixed 

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

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

1390 total = coarse.sum(axis=0) 

1391 if refine > 1: 

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

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

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

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

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

1397 # refined together with their neighbours. 

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

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

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

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

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

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

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

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

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

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

1408 sigma = prefactor * total / len(fracs) 

1409 if not full: 

1410 sigma = sigma[:, 0] 

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

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

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

1414 

1415 def hall_conductivity( 

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

1417 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1418 refine: int = 1, refine_fraction: float = 0.05, 

1419 ) -> float | NDArray[np.float64]: 

1420 r''' 

1421 Get the intrinsic anomalous Hall conductivity at any Fermi level -- 

1422 in a gap, in a band, or at a band crossing -- from the Kubo formula 

1423 over a uniform Brillouin-zone mesh, 

1424 

1425 .. math:: 

1426 

1427 \sigma_{xy} = \frac{e^2}{\hbar}\int_{BZ}\frac{d^dk}{(2\pi)^d}\, 

1428 F(\mathbf{k})\, ,\qquad 

1429 F = -\sum_{n\neq m}(f_n - f_m)\, 

1430 \frac{\mathrm{Im}\left[\langle n|\partial_x H|m\rangle\langle m|\partial_y H|n\rangle\right]} 

1431 {(E_n - E_m)^2} = \sum_n f_n\,\Omega_n\, , 

1432 

1433 with :math:`f_n = f(E_n)` the Fermi-Dirac occupations and 

1434 

1435 .. math:: 

1436 

1437 \Omega_n = -2\,\mathrm{Im}\sum_{m\neq n} 

1438 \frac{\langle n|\partial_x H|m\rangle\langle m|\partial_y H|n\rangle}{(E_n-E_m)^2} 

1439 = -2\,\mathrm{Im}\langle\partial_x u_n|\partial_y u_n\rangle 

1440 

1441 the Berry curvature of band *n* (:math:`\partial_\mu H = \partial H/\partial k_\mu`, 

1442 the convention :math:`\Omega = \partial_xA_y - \partial_yA_x`, 

1443 :math:`\mathbf{A} = i\langle u|\nabla_{\mathbf{k}}u\rangle` of 

1444 *berry_curvature*). The form with :math:`f_n - f_m` stays finite 

1445 where bands touch: degenerate pairs have equal occupations and 

1446 drop out, instead of cancelling between two diverging 

1447 :math:`\Omega_n`. :math:`\partial H/\partial\mathbf{k}` is built 

1448 analytically from the stored hoppings, :math:`\sum_{\mathbf{R}} 

1449 i\mathbf{d}\,t\,e^{i\mathbf{k}\cdot\mathbf{d}}` with the Cartesian bond 

1450 vector :math:`\mathbf{d}`. 

1451 

1452 **Sign.** In a gap, :math:`\sigma_{xy} = C\,e^2/h` with :math:`C` 

1453 the value *chern_number* returns for the occupied bands: this is the 

1454 sign of TKNN (Phys. Rev. Lett. 49, 405 (1982)), whose 

1455 :math:`\sigma_H = (e^2/h)\,\frac{i}{2\pi}\int d^2k\,(\langle\partial_1u|\partial_2u\rangle 

1456 - \langle\partial_2u|\partial_1u\rangle)` has the same integrand, 

1457 :math:`\Omega/2\pi`. The adiabatic (semiclassical) response of a 

1458 charge :math:`q` of either sign, :math:`\dot{\mathbf{r}} = \partial_{\mathbf{k}}E 

1459 - \dot{\mathbf{k}}\times\boldsymbol\Omega` with 

1460 :math:`\hbar\dot{\mathbf{k}} = q\mathbf{E}`, gives the opposite sign 

1461 for the tensor of Ohm's law :math:`j_x = \sigma^{\mathrm{Ohm}}_{xy}E_y`: 

1462 :math:`\sigma^{\mathrm{Ohm}}_{xy} = -(q^2/\hbar)\int F\,d^dk/(2\pi)^d`, 

1463 the convention of Wang, Yates, Souza and Vanderbilt (Phys. Rev. B 

1464 74, 195118 (2006)) and of the Kubo-Bastin formula (*tbkit.kpm.hall_conductivity* 

1465 returns, likewise, minus it). So the value returned is 

1466 :math:`\sigma^{\mathrm{Ohm}}_{yx} = -\sigma^{\mathrm{Ohm}}_{xy}`; e.g. a 

1467 massive Dirac cone :math:`v(k_x\sigma_x + k_y\sigma_y) + m\sigma_z` 

1468 gives :math:`+\frac{e^2}{2h}\frac{m}{|E_F|}` outside its gap (the 

1469 literature's :math:`\sigma_{xy} = -\frac{e^2}{2h}\frac{m}{|E_F|}`), and 

1470 :math:`\frac{e^2}{2h}\mathrm{sgn}(m)` inside. 

1471 

1472 **Orbital positions.** The velocity is :math:`\partial H/\partial\mathbf{k}` 

1473 in the gauge of *positions*: with ``positions=True`` (the default) 

1474 the bond vector is :math:`\mathbf{d} = \mathbf{R} + \boldsymbol\tau_j 

1475 - \boldsymbol\tau_i`, the matrix of the physical velocity 

1476 :math:`i[H, \mathbf{r}]` of a tight-binding model whose position 

1477 operator is diagonal on its orbitals (the one *kpm.hall_conductivity* 

1478 uses on a torus). With ``positions=False``, every orbital sits on 

1479 its cell origin (the periodic gauge of *get_ham*). The two differ by 

1480 :math:`i(E_n-E_m)\langle n|\boldsymbol\tau|m\rangle` in the 

1481 velocity's matrix elements, so the band curvatures differ by 

1482 :math:`\nabla\times\langle n|\boldsymbol\tau|n\rangle`, a total 

1483 derivative: the conductivities agree whenever the bands below 

1484 :math:`E_F` are all full (in a gap, at any temperature well below 

1485 the gap) and differ in general at partial filling, by the circulation 

1486 :math:`\frac{1}{2\pi}\oint_{FS}\langle n|\boldsymbol\tau|n\rangle\cdot d\mathbf{k}` 

1487 around the Fermi surface. The default is the physical one. 

1488 

1489 **Units and dimensions.** In 2D, :math:`\sigma` is in units of 

1490 :math:`e^2/h` (:math:`\frac{1}{2\pi}\int d^2k\,F`). In 3D, with 

1491 *k_fixed* given, the value of the plane (the conductance of that 

1492 slice of the Brillouin zone, in :math:`e^2/h`, oriented like 

1493 *chern_number* along :math:`\mathbf{a}_l`); with ``k_fixed=None``, 

1494 the full 3D conductivity as the Hall (pseudo)vector 

1495 :math:`(\sigma_{yz}, \sigma_{zx}, \sigma_{xy}) = \frac{1}{(2\pi)^2}\int d^3k\,\mathbf{F}` 

1496 in units of :math:`e^2/(h\cdot\mathrm{length})` (length the unit of 

1497 *prim_vec*): :math:`\frac{1}{2\pi}\sum_l\bar C_l\,\mathbf{b}_l`, e.g. 

1498 :math:`C/c` along :math:`z` for a stack of Chern layers :math:`c` apart. 

1499 

1500 **Convergence.** The integrand peaks where bands nearly touch close 

1501 to :math:`E_F` (hot spots), and jumps across the Fermi surface. A 

1502 *temperature* smooths the latter. *refine* resamples the fraction 

1503 *refine_fraction* of mesh cells with the largest :math:`|F|` on a 

1504 finer submesh (*refine* points per direction), in the spirit of the 

1505 adaptive mesh refinement of Wang, Yates, Souza and Vanderbilt 

1506 (2006); the cells are ranked by the size of the curvature whatever 

1507 the occupations. It is meant for metals: in a gap the plain uniform 

1508 mesh already converges exponentially, and refining part of it 

1509 costs a little accuracy (about :math:`4\cdot10^{-4}` for the 

1510 Haldane model on a 60 x 60 mesh). With an overlap (*set_overlap*), :math:`H` is first 

1511 Lowdin-orthogonalized, :math:`S^{-1/2}HS^{-1/2}`, as by the topology 

1512 tools (exact derivatives of :math:`S^{-1/2}` included). 

1513 

1514 :param e_fermi: Real number, or array of real numbers. Fermi energies 

1515 (all of them are computed from one diagonalization of the mesh). 

1516 :param temperature: Positive real or zero. Default value 0. In 

1517 energy units (:math:`k_B = 1`). 

1518 :param nk: Positive integer, or tuple (two integers for a plane, 

1519 three for the full 3D conductivity). Default value 60. k-mesh. 

1520 :param plane: Tuple of two integers. Default value (0, 1). 3D only: 

1521 the reciprocal vectors spanning the plane (see *berry_curvature*). 

1522 :param k_fixed: Real number or None. Default value None. 3D only: 

1523 the fractional coordinate of the plane along the remaining 

1524 reciprocal vector; None for the full 3D conductivity. Ignored in 2D. 

1525 :param positions: Boolean. Default value True. Include the orbital 

1526 positions in the bond vectors (see above). 

1527 :param refine: Positive integer. Default value 1 (no refinement). 

1528 Submesh points per direction in the refined cells. 

1529 :param refine_fraction: Real in (0, 1]. Default value 0.05. Fraction 

1530 of the mesh cells that are refined. 

1531 

1532 :returns: 

1533 * **sigma** -- Real number (or ndarray shaped like *e_fermi*); in 

1534 3D with ``k_fixed=None``, ndarray of shape (3,) (or 

1535 ``e_fermi.shape + (3,)``). 

1536 

1537 Example usage:: 

1538 

1539 e_f = np.linspace(-3., 3., 301) 

1540 sigma = hal.hall_conductivity(e_f, nk=120) # plateau at C in the gap 

1541 ''' 

1542 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1543 refine_fraction, None) 

1544 

1545 def spin_hall_conductivity( 

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

1547 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1548 spin_axis: str = 'z', refine: int = 1, refine_fraction: float = 0.05, 

1549 ) -> float | NDArray[np.float64]: 

1550 r''' 

1551 Get the intrinsic spin Hall conductivity of a spinful model 

1552 (``spin=True``): the Kubo formula of *hall_conductivity* with the 

1553 charge current along :math:`x` replaced by the spin current 

1554 (Sinova et al., Phys. Rev. Lett. 92, 126603 (2004)) 

1555 

1556 .. math:: 

1557 

1558 j^{s}_x = \tfrac12\{s, v_x\}\, ,\qquad s = \tfrac{\hbar}{2}\sigma_{a}\, , 

1559 

1560 i.e. :math:`\sigma^{s}_{xy} = \frac{e}{2\pi}\cdot\frac{1}{2\pi}\int d^2k\,F^s`, 

1561 :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` 

1562 (:math:`j^s_x` with :math:`s = \sigma_a/2`, in units of :math:`\hbar`). 

1563 The unit is :math:`e/2\pi`: one :math:`e/\hbar` from the electric 

1564 field, one :math:`\hbar/2` from the spin, and :math:`1/2\pi` from the 

1565 Brillouin-zone integral, as for :math:`e^2/h = (e^2/\hbar)/2\pi`. 

1566 When :math:`s_z` is conserved, the two spins decouple and 

1567 :math:`j^s_x = \pm\frac{\hbar}{2}v_x` in each, so in a gap 

1568 :math:`\sigma^s_{xy} = \frac{e}{2\pi}\cdot\frac{C_\uparrow - C_\downarrow}{2}`, 

1569 :math:`C_\sigma` the Chern number of each spin sector (the sign of 

1570 *hall_conductivity* and *chern_number*): :math:`\pm e/2\pi` for the 

1571 Kane-Mele quantum spin Hall insulator (Kane and Mele 2005). Rashba 

1572 coupling breaks the conservation of :math:`s_z`, and the value is 

1573 then no longer quantized (the :math:`\mathbb{Z}_2` invariant, see 

1574 *z2_invariant*, still is). 

1575 

1576 :param e_fermi: See *hall_conductivity*. 

1577 :param temperature: See *hall_conductivity*. 

1578 :param nk: Positive integer, or tuple of 2 integers. Default value 60. 

1579 :param plane: See *hall_conductivity*. 

1580 :param k_fixed: Real number or None. Default value None. 3D only: the 

1581 plane (required in 3D, where only planes are computed). 

1582 :param positions: Boolean. Default value True. See *hall_conductivity*. 

1583 :param spin_axis: 'x', 'y' or 'z'. Default value 'z'. Spin component 

1584 :math:`a` carried by the current. 

1585 :param refine: See *hall_conductivity*. 

1586 :param refine_fraction: See *hall_conductivity*. 

1587 

1588 :returns: 

1589 * **sigma_s** -- Real number (or ndarray shaped like *e_fermi*), in 

1590 units of :math:`e/2\pi`. 

1591 ''' 

1592 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1593 refine_fraction, spin_axis) 

1594 

1595 # ------------------------------------------------------------------ 

1596 # Berry-phase response beyond the Hall conductivity 

1597 # ------------------------------------------------------------------ 

1598 

1599 def orbital_magnetization( 

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 orbital magnetization in the modern theory (Thonhauser, 

1606 Ceresoli, Vanderbilt and Resta, Phys. Rev. Lett. 95, 137205 (2005); 

1607 Xiao, Shi and Niu, Phys. Rev. Lett. 95, 137204 (2005); Ceresoli et 

1608 al., Phys. Rev. B 74, 024408 (2006)), at any Fermi level and 

1609 temperature: 

1610 

1611 .. math:: 

1612 

1613 M = \frac{e}{h}\,\frac{1}{2\pi}\int_{BZ} d^2k\sum_n\left[f_n\,m_n 

1614 + g_n\,\Omega_n\right],\qquad 

1615 m_n = \mathrm{Im}\langle\partial_xu_n|(H-E_n)|\partial_yu_n\rangle\, , 

1616 

1617 with :math:`g = k_BT\ln(1+e^{-(E-\mu)/k_BT})` (:math:`\mu - E` below 

1618 :math:`\mu` and 0 above, at :math:`T = 0`). The first term is the 

1619 self-rotation of the wavepackets, the second the circulation of 

1620 the Berry-curvature currents at the edges. At :math:`T = 0` both 

1621 combine into :math:`\sum_nf_n\,\mathrm{Im}\langle\partial_xu_n|(H+E_n-2\mu)|\partial_yu_n\rangle`, 

1622 the formula of Thonhauser et al. It is evaluated, like 

1623 *hall_conductivity*, from :math:`\langle n|\partial H|m\rangle` 

1624 (no derivatives of the eigenstates), so it is finite where bands 

1625 touch. 

1626 

1627 **Sign and units.** The sign is that of *hall_conductivity*, so 

1628 that the Streda formula reads :math:`\partial M/\partial\mu = \sigma`: 

1629 in a gap (:math:`T = 0`), :math:`M` is linear in :math:`\mu` with 

1630 slope the Chern number :math:`C`. In 2D, :math:`M` (a magnetic 

1631 moment per area, i.e. a current) is in units of :math:`e/h` times 

1632 the energy unit; with ``k_fixed=None`` in 3D, the vector 

1633 :math:`(M_x, M_y, M_z)` per length. *positions* matters as for 

1634 *hall_conductivity*, and the physical choice is the default. 

1635 

1636 :param e_fermi: See *hall_conductivity*. 

1637 :param temperature: See *hall_conductivity*. 

1638 :param nk: See *hall_conductivity*. 

1639 :param plane: See *hall_conductivity*. 

1640 :param k_fixed: See *hall_conductivity*. 

1641 :param positions: See *hall_conductivity*. 

1642 :param refine: See *hall_conductivity*. 

1643 :param refine_fraction: See *hall_conductivity*. 

1644 

1645 :returns: 

1646 * **M** -- Real number (or ndarray shaped like *e_fermi*); in 3D 

1647 with ``k_fixed=None``, ndarray of shape (3,) (or ``e_fermi.shape + (3,)``). 

1648 

1649 Example usage:: 

1650 

1651 mu = np.linspace(-0.5, 0.5, 11) # in the gap of a Chern insulator 

1652 M = hal.orbital_magnetization(mu) # slope C = hall_conductivity(mu) 

1653 ''' 

1654 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1655 refine_fraction, None, 'magnetization') 

1656 

1657 def anomalous_nernst_conductivity( 

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

1659 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1660 refine: int = 1, refine_fraction: float = 0.05, 

1661 ) -> float | NDArray[np.float64]: 

1662 r''' 

1663 Get the intrinsic anomalous Nernst (transverse thermoelectric) 

1664 conductivity :math:`\alpha_{xy}`, :math:`j_x = \alpha_{xy}(-\partial_yT)`, 

1665 from the Berry curvature weighted by the entropy of each state 

1666 (Xiao, Yao, Fang and Niu, Phys. Rev. Lett. 97, 026603 (2006)): 

1667 

1668 .. math:: 

1669 

1670 \alpha_{xy} = \frac{ek_B}{h}\,\frac{1}{2\pi}\int_{BZ}d^2k\sum_n s_n\,\Omega_n\, , 

1671 \qquad s = -f\ln f - (1-f)\ln(1-f)\, . 

1672 

1673 Only the states within a few :math:`k_BT` of :math:`\mu` carry 

1674 entropy, so it vanishes in a gap at low temperature. It obeys the 

1675 Mott relation exactly, 

1676 :math:`\alpha_{xy}(\mu, T) = \frac{1}{eT}\int dE\,(E-\mu)\left(-\frac{\partial f}{\partial E}\right)\sigma_{xy}(E)`, 

1677 with :math:`\sigma_{xy}(E)` the :math:`T = 0` *hall_conductivity* 

1678 at Fermi energy :math:`E`, which fixes the sign. The pair form of 

1679 *hall_conductivity* keeps it finite where bands touch. 

1680 

1681 **Units.** :math:`ek_B/h` in 2D (:math:`k_B = 1`, temperatures in 

1682 energy units), per length for the 3D vector (``k_fixed=None``). 

1683 

1684 :param e_fermi: See *hall_conductivity*. 

1685 :param temperature: Positive real number. In energy units (:math:`k_B = 1`). 

1686 :param nk: See *hall_conductivity*. 

1687 :param plane: See *hall_conductivity*. 

1688 :param k_fixed: See *hall_conductivity*. 

1689 :param positions: See *hall_conductivity*. 

1690 :param refine: See *hall_conductivity*. 

1691 :param refine_fraction: See *hall_conductivity*. 

1692 

1693 :returns: 

1694 * **alpha** -- Real number (or ndarray, as *hall_conductivity*). 

1695 

1696 Example usage:: 

1697 

1698 alpha = hal.anomalous_nernst_conductivity(np.linspace(-3, 3, 301), 0.05) 

1699 ''' 

1700 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1701 refine_fraction, None, 'nernst') 

1702 

1703 def thermal_hall_conductivity( 

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

1705 plane: tuple[int, int] = (0, 1), k_fixed: float | None = None, positions: bool = True, 

1706 refine: int = 1, refine_fraction: float = 0.05, 

1707 ) -> float | NDArray[np.float64]: 

1708 r''' 

1709 Get the intrinsic thermal Hall conductivity of the electrons 

1710 :math:`\kappa_{xy}`, :math:`j^Q_x = \kappa_{xy}(-\partial_yT)` 

1711 (Qin, Niu and Shi, Phys. Rev. Lett. 107, 236601 (2011)): 

1712 

1713 .. math:: 

1714 

1715 \kappa_{xy} = \frac{k_B^2T}{h}\,\frac{1}{2\pi}\int_{BZ}d^2k 

1716 \sum_n c_2(x_n)\,\Omega_n\, ,\qquad 

1717 c_2(x) = \int_x^\infty y^2\left(-\frac{\partial f}{\partial y}\right)dy\, , 

1718 

1719 :math:`x_n = (E_n-\mu)/k_BT` (:math:`c_2 = \pi^2/3` deep below 

1720 :math:`\mu`, 0 far above). Equivalently, exactly, 

1721 :math:`\kappa_{xy}(\mu, T) = \frac{1}{e^2T}\int dE\,(E-\mu)^2\left(-\frac{\partial f}{\partial E}\right)\sigma_{xy}(E)` 

1722 with the :math:`T = 0` *hall_conductivity*. In a gap at low 

1723 temperature it is the Wiedemann-Franz value 

1724 :math:`\kappa_{xy} = \frac{\pi^2k_B^2}{3e^2}T\sigma_{xy}`: for a Chern 

1725 insulator the quantized :math:`C\,\pi^2k_B^2T/3h`. 

1726 

1727 **Units.** :math:`k_B^2/h` times the temperature (in energy units, 

1728 :math:`k_B = 1`) in 2D, so that :math:`\kappa/T = \pi^2C/3` on a 

1729 Chern plateau; per length for the 3D vector (``k_fixed=None``). 

1730 The sign is that of *hall_conductivity*. 

1731 

1732 :param e_fermi: See *hall_conductivity*. 

1733 :param temperature: Positive real number. In energy units (:math:`k_B = 1`). 

1734 :param nk: See *hall_conductivity*. 

1735 :param plane: See *hall_conductivity*. 

1736 :param k_fixed: See *hall_conductivity*. 

1737 :param positions: See *hall_conductivity*. 

1738 :param refine: See *hall_conductivity*. 

1739 :param refine_fraction: See *hall_conductivity*. 

1740 

1741 :returns: 

1742 * **kappa** -- Real number (or ndarray, as *hall_conductivity*). 

1743 

1744 Example usage:: 

1745 

1746 kappa = hal.thermal_hall_conductivity(0., 0.01) # = pi^2/3 C T in the gap 

1747 ''' 

1748 return self._hall(e_fermi, temperature, nk, plane, k_fixed, positions, refine, 

1749 refine_fraction, None, 'thermal') 

1750 

1751 def _axion_rate( 

1752 self, ks: NDArray[np.float64], bands: list[int], params: dict, param: str, 

1753 positions: bool, 

1754 ) -> float: 

1755 r''' 

1756 Private method. The mesh average, over *ks*, of 

1757 :math:`\mathrm{tr}[F_{\lambda x}F_{yz} + F_{\lambda y}F_{zx} + F_{\lambda z}F_{xy}]` 

1758 for the bands *bands* at the parameters *params*, the derivative 

1759 along *param* by central differences. The non-Abelian curvature is 

1760 :math:`F_{\mu\nu} = i(X_\mu^\dagger X_\nu - X_\nu^\dagger X_\mu)`, 

1761 :math:`(X_\mu)_{lm} = \langle l|\partial_\mu H|m\rangle/(E_m-E_l)` 

1762 (:math:`m` in *bands*, :math:`l` not), i.e. 

1763 :math:`Q|\partial_\mu u_m\rangle` in the eigenbasis: gauge covariant. 

1764 ''' 

1765 value = params[param] 

1766 step = 1e-5 * max(1., abs(value)) 

1767 others = [b for b in range(self.norb) if b not in bands] 

1768 total, gap = 0., np.inf 

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

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

1771 k = ks[c0:c0 + chunk] 

1772 ham, dham = self._bloch_derivatives(k, list(np.eye(3)), positions, params) 

1773 plus = self._bloch_derivatives(k, [], positions, dict(params, **{param: value + step}))[0] 

1774 minus = self._bloch_derivatives(k, [], positions, dict(params, **{param: value - step}))[0] 

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

1776 occ, emp = vec[:, :, bands], vec[:, :, others] 

1777 de = en[:, others][:, :, None] - en[:, bands][:, None, :] 

1778 gap = min(gap, np.abs(de).min()) 

1779 emp_h = emp.conj().transpose(0, 2, 1) 

1780 x = [-(emp_h @ d @ occ) / de for d in [(plus - minus) / (2 * step), *dham]] 

1781 f = {} 

1782 for a, b in ((0, 1), (0, 2), (0, 3), (1, 2), (2, 3), (3, 1)): 

1783 xa_h = x[a].conj().transpose(0, 2, 1) 

1784 xb_h = x[b].conj().transpose(0, 2, 1) 

1785 f[a, b] = 1j * (xa_h @ x[b] - xb_h @ x[a]) 

1786 trace = (np.einsum('kab,kba->k', f[0, 1], f[2, 3]) + np.einsum('kab,kba->k', f[0, 2], f[3, 1]) 

1787 + np.einsum('kab,kba->k', f[0, 3], f[1, 2])) 

1788 total += trace.real.sum() 

1789 error_handling.path_gap(gap, param, value) 

1790 return total / len(ks) 

1791 

1792 def axion_angle( 

1793 self, bands: list[int], param: str, values: ArrayLike, nk: int | tuple[int, int, int] = 16, 

1794 theta0: float = 0., positions: bool = True, 

1795 ) -> NDArray[np.float64]: 

1796 r''' 

1797 Get the axion angle :math:`\theta` of a 3D insulator along a path of 

1798 a parameter of its value functions (*set_hopping*), from the second 

1799 Chern form (Qi, Hughes and Zhang, Phys. Rev. B 78, 195424 (2008); 

1800 Essin, Moore and Vanderbilt, Phys. Rev. Lett. 102, 146805 (2009)): 

1801 

1802 .. math:: 

1803 

1804 \frac{d\theta}{d\lambda} = \frac{1}{2\pi}\int_{BZ}d^3k\; 

1805 \mathrm{tr}\left[F_{\lambda x}F_{yz} + F_{\lambda y}F_{zx} + F_{\lambda z}F_{xy}\right], 

1806 

1807 :math:`F_{\mu\nu}` the non-Abelian Berry curvature of the bands 

1808 *bands* in :math:`(\lambda, k_x, k_y, k_z)` space, so that 

1809 :math:`\theta(\lambda) = \theta_0 + \int_{\lambda_0}^{\lambda}\frac{d\theta}{d\lambda'}d\lambda'`. 

1810 This is the variation of the Chern-Simons axion coupling, computed 

1811 from gauge-invariant quantities only (no smooth gauge is needed), 

1812 and :math:`\theta_0` is its value at ``values[0]``: e.g. 0 

1813 for a path starting from an atomic insulator. A closed path (a 

1814 pumping cycle) changes :math:`\theta` by :math:`2\pi C_2`, with 

1815 :math:`C_2` the second Chern number of the 4D family. At a point 

1816 with time-reversal or inversion symmetry, :math:`\theta` is 0 or 

1817 :math:`\pi` (mod :math:`2\pi`): :math:`\pi` for a strong topological 

1818 insulator, cf. *z2_indices_3d*. The sign of :math:`\theta` follows 

1819 the orientation :math:`(\lambda, k_x, k_y, k_z)` above. 

1820 

1821 The path must stay gapped: the bands *bands* must be separated 

1822 from the others at every k-point and every value (a ValueError 

1823 names the value where they touch). The integrand is smooth, so the 

1824 uniform k-mesh converges exponentially; the :math:`\lambda` 

1825 integral is a trapezoid over *values* (error :math:`O(\Delta\lambda^2)`). 

1826 :math:`\partial H/\partial\lambda` is a central difference, and 

1827 :math:`\partial H/\partial\mathbf{k}` is exact (see 

1828 *hall_conductivity*, whose *positions* convention applies). 

1829 

1830 :param bands: List of integers. The occupied bands (e.g. the lower 

1831 half of the spectrum). 

1832 :param param: String. Name of the parameter of the value functions 

1833 that the path varies (see *set_params*; the other parameters keep 

1834 their values in *params*). 

1835 :param values: 1D array of at least two real numbers, increasing 

1836 or decreasing. The parameter values along the path. 

1837 :param nk: Positive integer, or tuple of 3 positive integers. 

1838 Default value 16. k-mesh. 

1839 :param theta0: Real number. Default value 0. :math:`\theta` at ``values[0]``. 

1840 :param positions: Boolean. Default value True. See *hall_conductivity*. 

1841 

1842 :returns: 

1843 * **theta** -- ndarray, shape ``(len(values),)``: :math:`\theta` 

1844 at each value, not reduced modulo :math:`2\pi`. 

1845 

1846 Example usage:: 

1847 

1848 phi = np.linspace(0., 2 * np.pi, 41) 

1849 theta = ks.axion_angle([0, 1], 'phi', phi) # theta[-1] = 2 pi C2 

1850 ''' 

1851 self._check_static() 

1852 error_handling.dim_exact(self.dim, 3) 

1853 error_handling.hermitian_model(self.is_hermitian()) 

1854 error_handling.band_indices(bands, self.norb) 

1855 error_handling.path_parameter(param) 

1856 error_handling.path_values(values) 

1857 if isinstance(nk, int): 

1858 nk = (nk,) * 3 

1859 error_handling.nk(nk, 3) 

1860 error_handling.real_number(theta0, 'theta0') 

1861 error_handling.boolean(positions, 'positions') 

1862 values = np.asarray(values, dtype='f8') 

1863 fracs = np.array(np.meshgrid(*[np.arange(n) / n for n in nk], indexing='ij')).reshape(3, -1).T 

1864 ks = fracs @ self.rec_vec_k 

1865 volume = abs(np.linalg.det(self.rec_vec_k)) 

1866 rates = [self._axion_rate(ks, bands, dict(self.params, **{param: float(v)}), param, positions) 

1867 for v in values] 

1868 return theta0 + cumulative_trapezoid(np.array(rates) * volume / (2 * PI), values, initial=0.) 

1869 

1870 # ------------------------------------------------------------------ 

1871 # Finite samples and non-Hermitian band theory 

1872 # ------------------------------------------------------------------ 

1873 

1874 def _hop_cells(self) -> list[tuple[int, int, tuple[int, ...], complex]]: 

1875 r''' 

1876 Private method. The stored hoppings with their lattice vector as 

1877 integers :math:`(n_1, ...)` (from :math:`\mathbf{R}\cdot\mathbf{b}_i/2\pi`). 

1878 ''' 

1879 rec = np.array(self.rec_vec, dtype='f8') 

1880 return [(i, j, tuple(int(n) for n in np.rint(rec @ R / (2 * PI))), t) 

1881 for i, j, R, t in self._hop] 

1882 

1883 def _finite_entries( 

1884 self, n_cells: int | tuple[int, ...], periodic: bool, 

1885 ) -> tuple[tuple[int, ...], int, list[tuple[NDArray, NDArray, int, int, tuple, NDArray, complex]]]: 

1886 ''' 

1887 Private method. For each stored hopping, the row and column indices 

1888 it fills in a finite sample of *n_cells* cells (see *finite_ham*), 

1889 with its orbitals, lattice vector (integers and Cartesian) and amplitude. 

1890 ''' 

1891 if isinstance(n_cells, int): 

1892 n_cells = (n_cells,) * self.dim 

1893 n_tot = int(np.prod(n_cells)) 

1894 cells = np.array(np.meshgrid(*[np.arange(n) for n in n_cells], indexing='ij')) 

1895 cells = cells.reshape(self.dim, -1).T # (n_tot, dim) 

1896 strides = np.cumprod((1,) + tuple(n_cells[:-1])) 

1897 index = cells @ strides 

1898 order = np.argsort(index) 

1899 cells = cells[order] 

1900 dims = np.array(n_cells) 

1901 entries = [] 

1902 for (i, j, R, t), (_, _, R_cart, _) in zip(self._hop_cells(), self._hop): 

1903 target = cells + np.array(R)[None, :] 

1904 if periodic: 

1905 target = target % dims 

1906 keep = np.ones(n_tot, bool) 

1907 else: 

1908 keep = np.all((target >= 0) & (target < dims), axis=1) 

1909 rows = np.arange(n_tot)[keep] * self.norb + i 

1910 cols = (target[keep] @ strides) * self.norb + j 

1911 entries.append((rows, cols, i, j, R, R_cart, t)) 

1912 return n_cells, n_tot, entries 

1913 

1914 def finite_ham( 

1915 self, n_cells: int | tuple[int, ...], periodic: bool = False, sparse: bool = False, 

1916 ) -> NDArray[np.complex128] | sp.csr_matrix: 

1917 r''' 

1918 Get the real-space Hamiltonian of a finite sample of 

1919 :math:`N_1\times N_2\times\dots` unit cells of the model -- open 

1920 boundaries (a flake, a finite chain), or periodic ones (a torus). 

1921 Orbital *o* of cell :math:`(n_1, n_2, n_3)` is row 

1922 :math:`((n_3 N_2 + n_2) N_1 + n_1)\,\mathrm{norb} + o`. 

1923 

1924 :param n_cells: Positive integer, or tuple of *dim* positive integers. 

1925 :param periodic: Boolean. Default value False. Periodic boundary 

1926 conditions (the spectrum is then that of *get_bands* on the 

1927 :math:`N_1\times N_2\times\dots` k-mesh), else open ones. 

1928 :param sparse: Boolean. Default value False. Return a sparse CSR 

1929 matrix (for large samples, e.g. with *tbkit.kpm*) instead of a 

1930 dense array. 

1931 

1932 :returns: 

1933 * **ham** -- Complex ndarray (or CSR matrix), shape (N*norb, N*norb), N the number of cells. 

1934 ''' 

1935 error_handling.nk(n_cells, self.dim) 

1936 error_handling.boolean(periodic, 'periodic') 

1937 error_handling.boolean(sparse, 'sparse') 

1938 _, n_tot, entries = self._finite_entries(n_cells, periodic) 

1939 onsite = np.diag(self.onsite) + self._onsite_offdiag 

1940 if sparse: 

1941 onsite_rows, onsite_cols = np.nonzero(onsite) 

1942 rows = [c * self.norb + onsite_rows for c in range(n_tot)] 

1943 cols = [c * self.norb + onsite_cols for c in range(n_tot)] 

1944 vals = [onsite[onsite_rows, onsite_cols]] * n_tot 

1945 for r, c, _, _, _, _, t in entries: 

1946 rows.append(r) 

1947 cols.append(c) 

1948 vals.append(np.full(len(r), t, dtype='c16')) 

1949 n = n_tot * self.norb 

1950 return sp.coo_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), 

1951 shape=(n, n)).tocsr() 

1952 ham = np.zeros((n_tot * self.norb, n_tot * self.norb), 'c16') 

1953 for cell in range(n_tot): 

1954 ham[cell*self.norb:(cell+1)*self.norb, cell*self.norb:(cell+1)*self.norb] += onsite 

1955 for r, c, _, _, _, _, t in entries: 

1956 np.add.at(ham, (r, c), t) 

1957 return ham 

1958 

1959 def finite_velocity( 

1960 self, n_cells: int | tuple[int, ...], periodic: bool = False, sparse: bool = False, 

1961 ) -> list[NDArray[np.complex128] | sp.csr_matrix]: 

1962 r''' 

1963 Get the velocity operators :math:`\mathbf{v} = i[H, \mathbf{r}]` 

1964 (:math:`\hbar = 1`) of the finite sample of *finite_ham*, from the 

1965 Cartesian bond vectors of the hoppings rather than from the site 

1966 coordinates: 

1967 

1968 .. math:: 

1969 

1970 (v_\alpha)_{ab} = i\,H_{ab}\,d_\alpha\, ,\qquad 

1971 \mathbf{d} = \mathbf{R} + \boldsymbol\tau_j - \boldsymbol\tau_i\, , 

1972 

1973 so that they are well defined on a torus (``periodic=True``), where 

1974 the position operator is not: a bond that wraps around the sample 

1975 keeps its short displacement. They are the real-space counterpart of 

1976 :math:`\partial H/\partial\mathbf{k}` with ``positions=True`` 

1977 (*hall_conductivity*), and the input of *tbkit.kpm.hall_conductivity*. 

1978 

1979 :param n_cells: See *finite_ham*. 

1980 :param periodic: Boolean. Default value False. See *finite_ham*. 

1981 :param sparse: Boolean. Default value False. See *finite_ham*. 

1982 

1983 :returns: 

1984 * **vel** -- List of *space_dim* complex ndarrays (or CSR 

1985 matrices), shape (N*norb, N*norb): :math:`v_x, v_y, (v_z)`. 

1986 ''' 

1987 error_handling.nk(n_cells, self.dim) 

1988 error_handling.boolean(periodic, 'periodic') 

1989 error_handling.boolean(sparse, 'sparse') 

1990 _, n_tot, entries = self._finite_entries(n_cells, periodic) 

1991 tau = self.orbital_positions() 

1992 n = n_tot * self.norb 

1993 vel = [] 

1994 for alpha in range(self.space_dim): 

1995 rows, cols, vals = [np.zeros(0, int)], [np.zeros(0, int)], [np.zeros(0, 'c16')] 

1996 for r, c, i, j, _, R_cart, t in entries: 

1997 d = R_cart[alpha] + tau[j, alpha] - tau[i, alpha] 

1998 rows.append(r) 

1999 cols.append(c) 

2000 vals.append(np.full(len(r), 1j * t * d, dtype='c16')) 

2001 mat = sp.coo_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), 

2002 shape=(n, n)).tocsr() 

2003 vel.append(mat if sparse else mat.toarray()) 

2004 return vel 

2005 

2006 def get_ham_beta(self, beta: complex) -> NDArray[np.complex128]: 

2007 r''' 

2008 Get the non-Bloch Hamiltonian of a 1D model, 

2009 :math:`H(\beta) = \sum_{n} t(n)\, \beta^{n}`: the Bloch Hamiltonian 

2010 continued off the unit circle, :math:`\beta = e^{ik}` with complex 

2011 :math:`k`. On the generalized Brillouin zone (see *gbz*), its 

2012 spectrum is the open-chain spectrum of a non-Hermitian chain. 

2013 

2014 :param beta: Complex number, nonzero. 

2015 

2016 :returns: 

2017 * **ham** -- Complex ndarray, shape (norb, norb). 

2018 ''' 

2019 error_handling.dim_exact(self.dim, 1) 

2020 error_handling.nonzero_number(beta, 'beta') 

2021 ham = np.diag(self.onsite).astype('c16') + self._onsite_offdiag 

2022 for i, j, (n,), t in self._hop_cells(): 

2023 ham[i, j] += t * complex(beta) ** n 

2024 return ham 

2025 

2026 def spectral_winding(self, e_ref: complex = 0., nk: int = 400) -> float: 

2027 r''' 

2028 Get the spectral winding number of a 1D (non-Hermitian) model about 

2029 the reference energy :math:`E_r`, 

2030 

2031 .. math:: 

2032 

2033 W(E_r) = \frac{1}{2\pi i}\oint_0^{2\pi} dk\, 

2034 \partial_k \ln\det\left[H(k) - E_r\right]\, , 

2035 

2036 the number of times the periodic-chain spectrum winds around 

2037 :math:`E_r` in the complex plane. :math:`W \neq 0` for some 

2038 :math:`E_r` is the criterion for the non-Hermitian skin effect: the 

2039 open-chain eigenstates then pile up at one end (Okuma, Kawabata, 

2040 Shiozaki and Sato, Phys. Rev. Lett. 124, 086801 (2020)). 

2041 

2042 :param e_ref: Complex number. Default value 0. Reference energy. 

2043 :param nk: Positive integer. Default value 400. k-points. 

2044 

2045 :returns: 

2046 * **W** -- Real number, close to an integer (it is ill defined if 

2047 :math:`E_r` lies on the periodic spectrum). 

2048 ''' 

2049 error_handling.dim_exact(self.dim, 1) 

2050 error_handling.number(e_ref, 'e_ref') 

2051 error_handling.positive_int(nk, 'nk') 

2052 ks = np.arange(nk + 1) / nk * self.rec_vec_k[0, 0] 

2053 dets = np.array([np.linalg.det(self.get_ham([k]) - e_ref * np.eye(self.norb)) for k in ks]) 

2054 steps = np.angle(dets[1:] / dets[:-1]) 

2055 return float(np.sum(steps) / (2 * PI)) 

2056 

2057 def gbz(self, energies: ArrayLike) -> NDArray[np.complex128]: 

2058 r''' 

2059 Get the generalized Brillouin zone of a 1D non-Hermitian model 

2060 (Yao and Wang, Phys. Rev. Lett. 121, 086803 (2018); Yokomizo and 

2061 Murakami, Phys. Rev. Lett. 123, 066404 (2019)): for each energy 

2062 :math:`E`, the roots of :math:`\det[H(\beta)-E] = 0` sorted by modulus, 

2063 :math:`|\beta_1|\le\dots\le|\beta_{p+s}|` (:math:`p` the order of its 

2064 pole at :math:`\beta = 0`), and the middle pair 

2065 :math:`\beta_p, \beta_{p+1}`. The open-chain spectrum, in the limit of 

2066 a long chain, is where :math:`|\beta_p| = |\beta_{p+1}|`; those 

2067 :math:`\beta` trace the generalized Brillouin zone, on which the 

2068 non-Bloch bands :math:`H(\beta)` (see *get_ham_beta*) replace the 

2069 Bloch ones. For a Hermitian model it is the unit circle. 

2070 

2071 :param energies: Complex array of energies (e.g. open-chain eigenvalues 

2072 from *finite_ham*). 

2073 

2074 :returns: 

2075 * **beta** -- Complex ndarray, shape (len(energies), 2): :math:`\beta_p, \beta_{p+1}`. 

2076 ''' 

2077 error_handling.dim_exact(self.dim, 1) 

2078 energies = np.atleast_1d(np.asarray(energies, dtype='c16')) 

2079 error_handling.ndarray_empty(energies, 'energies') 

2080 powers = [n for _, _, (n,), _ in self._hop_cells()] + [0] 

2081 low, high = self.norb * min(powers), self.norb * max(powers) 

2082 n_samples = 2 * (high - low) + 8 

2083 theta = 2 * PI * np.arange(n_samples) / n_samples 

2084 hams = np.array([self.get_ham_beta(np.exp(1j * th)) for th in theta]) 

2085 out = np.zeros((len(energies), 2), 'c16') 

2086 exps = np.arange(low, high + 1) 

2087 for n, e in enumerate(energies): 

2088 dets = np.linalg.det(hams - e * np.eye(self.norb)[None]) 

2089 # Laurent coefficients c_m of det[H(beta) - E] = sum_m c_m beta^m 

2090 coef = np.array([np.mean(dets * np.exp(-1j * m * theta)) for m in exps]) 

2091 nonzero = np.abs(coef) > 1e-12 * np.max(np.abs(coef)) 

2092 first, last = np.argmax(nonzero), len(coef) - 1 - np.argmax(nonzero[::-1]) 

2093 p = -exps[first] 

2094 roots = np.roots(coef[first:last + 1][::-1]) 

2095 roots = roots[np.argsort(np.abs(roots))] 

2096 error_handling.gbz_roots(len(roots), p) 

2097 out[n] = roots[p - 1], roots[p] 

2098 return out 

2099 

2100 # ------------------------------------------------------------------ 

2101 # Berry phases, Wannier centres, Z2, symmetries, quantum geometry 

2102 # ------------------------------------------------------------------ 

2103 

2104 def orbital_positions(self) -> NDArray[np.float64]: 

2105 r''' 

2106 Get the positions :math:`\boldsymbol\tau` of the orbitals within the 

2107 unit cell (each site twice, for its two spin components, if 

2108 ``spin=True``). 

2109 

2110 :returns: 

2111 * **tau** -- Real ndarray, shape (norb, space_dim). 

2112 ''' 

2113 tau = np.array([dic['r0'] for dic in self.lat.unit_cell], dtype='f8') 

2114 return np.repeat(tau, 2, axis=0) if self.spin else tau 

2115 

2116 def _loop( 

2117 self, bands: list[int], nk: int, direction: int, fracs_perp: dict[int, float], 

2118 positions: bool, 

2119 ) -> NDArray[np.complex128]: 

2120 ''' 

2121 Private method. Wilson loop matrix of *bands* along the closed path 

2122 k0 -> k0 + b_direction (nk steps), at fixed fractional coordinates 

2123 *fracs_perp* along the other reciprocal vectors. 

2124 ''' 

2125 frac = np.zeros(self.dim) 

2126 for d, f in fracs_perp.items(): 

2127 frac[d] = f 

2128 b = self.rec_vec_k[direction] 

2129 k0 = frac @ self.rec_vec_k 

2130 # each link carries exp(-i dk.tau): the Bloch sums then include the 

2131 # orbital positions, so that the phases locate the Wannier centres 

2132 # in space rather than on the cell origin. 

2133 dk_space = self.k_basis @ (b / nk) 

2134 phase = np.exp(-1j * self.orbital_positions() @ dk_space) if positions \ 

2135 else np.ones(self.norb) 

2136 vs = self._subspaces(k0 + np.arange(nk)[:, None] * b[None] / nk, bands) 

2137 links = vs.conj().transpose(0, 2, 1) @ (phase[:, None] * np.roll(vs, -1, axis=0)) 

2138 # keep only the unitary part of each link (its singular values 

2139 # tend to 1 as nk grows): the product then stays unitary 

2140 u, _, vh = np.linalg.svd(links) 

2141 wilson = np.eye(len(bands), dtype='c16') 

2142 for unitary in u @ vh: 

2143 wilson = wilson @ unitary 

2144 return wilson 

2145 

2146 def _perp(self, direction: int, k_perp: float | tuple | None) -> dict[int, float]: 

2147 ''' 

2148 Private method. Fractional coordinates of a loop along the other 

2149 reciprocal vectors, from *k_perp* (a number in 2D, a pair in 3D). 

2150 ''' 

2151 others = [d for d in range(self.dim) if d != direction] 

2152 if k_perp is None: 

2153 k_perp = (0.,) * len(others) 

2154 if not isinstance(k_perp, tuple): 

2155 k_perp = (k_perp,) 

2156 error_handling.k_perp(k_perp, len(others)) 

2157 return dict(zip(others, k_perp)) 

2158 

2159 def berry_phase( 

2160 self, bands: int | list[int], nk: int = 100, direction: int = 0, 

2161 k_perp: float | tuple[float, float] | None = None, positions: bool = True, 

2162 ) -> float: 

2163 r''' 

2164 Get the Berry phase of a group of bands along a closed loop across the 

2165 Brillouin zone, :math:`\mathbf{k}\to\mathbf{k}+\mathbf{b}_{direction}`, 

2166 

2167 .. math:: 

2168 

2169 \gamma = \oint \mathbf{A}\cdot d\mathbf{k} = -\arg\det W\, ,\qquad 

2170 W = \prod_{m} \langle u(\mathbf{k}_m)|u(\mathbf{k}_{m+1})\rangle\, , 

2171 

2172 with :math:`\mathbf{A} = i\langle u|\nabla_{\mathbf{k}}u\rangle` (the 

2173 convention of *berry_curvature*). In 1D it is the Zak phase: its 

2174 value :math:`\gamma/2\pi`, modulo 1, is the centre of the band's 

2175 Wannier functions in units of :math:`\mathbf{a}_{direction}` (the 

2176 polarization). 

2177 

2178 :param bands: Band index, or list of band indices. 

2179 :param nk: Positive integer. Default value 100. Number of k-points on the loop. 

2180 :param direction: Integer. Default value 0. Reciprocal vector the loop follows. 

2181 :param k_perp: Real number (2D) or pair of real numbers (3D). Default 

2182 value None (zero). Fractional coordinates of the loop along the 

2183 other reciprocal vectors. 

2184 :param positions: Boolean. Default value True. Include the orbital 

2185 positions in the Bloch functions (the physical choice: the phase 

2186 then locates the Wannier centres in the crystal). If False, all 

2187 orbitals are placed on the cell origin (the periodic gauge of 

2188 *get_ham*); the two differ by :math:`\sum_n\mathbf{b}\cdot\boldsymbol\tau` 

2189 weighted by the orbital content, and agree on quantized 

2190 differences between phases. 

2191 

2192 :returns: 

2193 * **gamma** -- Real number in :math:`(-\pi, \pi]`. 

2194 ''' 

2195 bands = self._check_loop(bands, nk, direction, positions) 

2196 wilson = self._loop(bands, nk, direction, self._perp(direction, k_perp), positions) 

2197 return float(-np.angle(np.linalg.det(wilson))) 

2198 

2199 def wannier_centers( 

2200 self, bands: int | list[int], nk: int = 100, direction: int = 0, 

2201 k_perp: float | tuple[float, float] | None = None, positions: bool = True, 

2202 ) -> NDArray[np.float64]: 

2203 r''' 

2204 Get the (hybrid) Wannier centres of a group of bands: minus the 

2205 phases of the eigenvalues of the Wilson loop (see *berry_phase*), 

2206 divided by :math:`2\pi`. They are the positions, in units of 

2207 :math:`\mathbf{a}_{direction}` and modulo 1, of the maximally 

2208 localized Wannier functions along that direction (hybrid: still 

2209 Bloch waves along the others, at *k_perp*). Their sum is 

2210 :math:`\gamma/2\pi`, modulo 1. 

2211 

2212 :param bands: Band index, or list of band indices. 

2213 :param nk: Positive integer. Default value 100. Number of k-points on the loop. 

2214 :param direction: Integer. Default value 0. 

2215 :param k_perp: See *berry_phase*. 

2216 :param positions: Boolean. Default value True. See *berry_phase*. 

2217 

2218 :returns: 

2219 * **centers** -- Real ndarray, sorted, in :math:`[0, 1)`. 

2220 ''' 

2221 bands = self._check_loop(bands, nk, direction, positions) 

2222 wilson = self._loop(bands, nk, direction, self._perp(direction, k_perp), positions) 

2223 return np.sort((-np.angle(np.linalg.eigvals(wilson)) / (2*PI)) % 1.) 

2224 

2225 def wannier_flow( 

2226 self, bands: int | list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

2227 flow: int | None = None, k_fixed: float = 0., positions: bool = True, 

2228 k_range: tuple[float, float] = (0., 1.), 

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

2230 r''' 

2231 Get the flow of the hybrid Wannier centres (see *wannier_centers*) 

2232 along *direction*, as the loop is moved across the Brillouin zone 

2233 along the reciprocal vector *flow*. In a Chern insulator the centres 

2234 wind by :math:`C` cells over a full period; in a 

2235 :math:`\mathbb{Z}_2` topological insulator, the Kramers pairs swap 

2236 partners over half a period (see *z2_invariant*). 

2237 

2238 :param bands: Band index, or list of band indices. 

2239 :param nk: Positive integer. Default value 100. k-points on each loop. 

2240 :param nk_perp: Positive integer. Default value 51. Number of loops. 

2241 :param direction: Integer. Default value 0. Direction of the loops. 

2242 :param flow: Integer. Default value None (the first other direction). 

2243 Reciprocal vector along which the loops are moved. 

2244 :param k_fixed: Real number. Default value 0. 3D only: fractional 

2245 coordinate along the remaining reciprocal vector. 

2246 :param positions: Boolean. Default value True. See *berry_phase*. 

2247 :param k_range: Pair of real numbers. Default value (0, 1). Range of 

2248 the fractional coordinate along *flow* (both ends included). 

2249 

2250 :returns: 

2251 * **fracs** -- Real ndarray, shape (nk_perp,). Fractional coordinates along *flow*. 

2252 * **centers** -- Real ndarray, shape (nk_perp, len(bands)). Sorted centres, in [0, 1). 

2253 ''' 

2254 bands = self._check_loop(bands, nk, direction, positions) 

2255 error_handling.dim_min(self.dim, 2) 

2256 error_handling.positive_int(nk_perp, 'nk_perp') 

2257 others = [d for d in range(self.dim) if d != direction] 

2258 if flow is None: 

2259 flow = others[0] 

2260 error_handling.flow(flow, direction, self.dim) 

2261 error_handling.real_number(k_fixed, 'k_fixed') 

2262 error_handling.lims(k_range) 

2263 fracs = np.linspace(k_range[0], k_range[1], nk_perp) 

2264 centers = np.zeros((nk_perp, len(bands))) 

2265 for n, f in enumerate(fracs): 

2266 perp = {d: (f if d == flow else k_fixed) for d in others} 

2267 wilson = self._loop(bands, nk, direction, perp, positions) 

2268 centers[n] = np.sort((-np.angle(np.linalg.eigvals(wilson)) / (2*PI)) % 1.) 

2269 return fracs, centers 

2270 

2271 def z2_invariant( 

2272 self, bands: list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

2273 flow: int | None = None, k_fixed: float = 0., 

2274 ) -> int: 

2275 r''' 

2276 Get the :math:`\mathbb{Z}_2` invariant of a time-reversal-symmetric 

2277 group of bands (an even number, e.g. all occupied bands of the 

2278 Kane-Mele model) from the flow of its hybrid Wannier centres over 

2279 half the Brillouin zone (Soluyanov and Vanderbilt, Phys. Rev. B 83, 

2280 235401 (2011); Yu, Qi, Bernevig, Fang and Dai, Phys. Rev. B 84, 

2281 075119 (2011)): follow the midpoint of the largest gap between the 

2282 centres, and count, modulo 2, how many centres it jumps over. It is 

2283 odd when the Kramers partners exchange along the flow -- a 

2284 topological insulator -- and even otherwise. 

2285 

2286 Unlike the parity criterion (*parity_z2*), it needs no inversion 

2287 symmetry, so it also works with Rashba coupling. 

2288 

2289 :param bands: List of band indices (an even number of bands). 

2290 :param nk: Positive integer. Default value 100. k-points on each loop. 

2291 :param nk_perp: Positive integer. Default value 51. Number of loops 

2292 over the half zone (the flow must be resolved finely enough that 

2293 no two centres cross between neighbouring loops unnoticed). 

2294 :param direction: Integer. Default value 0. See *wannier_flow*. 

2295 :param flow: Integer. Default value None. See *wannier_flow*. 

2296 :param k_fixed: Real number. Default value 0. 3D only: a time-reversal 

2297 invariant plane has *k_fixed* 0 or 0.5 (the weak and strong 

2298 indices follow from the six such planes). 

2299 

2300 :returns: 

2301 * **nu** -- 0 (trivial) or 1 (topological). 

2302 ''' 

2303 error_handling.even_bands(bands) 

2304 _, centers = self.wannier_flow(bands, nk, nk_perp, direction, flow, k_fixed, 

2305 positions=False, k_range=(0., 0.5)) 

2306 gap = [self._largest_gap(c) for c in centers] 

2307 jumps = sum(self._between(gap[n], gap[n + 1], centers[n + 1]) 

2308 for n in range(len(gap) - 1)) 

2309 return int(jumps % 2) 

2310 

2311 def z2_indices_3d( 

2312 self, bands: list[int], nk: int = 100, nk_perp: int = 51, 

2313 ) -> tuple[int, int, int, int]: 

2314 r''' 

2315 Get the four :math:`\mathbb{Z}_2` indices 

2316 :math:`(\nu_0;\nu_1\nu_2\nu_3)` of a 3D time-reversal-invariant 

2317 insulator (Fu, Kane and Mele, Phys. Rev. Lett. 98, 106803 (2007); 

2318 Moore and Balents, Phys. Rev. B 75, 121306(R) (2007)) from the 

2319 :math:`\mathbb{Z}_2` invariants of its six time-reversal-invariant 

2320 planes :math:`k_i = 0` and :math:`k_i = 1/2` (fractional coordinates 

2321 along :math:`\mathbf{b}_i`), each computed by *z2_invariant*: 

2322 

2323 .. math:: 

2324 

2325 \nu_0 = \nu(k_i = 0) + \nu(k_i = 1/2) \bmod 2\, ,\qquad 

2326 \nu_i = \nu(k_i = 1/2)\, . 

2327 

2328 The strong index :math:`\nu_0` comes out the same for the three 

2329 :math:`i`, which is checked (a ValueError asks for a finer flow 

2330 otherwise). :math:`\nu_0 = 1` is a strong topological insulator; a 

2331 weak one has :math:`\nu_0 = 0` and some :math:`\nu_i = 1`, i.e. 

2332 stacked quantum spin Hall layers normal to 

2333 :math:`\nu_1\mathbf{b}_1 + \nu_2\mathbf{b}_2 + \nu_3\mathbf{b}_3`. 

2334 No inversion symmetry is needed (compare *parity_z2*). 

2335 

2336 :param bands: List of band indices (an even number of bands). 

2337 :param nk: Positive integer. Default value 100. See *z2_invariant*. 

2338 :param nk_perp: Positive integer. Default value 51. See *z2_invariant*. 

2339 

2340 :returns: 

2341 * **indices** -- Tuple of four integers, 0 or 1: :math:`(\nu_0, \nu_1, \nu_2, \nu_3)`. 

2342 ''' 

2343 error_handling.dim_exact(self.dim, 3) 

2344 nu = np.zeros((3, 2), int) 

2345 for i in range(3): 

2346 direction, flow = [d for d in range(3) if d != i] 

2347 for n, k_fixed in enumerate((0., 0.5)): 

2348 nu[i, n] = self.z2_invariant(bands, nk, nk_perp, direction, flow, k_fixed) 

2349 strong = nu.sum(axis=1) % 2 

2350 error_handling.strong_index(strong) 

2351 return int(strong[0]), int(nu[0, 1]), int(nu[1, 1]), int(nu[2, 1]) 

2352 

2353 def entanglement_spectrum( 

2354 self, ks: ArrayLike, region: list[int], bands: int | list[int], 

2355 ) -> NDArray[np.float64]: 

2356 r''' 

2357 Get the k-resolved entanglement spectrum of a group of bands 

2358 (Peschel, J. Phys. A 36, L205 (2003); Li and Haldane, Phys. Rev. Lett. 

2359 101, 010504 (2008); Fidkowski, Phys. Rev. Lett. 104, 130502 (2010)): 

2360 the eigenvalues :math:`\xi_n(\mathbf{k})` of the correlation matrix 

2361 :math:`P(\mathbf{k})` of the filled *bands*, restricted to the 

2362 orbitals of *region*, 

2363 

2364 .. math:: 

2365 

2366 C_A(\mathbf{k}) = \left[P(\mathbf{k})\right]_{A}\, ,\qquad 

2367 P(\mathbf{k}) = \sum_{n\in\mathrm{bands}}|u_n(\mathbf{k})\rangle\langle u_n(\mathbf{k})|\, , 

2368 

2369 between 0 and 1. For a ribbon or a supercell 

2370 (*tbkit.moire.supercell*) cut in two halves across its width, the 

2371 states of the whole are either inside the region 

2372 (:math:`\xi \approx 1`) or outside (:math:`\xi \approx 0`), except 

2373 for those localized at the cuts: a topological phase leaves 

2374 entanglement modes inside :math:`(0, 1)` -- a mode pinned at 1/2 in 

2375 a chiral-symmetric chain, a branch that flows from 0 to 1 across 

2376 the zone in a Chern insulator. The single-particle entanglement 

2377 energies are :math:`\epsilon = \ln[(1-\xi)/\xi]`. 

2378 

2379 :param ks: ndarray, shape (nk, dim). k-points (see *get_bands*). 

2380 :param region: List of orbital indices (the region :math:`A`). 

2381 :param bands: Band index, or list of band indices (the filled bands). 

2382 

2383 :returns: 

2384 * **xi** -- Real ndarray, shape (nk, len(region)), sorted ascending at every k. 

2385 ''' 

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

2387 error_handling.ks(ks, self.dim) 

2388 error_handling.region(region, self.norb) 

2389 if isinstance(bands, int): 

2390 bands = [bands] 

2391 error_handling.band_indices(bands, self.norb) 

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

2393 v = self._subspaces(ks, bands)[:, region, :] 

2394 return np.linalg.eigvalsh(v @ v.conj().transpose(0, 2, 1)) 

2395 

2396 @staticmethod 

2397 def _largest_gap(centers: NDArray[np.float64]) -> float: 

2398 ''' 

2399 Private method. Midpoint of the largest gap between sorted centres on the circle [0, 1). 

2400 ''' 

2401 ext = np.append(centers, centers[0] + 1.) 

2402 n = np.argmax(np.diff(ext)) 

2403 return float((ext[n] + ext[n + 1]) / 2 % 1.) 

2404 

2405 @staticmethod 

2406 def _between(a: float, b: float, centers: NDArray[np.float64]) -> int: 

2407 ''' 

2408 Private method. Number of centres on the shorter arc between a and b. 

2409 ''' 

2410 d = (b - a) % 1. 

2411 start, length = (a, d) if d <= 0.5 else (b, 1. - d) 

2412 return int(np.sum((centers - start) % 1. < length)) 

2413 

2414 def _check_loop(self, bands, nk, direction, positions) -> list[int]: 

2415 ''' 

2416 Private method. Validate the arguments shared by the Wilson-loop methods. 

2417 ''' 

2418 if isinstance(bands, int): 

2419 bands = [bands] 

2420 error_handling.band_indices(bands, self.norb) 

2421 error_handling.positive_int(nk, 'nk') 

2422 error_handling.direction(direction, self.dim) 

2423 error_handling.boolean(positions, 'positions') 

2424 return bands 

2425 

2426 def _trims(self) -> list[NDArray[np.float64]]: 

2427 ''' 

2428 Private method. Time-reversal-invariant momenta: fractional 

2429 coordinates in {0, 1/2}^dim, as k points. 

2430 ''' 

2431 grid = np.array(np.meshgrid(*[[0., 0.5]] * self.dim, indexing='ij')).reshape(self.dim, -1).T 

2432 return [f @ self.rec_vec_k for f in grid] 

2433 

2434 def _operator(self, op, k: NDArray[np.float64]) -> NDArray[np.complex128]: 

2435 ''' 

2436 Private method. Matrix of a symmetry operator at k (op is a matrix, or a callable of k). 

2437 ''' 

2438 mat = np.asarray(op(k) if callable(op) else op, dtype='c16') 

2439 error_handling.operator(mat, self.norb) 

2440 return mat 

2441 

2442 def parity_z2(self, inversion, bands: list[int]) -> int: 

2443 r''' 

2444 Get the :math:`\mathbb{Z}_2` invariant of an inversion- and 

2445 time-reversal-symmetric model from the parity eigenvalues of its 

2446 occupied bands at the time-reversal-invariant momenta 

2447 :math:`\Gamma_i` (Fu and Kane, Phys. Rev. B 76, 045302 (2007)): 

2448 

2449 .. math:: 

2450 

2451 (-1)^\nu = \prod_i \delta_i\, ,\qquad 

2452 \delta_i = \prod_{m=1}^{N} \xi_{2m}(\Gamma_i)\, , 

2453 

2454 one parity :math:`\xi = \pm1` per Kramers pair. In 3D, :math:`\nu` 

2455 is the strong index. 

2456 

2457 :param inversion: Complex ndarray, shape (norb, norb), or callable of 

2458 k returning one: the inversion operator :math:`P`, with 

2459 :math:`P H(\mathbf{k}) P^{-1} = H(-\mathbf{k})` (e.g. swapping the 

2460 two sublattices of graphene, and the identity on spin). 

2461 :param bands: List of band indices (the occupied Kramers pairs). 

2462 

2463 :returns: 

2464 * **nu** -- 0 (trivial) or 1 (topological). 

2465 ''' 

2466 error_handling.even_bands(bands) 

2467 error_handling.band_indices(bands, self.norb) 

2468 product = 1 

2469 for k in self._trims(): 

2470 v = self._subspace(k, bands) 

2471 xi = np.linalg.eigvals(v.conj().T @ self._operator(inversion, k) @ v) 

2472 error_handling.parities(xi) 

2473 n_minus = int(np.sum(xi.real < 0)) 

2474 product *= (-1) ** (n_minus // 2) 

2475 return 0 if product == 1 else 1 

2476 

2477 def symmetry_error( 

2478 self, op, k_map: str = 'minus', antiunitary: bool = False, anti: bool = False, 

2479 nk: int = 5, seed: int = 0, 

2480 ) -> float: 

2481 r''' 

2482 Check a symmetry of the Bloch Hamiltonian, 

2483 

2484 .. math:: 

2485 

2486 H(g\mathbf{k}) = \pm\, U\, \mathcal{K}H(\mathbf{k})\mathcal{K}^{-1} U^\dagger\, , 

2487 

2488 with :math:`g\mathbf{k} = -\mathbf{k}` or :math:`\mathbf{k}`, 

2489 :math:`\mathcal{K}` complex conjugation if *antiunitary*, and the 

2490 minus sign if *anti*: time reversal is antiunitary with 

2491 :math:`\mathbf{k}\to-\mathbf{k}` and the plus sign, particle-hole 

2492 antiunitary with :math:`\mathbf{k}\to-\mathbf{k}` and the minus sign, 

2493 chiral unitary with :math:`\mathbf{k}\to\mathbf{k}` and the minus sign, 

2494 inversion unitary with :math:`\mathbf{k}\to-\mathbf{k}` and the plus 

2495 sign. 

2496 

2497 :param op: Complex ndarray, shape (norb, norb), or callable of k: :math:`U`. 

2498 :param k_map: 'minus' or 'identity'. Default value 'minus'. 

2499 :param antiunitary: Boolean. Default value False. 

2500 :param anti: Boolean. Default value False. 

2501 :param nk: Positive integer. Default value 5. Points per direction of 

2502 the (randomly shifted) test mesh. 

2503 :param seed: Integer. Default value 0. Seed of the random shift. 

2504 

2505 :returns: 

2506 * **error** -- Real number. Largest deviation over the mesh (0 for a symmetry). 

2507 ''' 

2508 error_handling.k_map(k_map) 

2509 error_handling.boolean(antiunitary, 'antiunitary') 

2510 error_handling.boolean(anti, 'anti') 

2511 error_handling.positive_int(nk, 'nk') 

2512 rng = np.random.default_rng(seed) 

2513 fracs = np.array(np.meshgrid(*[np.arange(nk) / nk] * self.dim, indexing='ij')).reshape(self.dim, -1).T 

2514 fracs = fracs + rng.uniform(0., 1. / nk, self.dim) 

2515 sign = -1. if anti else 1. 

2516 err = 0. 

2517 for f in fracs: 

2518 k = f @ self.rec_vec_k 

2519 gk = -k if k_map == 'minus' else k 

2520 ham = self.get_ham(k) 

2521 u = self._operator(op, k) 

2522 rhs = u @ (ham.conj() if antiunitary else ham) @ u.conj().T 

2523 err = max(err, float(np.max(np.abs(self.get_ham(gk) - sign * rhs)))) 

2524 return err 

2525 

2526 def tenfold_class( 

2527 self, time_reversal=None, particle_hole=None, chiral=None, nk: int = 5, tol: float = 1e-8, 

2528 ) -> str: 

2529 r''' 

2530 Get the Altland-Zirnbauer symmetry class (the "tenfold way") of the 

2531 model from its time-reversal :math:`T = U_T\mathcal{K}`, particle-hole 

2532 :math:`C = U_C\mathcal{K}` and chiral :math:`S = U_S` symmetries. Each 

2533 given operator is first checked (see *symmetry_error*) to be a 

2534 symmetry; the class then follows from :math:`T^2 = U_TU_T^* = \pm1`, 

2535 :math:`C^2 = \pm1`, and the presence of :math:`S`. Any two of the 

2536 three imply the third (:math:`S = TC`), which need not be given. 

2537 

2538 :param time_reversal: Matrix :math:`U_T`, or None (absent). 

2539 :param particle_hole: Matrix :math:`U_C`, or None (absent). 

2540 :param chiral: Matrix :math:`U_S`, or None (absent). 

2541 :param nk: Positive integer. Default value 5. See *symmetry_error*. 

2542 :param tol: Positive real. Default value 1e-8. Tolerance of the checks. 

2543 

2544 :returns: 

2545 * **name** -- String: 'A', 'AIII', 'AI', 'BDI', 'D', 'DIII', 'AII', 'CII', 'C' or 'CI'. 

2546 ''' 

2547 error_handling.positive_real(tol, 'tol') 

2548 k0 = np.zeros(self.dim) 

2549 for op, k_map, antiu, anti in ((time_reversal, 'minus', True, False), 

2550 (particle_hole, 'minus', True, True), 

2551 (chiral, 'identity', False, True)): 

2552 if op is not None: 

2553 error_handling.is_symmetry(self.symmetry_error(op, k_map, antiu, anti, nk), tol) 

2554 u_t = None if time_reversal is None else self._operator(time_reversal, k0) 

2555 u_c = None if particle_hole is None else self._operator(particle_hole, k0) 

2556 # two of the three imply the third: C = S T, T = S C 

2557 if chiral is not None and (u_t is None) != (u_c is None): 

2558 u_s = self._operator(chiral, k0) 

2559 if u_t is not None: 

2560 u_c = u_s @ u_t 

2561 elif u_c is not None: 

2562 u_t = u_s @ u_c 

2563 squares = [] 

2564 for u in (u_t, u_c): 

2565 if u is None: 

2566 squares.append(0) 

2567 continue 

2568 square = u @ u.conj() 

2569 error_handling.square(square, tol) 

2570 squares.append(int(np.sign(square[0, 0].real))) 

2571 has_s = chiral is not None or (u_t is not None and u_c is not None) 

2572 table = {(0, 0, False): 'A', (0, 0, True): 'AIII', (1, 0, False): 'AI', 

2573 (1, 1, True): 'BDI', (0, 1, False): 'D', (-1, 1, True): 'DIII', 

2574 (-1, 0, False): 'AII', (-1, -1, True): 'CII', (0, -1, False): 'C', 

2575 (1, -1, True): 'CI'} 

2576 return table[(squares[0], squares[1], has_s)] 

2577 

2578 def quantum_geometric_tensor( 

2579 self, bands: int | list[int], k: ArrayLike, dk: float = 1e-4, positions: bool = True, 

2580 ) -> NDArray[np.complex128]: 

2581 r''' 

2582 Get the quantum geometric tensor of a group of bands at :math:`\mathbf{k}`, 

2583 

2584 .. math:: 

2585 

2586 Q_{\mu\nu} = \mathrm{Tr}\left[P\,\partial_\mu P\,\partial_\nu P\right] 

2587 = \sum_{n\in\mathrm{bands}}\langle\partial_\mu u_n|(1-P)|\partial_\nu u_n\rangle\, , 

2588 

2589 with :math:`P(\mathbf{k})` the projector on the bands (gauge 

2590 invariant, so degeneracies within the group are harmless). Its real 

2591 part is the quantum metric :math:`g_{\mu\nu}` (Provost and Vallee, 

2592 1980), minus twice its imaginary part the Berry curvature 

2593 :math:`\Omega_{\mu\nu} = -2\,\mathrm{Im}\,Q_{\mu\nu}` (the convention of 

2594 *berry_curvature*, per unit k-area here). They obey 

2595 :math:`\sqrt{\det g} \ge |\Omega_{xy}|/2` in 2D. 

2596 

2597 :param bands: Band index, or list of band indices. 

2598 :param k: k point (see *get_ham*). 

2599 :param dk: Positive real. Default value 1e-4. Finite-difference step. 

2600 :param positions: Boolean. Default value True. Include the orbital 

2601 positions in the Bloch functions. The local metric and curvature 

2602 depend on this choice (the Berry curvature of *berry_curvature* is 

2603 that of ``positions=False``); integrals such as the Chern number 

2604 do not. 

2605 

2606 :returns: 

2607 * **Q** -- Complex ndarray, shape (dim, dim), in the k coordinates of *get_ham*. 

2608 ''' 

2609 if isinstance(bands, int): 

2610 bands = [bands] 

2611 error_handling.band_indices(bands, self.norb) 

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

2613 error_handling.positive_real(dk, 'dk') 

2614 error_handling.boolean(positions, 'positions') 

2615 k = np.asarray(k, dtype='f8') 

2616 tau = self.orbital_positions() 

2617 

2618 def proj(kk): 

2619 v = self._subspace(kk, bands) 

2620 if positions: 

2621 v = np.exp(-1j * tau @ (self.k_basis @ kk))[:, None] * v 

2622 return v @ v.conj().T 

2623 

2624 p0 = proj(k) 

2625 dp = [] 

2626 for mu in range(self.dim): 

2627 e = np.zeros(self.dim) 

2628 e[mu] = dk 

2629 dp.append((proj(k + e) - proj(k - e)) / (2 * dk)) 

2630 return np.array([[np.trace(p0 @ dp[mu] @ dp[nu]) for nu in range(self.dim)] 

2631 for mu in range(self.dim)]) 

2632 

2633 def surface_spectral_function( 

2634 self, ks: ArrayLike, energies: ArrayLike, direction: int, side: int = 1, 

2635 eta: float = 1e-2, bulk: bool = False, max_iter: int = 10000, 

2636 ) -> NDArray[np.float64]: 

2637 r''' 

2638 Get the spectral function of the surface of a semi-infinite crystal, 

2639 

2640 .. math:: 

2641 

2642 A_s(\mathbf{k}_\parallel, E) = -\frac{1}{\pi}\, 

2643 \mathrm{Im}\,\mathrm{Tr}\, G_{00}(\mathbf{k}_\parallel, E + i\eta)\, , 

2644 

2645 with :math:`G_{00}` the Green's function of the outermost unit cell: 

2646 the crystal fills the cells :math:`n\,\mathbf{a}_d`, 

2647 :math:`n = 0, 1, 2, \dots` along the primitive vector 

2648 :math:`\mathbf{a}_d` (``lat.prim_vec[direction]``), times *side*, and 

2649 is periodic along the others. It comes from the Lopez Sancho-Rubio 

2650 decimation (see *transport.surface_green*) of the chain of principal 

2651 layers (as many cells as the longest hopping along 

2652 :math:`\mathbf{a}_d` spans), so the crystal is truly semi-infinite: no 

2653 finite slab, and no states of the opposite surface. It is the 

2654 tight-binding picture of an ARPES map -- surface states (edge states, 

2655 in 2D; the end state of a 1D chain) show up as sharp lines inside the 

2656 bulk gaps. With *bulk*, the same quantity for a unit cell deep inside 

2657 the crystal: the bulk bands projected on the surface Brillouin zone. 

2658 

2659 :param ks: Real array, shape (nk, dim). k-points, in the coordinates 

2660 of *get_ham* (e.g. from a *k_path* of the surface Brillouin zone). 

2661 Only :math:`\mathbf{k}_\parallel` matters: the result does not 

2662 depend on the component along the reciprocal vector 

2663 :math:`\mathbf{b}_d`. 

2664 :param energies: Real number or real array. Energies :math:`E`. 

2665 :param direction: Integer, between 0 and dim-1. The primitive vector 

2666 normal to the surface (the one along which the crystal is 

2667 semi-infinite). 

2668 :param side: +1 or -1. Default value 1. The crystal extends along 

2669 :math:`+\mathbf{a}_d` (the surface faces :math:`-\mathbf{a}_d`) or 

2670 along :math:`-\mathbf{a}_d` (the opposite surface). 

2671 :param eta: Positive real. Default value 1e-2. Broadening: the 

2672 width of the lines, and the decay length (about :math:`v/\eta` 

2673 cells) over which the bulk is resolved. 

2674 :param bulk: Boolean. Default value False. If True, the spectral 

2675 function of a bulk unit cell instead of the surface one. 

2676 :param max_iter: Positive integer. Default value 10000. Maximum 

2677 number of decimation steps. 

2678 

2679 :returns: 

2680 * **A** -- Real ndarray, shape (nk, len(energies)). 

2681 

2682 Example usage:: 

2683 

2684 # the (001) surface of a 3D model, along a path of the surface zone 

2685 A = ks.surface_spectral_function(k_path, np.linspace(-1, 1, 201), direction=2) 

2686 ''' 

2687 error_handling.no_overlap(self._overlap_hop) 

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

2689 error_handling.ks(ks, self.dim) 

2690 error_handling.frequencies(energies, 'energies') 

2691 energies = np.atleast_1d(np.asarray(energies, dtype='f8')) 

2692 error_handling.direction(direction, self.dim) 

2693 error_handling.surface_side(side) 

2694 error_handling.positive_real(eta, 'eta') 

2695 error_handling.boolean(bulk, 'bulk') 

2696 error_handling.positive_int(max_iter, 'max_iter') 

2697 # H_m(k): the hoppings from a cell to the cell m a_d away, with the 

2698 # phase of their in-plane part only (independent of k along b_d) 

2699 a_dir = np.array(self.lat.prim_vec[direction], dtype='f8') 

2700 layers: dict[int, list] = {} 

2701 for (i, j, cell, t), (_, _, R, _) in zip(self._hop_cells(), self._hop): 

2702 layers.setdefault(cell[direction], []).append((i, j, R - cell[direction] * a_dir, t)) 

2703 k_cart = ks @ self.k_basis.T 

2704 blocks: dict[int, NDArray] = {m: self._bloch_sum(hops, k_cart)[0] for m, hops in layers.items()} 

2705 blocks[0] = blocks.get(0, 0.) + (np.diag(self.onsite) + self._onsite_offdiag)[None] 

2706 width = max([abs(m) for m in layers] + [1]) 

2707 norb, nk = self.norb, len(ks) 

2708 

2709 def layer(shift): 

2710 # block (p, q) couples cell p of a principal layer to cell q of 

2711 # the layer *shift* cells further into the crystal 

2712 out = np.zeros((nk, width, norb, width, norb), 'c16') 

2713 for p in range(width): 

2714 for q in range(width): 

2715 m = side * (q - p + shift) 

2716 if m in blocks: 

2717 out[:, p, :, q, :] = blocks[m] 

2718 return out.reshape(nk, width * norb, width * norb) 

2719 h0, alpha, beta = layer(0), layer(width), layer(-width) 

2720 spec = np.empty((nk, len(energies))) 

2721 for n in range(nk): 

2722 gs, gb = _green_surface_bulk(energies + 1j * eta, h0[n], alpha[n], beta[n], 1e-12, 

2723 max_iter, 'surface_spectral_function') 

2724 g = gb if bulk else gs 

2725 spec[n] = -np.trace(g[:, :norb, :norb], axis1=1, axis2=2).imag / PI 

2726 return spec 

2727 

2728 # ------------------------------------------------------------------ 

2729 # Projections, constant-energy contours and plot helpers 

2730 # ------------------------------------------------------------------ 

2731 

2732 def spin_operator(self, axis: str) -> NDArray[np.complex128]: 

2733 r''' 

2734 Get the Pauli matrix :math:`\sigma_{axis}` acting on the spin of 

2735 every site, :math:`\mathbb{1}_{sites}\otimes\sigma_{axis}`, for a model 

2736 built with ``spin=True`` (for *band_weights* and *spin_texture*). 

2737 

2738 :param axis: 'x', 'y' or 'z'. 

2739 

2740 :returns: 

2741 * **op** -- Complex ndarray, shape (norb, norb). 

2742 ''' 

2743 error_handling.spinful(self.spin) 

2744 error_handling.spin_axis(axis) 

2745 return np.kron(np.eye(self.n_sites), PAULI[axis]) 

2746 

2747 def _projector(self, projector) -> NDArray[np.complex128]: 

2748 ''' 

2749 Private method. The (norb, norb) operator of *band_weights* from an 

2750 orbital index, a list of them, a sublattice tag or a matrix. 

2751 ''' 

2752 error_handling.projector(projector, self.norb, self.tags) 

2753 if isinstance(projector, np.ndarray): 

2754 return projector.astype('c16') 

2755 if isinstance(projector, str): 

2756 sites = np.nonzero(self.tags == projector)[0] 

2757 projector = np.concatenate([2 * sites, 2 * sites + 1]) if self.spin else sites 

2758 diag = np.zeros(self.norb) 

2759 diag[projector] = 1. 

2760 return np.diag(diag).astype('c16') 

2761 

2762 def _expectations(self, ks: NDArray[np.float64], ops: list) -> NDArray[np.float64]: 

2763 r''' 

2764 Private method. :math:`\mathrm{Re}\langle u_n(\mathbf{k})|O|u_n(\mathbf{k})\rangle 

2765 /\langle u_n|u_n\rangle` for each operator of *ops*, band and k-point: 

2766 shape (len(ops), nk, norb), from one diagonalization. Within a group 

2767 of degenerate bands (energies within 1e-8 of each other, relative to 

2768 the largest one), each band gets the average over the group: the 

2769 trace of O over the degenerate subspace divided by its dimension, 

2770 which does not depend on the eigenvectors the solver returns. 

2771 ''' 

2772 en, vn = self._eigs(ks, eigenvec=True) 

2773 norm = np.sum(np.abs(vn) ** 2, axis=1) 

2774 values = np.array([np.einsum('kin,ij,kjn->kn', vn.conj(), op, vn).real / norm for op in ops]) 

2775 # label the degenerate groups (consecutive bands, sorted by energy) 

2776 tol = 1e-8 * max(1., np.abs(en).max()) 

2777 new_group = np.concatenate([np.ones((len(en), 1), bool), 

2778 np.abs(np.diff(en, axis=1)) > tol], axis=1) 

2779 groups = np.cumsum(new_group.ravel()) - 1 

2780 counts = np.bincount(groups) 

2781 return np.array([(np.bincount(groups, v.ravel()) / counts)[groups].reshape(v.shape) 

2782 for v in values]) 

2783 

2784 def band_weights(self, projector, ks: ArrayLike | None = None) -> NDArray[np.float64]: 

2785 r''' 

2786 Get the weight of each band on a set of orbitals, or the expectation 

2787 value of an operator, for "fat band" plots (see *plot_bands*): 

2788 

2789 .. math:: 

2790 

2791 w_n(\mathbf{k}) = \langle u_n(\mathbf{k})|P|u_n(\mathbf{k})\rangle\, , 

2792 

2793 with :math:`P` the projector on the chosen orbitals (a weight 

2794 between 0 and 1; the weights on all the sublattices add up to 1), or 

2795 any Hermitian operator, e.g. ``ks.spin_operator('z')`` for the spin 

2796 polarization (between -1 and 1). Within a group of degenerate bands 

2797 (e.g. Kramers pairs with inversion and time reversal), the weight of 

2798 a single eigenvector depends on the choice of eigenvectors; each band 

2799 of the group gets instead the average over the group, which does not 

2800 (a Kramers pair has zero spin polarization). 

2801 For a non-Hermitian model, the right eigenvectors are used; with an 

2802 overlap (*set_overlap*), :math:`v^\dagger Pv/v^\dagger v`. 

2803 

2804 :param projector: Orbital index, list of orbital indices, sublattice 

2805 tag (all the orbitals, both spins, of the sites with that tag), 

2806 or a (norb, norb) Hermitian matrix. 

2807 :param ks: Real array, shape (nk, dim). Default value None: the 

2808 k-points of the last *get_bands* or *k_path*. 

2809 

2810 :returns: 

2811 * **weights** -- Real ndarray, shape (nk, norb), ordered as the bands. 

2812 

2813 Example usage:: 

2814 

2815 ks.k_path(points, nk=100) 

2816 ks.plot_bands(weights=ks.band_weights('a')) 

2817 ''' 

2818 op = self._projector(projector) 

2819 if ks is None: 

2820 error_handling.empty_ndarray(self.en, 'get_bands or k_path') 

2821 ks = self.ks 

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

2823 error_handling.ks(ks, self.dim) 

2824 return self._expectations(ks, [op])[0] 

2825 

2826 def spin_texture(self, ks: ArrayLike, band: int) -> NDArray[np.float64]: 

2827 r''' 

2828 Get the spin expectation values 

2829 :math:`\langle\boldsymbol\sigma\rangle_n(\mathbf{k}) = 

2830 \langle u_n|\boldsymbol\sigma|u_n\rangle` of a band, for a model built 

2831 with ``spin=True``. Their length is 1 if the band is fully polarized, 

2832 less if spin-orbit coupling entangles the spin with the orbitals. 

2833 Within a group of degenerate bands, each band gets the average spin 

2834 of the group (see *band_weights*): zero for a Kramers pair, with both 

2835 inversion and time reversal. 

2836 

2837 :param ks: Real array, shape (nk, dim). k-points. 

2838 :param band: Integer. Band index. 

2839 

2840 :returns: 

2841 * **spin** -- Real ndarray, shape (nk, 3): 

2842 :math:`(\langle\sigma_x\rangle, \langle\sigma_y\rangle, \langle\sigma_z\rangle)`. 

2843 ''' 

2844 error_handling.spinful(self.spin) 

2845 error_handling.band_index(band, self.norb) 

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

2847 error_handling.ks(ks, self.dim) 

2848 ops = [self.spin_operator(a) for a in 'xyz'] 

2849 return self._expectations(ks, ops)[:, :, band].T 

2850 

2851 def _fold(self, elements: NDArray[np.float64]) -> NDArray[np.float64]: 

2852 ''' 

2853 Private method. Bring segments or triangles (fractional coordinates, 

2854 shape (n, nv, dim)) into the first Brillouin zone (the Wigner-Seitz 

2855 cell of the reciprocal lattice, by their centres), in the k 

2856 coordinates of *get_ham*. 

2857 ''' 

2858 elements = elements - np.floor(elements.mean(axis=1) + 0.5)[:, None, :] 

2859 cart = elements @ self.rec_vec_k 

2860 shifts = np.array(list(product((-1, 0, 1), repeat=self.dim))) @ self.rec_vec_k 

2861 dist = np.linalg.norm(cart.mean(axis=1)[:, None, :] - shifts[None], axis=2) 

2862 return cart - shifts[np.argmin(dist, axis=1)][:, None, :] 

2863 

2864 def fermi_surface( 

2865 self, energy: float = 0., nk: int | tuple[int, ...] | None = None, 

2866 bands: int | list[int] | None = None, 

2867 ) -> list[NDArray[np.float64]]: 

2868 r''' 

2869 Get the constant-energy surfaces :math:`E_n(\mathbf{k}) = E` (the 

2870 Fermi surface at :math:`E = E_F`): lines in a 2D Brillouin zone, 

2871 surfaces in 3D. The bands are interpolated linearly in the triangles 

2872 (tetrahedra in 3D) of a uniform mesh, the same decomposition as 

2873 *dos.tetrahedron_dos*, and the level set of each simplex is a segment 

2874 (a triangle or a quadrilateral, split in two, in 3D): marching 

2875 triangles and tetrahedra, exact for the interpolated bands. Each 

2876 piece is brought into the first Brillouin zone, the Wigner-Seitz 

2877 cell around :math:`\Gamma`. For a non-Hermitian model, the real part 

2878 of the energies is used. 

2879 

2880 :param energy: Real number. Default value 0. The energy :math:`E`. 

2881 :param nk: Positive integer, or tuple of *dim* positive integers. 

2882 Default value None: 100 in 2D, 40 in 3D. Number of k-points along 

2883 each reciprocal lattice vector. 

2884 :param bands: Band index, or list of band indices. Default value 

2885 None: all bands. 

2886 

2887 :returns: 

2888 * **surfaces** -- List, one entry per band of *bands*: a real 

2889 ndarray of shape (n, 2, 2) in 2D (n segments, each two k-points) 

2890 or (n, 3, 3) in 3D (n triangles, each three k-points), in the 

2891 k coordinates of *get_ham*. Empty (n = 0) if the band does not 

2892 cross *energy*. 

2893 

2894 Example usage:: 

2895 

2896 # the Fermi surface of the half-filled square lattice 

2897 segments = sq.fermi_surface(0.)[0] 

2898 ''' 

2899 error_handling.dim_min(self.dim, 2) 

2900 error_handling.real_number(energy, 'energy') 

2901 if nk is None: 

2902 nk = 100 if self.dim == 2 else 40 

2903 error_handling.nk(nk, self.dim) 

2904 if isinstance(nk, int): 

2905 nk = (nk,) * self.dim 

2906 if bands is None: 

2907 bands = list(range(self.norb)) 

2908 elif isinstance(bands, int): 

2909 bands = [bands] 

2910 error_handling.band_indices(bands, self.norb) 

2911 _, ks = self.mesh_grid(nk) 

2912 en = self._eigs(ks).real.reshape(*nk, self.norb) 

2913 return [self._fold(_level_set(en[..., n], energy)) for n in bands] 

2914 

2915 def _k_labels(self) -> list[str]: 

2916 ''' 

2917 Private method. Axis labels of the k coordinates of *get_ham*: 

2918 '$k_x$' for a coordinate along the Cartesian x axis (the slab of a 

2919 3D model along x and z gives '$k_x$', '$k_z$'), '$k_1$', ... otherwise. 

2920 ''' 

2921 labels = [] 

2922 for n, col in enumerate(self.k_basis.T): 

2923 axis = np.nonzero(np.isclose(np.abs(col), 1.))[0] 

2924 labels.append('$k_{}$'.format('xyz'[axis[0]] if len(axis) else n + 1)) 

2925 return labels 

2926 

2927 def _zone_axes(self, ax, fs: float) -> None: 

2928 ''' 

2929 Private method. Draw the first Brillouin zone of a 2D model on *ax*, 

2930 and label and scale its axes. 

2931 ''' 

2932 zone = _zone_polygon(self.rec_vec_k) 

2933 ax.plot(*np.vstack([zone, zone[:1]]).T, 'k', lw=1) 

2934 ax.set_aspect('equal') 

2935 pad = 0.05 * np.ptp(zone, axis=0).max() 

2936 ax.set_xlim(zone[:, 0].min() - pad, zone[:, 0].max() + pad) 

2937 ax.set_ylim(zone[:, 1].min() - pad, zone[:, 1].max() + pad) 

2938 ax.set_xlabel(self._k_labels()[0], fontsize=fs) 

2939 ax.set_ylabel(self._k_labels()[1], fontsize=fs) 

2940 

2941 def plot_fermi_surface( 

2942 self, energy: float = 0., nk: int | tuple[int, ...] | None = None, 

2943 bands: int | list[int] | None = None, lw: float = 2., alpha: float = 0.6, 

2944 fs: float = 20, figsize: tuple[float, float] | None = None, 

2945 ) -> Figure: 

2946 ''' 

2947 Plot the constant-energy contours of *fermi_surface* (one color per 

2948 band, with a legend if several bands cross the energy): lines inside 

2949 the first Brillouin zone (drawn in black) in 2D, surfaces in 3D. 

2950 

2951 :param energy: Real number. Default value 0. The energy. 

2952 :param nk: See *fermi_surface*. 

2953 :param bands: See *fermi_surface*. 

2954 :param lw: Positive number. Default value 2. Linewidth (2D). 

2955 :param alpha: Real number in (0, 1]. Default value 0.6. Opacity of 

2956 the surfaces (3D). 

2957 :param fs: Positive number. Default value 20. Fontsize. 

2958 :param figsize: Tuple. Default value None. Figure size. 

2959 

2960 :returns: 

2961 * **fig** -- Figure. 

2962 ''' 

2963 error_handling.positive_real(lw, 'lw') 

2964 error_handling.positive_real(alpha, 'alpha') 

2965 error_handling.positive_real(fs, 'fs') 

2966 error_handling.tuple_2elem(figsize, 'figsize') 

2967 surfaces = self.fermi_surface(energy, nk, bands) 

2968 bands = list(range(self.norb)) if bands is None else np.atleast_1d(bands).tolist() 

2969 fig = plt.figure(figsize=figsize) 

2970 if self.dim == 2: 

2971 ax = fig.add_subplot() 

2972 for n, (band, segs) in enumerate(zip(bands, surfaces)): 

2973 if len(segs): 

2974 ax.add_collection(LineCollection(list(segs), colors='C{}'.format(n % 10), lw=lw, 

2975 label='band {}'.format(band))) 

2976 self._zone_axes(ax, fs) 

2977 else: 

2978 ax = fig.add_subplot(projection='3d') 

2979 for n, (band, tris) in enumerate(zip(bands, surfaces)): 

2980 if len(tris): 

2981 ax.add_collection3d(Poly3DCollection(tris, facecolor='C{}'.format(n % 10), 

2982 alpha=min(alpha, 1.), edgecolor='none', 

2983 label='band {}'.format(band))) 

2984 extent = np.abs(np.concatenate([t.reshape(-1, 3) for t in surfaces] + 

2985 [self.rec_vec_k / 2])).max() 

2986 ax.set(xlim=(-extent, extent), ylim=(-extent, extent), zlim=(-extent, extent)) 

2987 ax.set_box_aspect((1, 1, 1)) 

2988 ax.set_xlabel(self._k_labels()[0], fontsize=fs) 

2989 ax.set_ylabel(self._k_labels()[1], fontsize=fs) 

2990 ax.set_zlabel(self._k_labels()[2], fontsize=fs) 

2991 if sum(len(s) > 0 for s in surfaces) > 1: 

2992 ax.legend(fontsize=0.6 * fs) 

2993 ax.set_title('$E = {:g}$'.format(energy), fontsize=fs) 

2994 plt.draw() 

2995 return fig 

2996 

2997 def plot_spin_texture( 

2998 self, band: int, energy: float = 0., nk: int = 100, n_arrows: int = 40, 

2999 cmap: str = 'RdBu_r', lw: float = 1., fs: float = 20, 

3000 figsize: tuple[float, float] | None = None, 

3001 ) -> Figure: 

3002 r''' 

3003 Plot the spin texture of a band of a 2D spinful model on its 

3004 constant-energy contour (see *fermi_surface*), zoomed on the 

3005 contour: arrows of 

3006 :math:`(\langle\sigma_x\rangle, \langle\sigma_y\rangle)` along the 

3007 contour, colored by :math:`\langle\sigma_z\rangle` (see *spin_texture*). 

3008 Rashba coupling, for instance, locks the spin perpendicular to 

3009 :math:`\mathbf{k}`, winding once around the contour. 

3010 

3011 :param band: Integer. Band index. 

3012 :param energy: Real number. Default value 0. Energy of the contour. 

3013 :param nk: Positive integer. Default value 100. Mesh of the contour. 

3014 :param n_arrows: Positive integer. Default value 40. Approximate 

3015 number of arrows, evenly spaced along the contour. A fully 

3016 polarized in-plane spin is drawn 80% as long as their spacing. 

3017 :param cmap: Default value 'RdBu_r'. Colormap of :math:`\langle\sigma_z\rangle`. 

3018 :param lw: Positive number. Default value 1. Linewidth of the contour. 

3019 :param fs: Positive number. Default value 20. Fontsize. 

3020 :param figsize: Tuple. Default value None. Figure size. 

3021 

3022 :returns: 

3023 * **fig** -- Figure. 

3024 ''' 

3025 error_handling.dim_2(self.dim) 

3026 error_handling.spinful(self.spin) 

3027 error_handling.band_index(band, self.norb) 

3028 error_handling.positive_int(nk, 'nk') 

3029 error_handling.positive_int(n_arrows, 'n_arrows') 

3030 error_handling.positive_real(lw, 'lw') 

3031 error_handling.positive_real(fs, 'fs') 

3032 error_handling.tuple_2elem(figsize, 'figsize') 

3033 segs = self.fermi_surface(energy, nk, [band])[0] 

3034 error_handling.contour_found(len(segs), energy) 

3035 # arrows at segment midpoints, at least one contour length / n_arrows apart 

3036 mids = segs.mean(axis=1) 

3037 spacing = np.linalg.norm(segs[:, 1] - segs[:, 0], axis=1).sum() / n_arrows 

3038 keep = [0] 

3039 for i in range(1, len(mids)): 

3040 if np.min(np.linalg.norm(mids[keep] - mids[i], axis=1)) >= spacing: 

3041 keep.append(i) 

3042 spin = self.spin_texture(mids[keep], band) 

3043 fig, ax = plt.subplots(figsize=figsize) 

3044 ax.add_collection(LineCollection(list(segs), colors='0.6', lw=lw)) 

3045 arrows = ax.quiver(mids[keep, 0], mids[keep, 1], spin[:, 0], spin[:, 1], spin[:, 2], 

3046 cmap=cmap, norm=Normalize(-1., 1.), pivot='mid', edgecolor='k', 

3047 linewidth=0.5, angles='xy', scale_units='xy', scale=1.25 / spacing) 

3048 fig.colorbar(arrows, ax=ax).set_label(r'$\langle\sigma_z\rangle$', fontsize=fs) 

3049 self._zone_axes(ax, fs) 

3050 # zoom on the contour (within the zone) 

3051 low, high = segs.reshape(-1, 2).min(axis=0), segs.reshape(-1, 2).max(axis=0) 

3052 pad = 0.1 * (high - low).max() + spacing 

3053 ax.set_xlim(max(ax.get_xlim()[0], low[0] - pad), min(ax.get_xlim()[1], high[0] + pad)) 

3054 ax.set_ylim(max(ax.get_ylim()[0], low[1] - pad), min(ax.get_ylim()[1], high[1] + pad)) 

3055 ax.set_title('Spin texture of band {}, $E = {:g}$'.format(band, energy), fontsize=fs) 

3056 plt.draw() 

3057 return fig 

3058 

3059 def plot_berry_curvature( 

3060 self, bands: int | list[int], nk: int | tuple[int, int] = 60, 

3061 plane: tuple[int, int] = (0, 1), k_fixed: float = 0., cmap: str = 'RdBu_r', 

3062 fs: float = 20, figsize: tuple[float, float] | None = None, 

3063 ) -> Figure: 

3064 r''' 

3065 Plot the Berry curvature of *berry_curvature* as a density, 

3066 :math:`\Omega(\mathbf{k})` = flux of each plaquette / its area. In 

3067 2D, the plaquettes are brought into the first Brillouin zone (drawn 

3068 in black), in the k coordinates of *get_ham*. In 3D, they fill the 

3069 parallelogram spanned by the two reciprocal vectors of *plane*, in 

3070 orthonormal coordinates of the plane (:math:`k_1` along 

3071 :math:`\mathbf{b}_i`). The title gives the Chern number, the total 

3072 flux over :math:`2\pi`. 

3073 

3074 :param bands: See *berry_curvature*. 

3075 :param nk: Default value 60. See *berry_curvature*. 

3076 :param plane: See *berry_curvature*. 

3077 :param k_fixed: See *berry_curvature*. 

3078 :param cmap: Default value 'RdBu_r'. Colormap, centred on zero. 

3079 :param fs: Positive number. Default value 20. Fontsize. 

3080 :param figsize: Tuple. Default value None. Figure size. 

3081 

3082 :returns: 

3083 * **fig** -- Figure. 

3084 ''' 

3085 error_handling.positive_real(fs, 'fs') 

3086 error_handling.tuple_2elem(figsize, 'figsize') 

3087 curv = self.berry_curvature(bands, nk, plane, k_fixed) 

3088 n1, n2 = curv.shape 

3089 # the corners of each plaquette, counterclockwise, in fractional coordinates 

3090 f1, f2 = np.meshgrid(np.arange(n1) / n1, np.arange(n2) / n2, indexing='ij') 

3091 corner = np.stack([f1.ravel(), f2.ravel()], axis=-1)[:, None, :] 

3092 quads = corner + np.array([[0., 0.], [1. / n1, 0.], [1. / n1, 1. / n2], [0., 1. / n2]]) 

3093 b_i, b_j = self.rec_vec_k[plane[0]], self.rec_vec_k[plane[1]] 

3094 if self.dim == 2: 

3095 quads = self._fold(quads) 

3096 else: 

3097 # orthonormal coordinates of the plane, the first along b_i 

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

3099 v = b_j - (b_j @ u) * u 

3100 b_i, b_j = np.array([b_i @ u, 0.]), np.array([b_j @ u, b_j @ v / np.linalg.norm(v)]) 

3101 quads = quads @ np.array([b_i, b_j]) 

3102 area = abs(b_i[0] * b_j[1] - b_i[1] * b_j[0]) / (n1 * n2) 

3103 omega = curv.ravel() / area 

3104 vmax = np.abs(omega).max() or 1. 

3105 fig, ax = plt.subplots(figsize=figsize) 

3106 patches = PolyCollection(quads, array=omega, cmap=cmap, norm=Normalize(-vmax, vmax), 

3107 edgecolors='face', linewidths=0.2) 

3108 ax.add_collection(patches) 

3109 fig.colorbar(patches, ax=ax).set_label(r'$\Omega(\mathbf{k})$', fontsize=fs) 

3110 if self.dim == 2: 

3111 self._zone_axes(ax, fs) 

3112 else: 

3113 ax.autoscale_view() 

3114 ax.set_aspect('equal') 

3115 ax.set_xlabel('$k_1$', fontsize=fs) 

3116 ax.set_ylabel('$k_2$', fontsize=fs) 

3117 ax.set_title('Berry curvature, $C = {:.3f}$'.format(curv.sum() / (2 * PI)), fontsize=fs) 

3118 fig.set_layout_engine('tight') 

3119 plt.draw() 

3120 return fig 

3121 

3122 def plot_wannier_flow( 

3123 self, bands: int | list[int], nk: int = 100, nk_perp: int = 51, direction: int = 0, 

3124 flow: int | None = None, k_fixed: float = 0., positions: bool = True, 

3125 k_range: tuple[float, float] = (0., 1.), ms: float = 4., c: str = 'b', fs: float = 20, 

3126 figsize: tuple[float, float] | None = None, 

3127 ) -> Figure: 

3128 r''' 

3129 Plot the flow of the hybrid Wannier centres (the Wilson-loop 

3130 spectrum) of *wannier_flow*: the centres along *direction*, in 

3131 units of :math:`\mathbf{a}_{direction}`, as the loop moves along 

3132 the reciprocal vector *flow*. They wind :math:`C` times in a Chern 

3133 insulator, and the Kramers pairs swap partners in a 

3134 :math:`\mathbb{Z}_2` topological insulator. 

3135 

3136 :param bands: See *wannier_flow*. 

3137 :param nk: See *wannier_flow*. 

3138 :param nk_perp: See *wannier_flow*. 

3139 :param direction: See *wannier_flow*. 

3140 :param flow: See *wannier_flow*. 

3141 :param k_fixed: See *wannier_flow*. 

3142 :param positions: See *wannier_flow*. 

3143 :param k_range: See *wannier_flow*. 

3144 :param ms: Positive number. Default value 4. Marker size. 

3145 :param c: Default value 'b'. Marker color. 

3146 :param fs: Positive number. Default value 20. Fontsize. 

3147 :param figsize: Tuple. Default value None. Figure size. 

3148 

3149 :returns: 

3150 * **fig** -- Figure. 

3151 ''' 

3152 error_handling.positive_real(ms, 'ms') 

3153 error_handling.positive_real(fs, 'fs') 

3154 error_handling.tuple_2elem(figsize, 'figsize') 

3155 fracs, centers = self.wannier_flow(bands, nk, nk_perp, direction, flow, k_fixed, 

3156 positions, k_range) 

3157 if flow is None: 

3158 flow = [d for d in range(self.dim) if d != direction][0] 

3159 fig, ax = plt.subplots(figsize=figsize) 

3160 ax.plot(fracs, centers, 'o', c=c, ms=ms) 

3161 ax.set_xlim(fracs[0], fracs[-1]) 

3162 ax.set_ylim(0., 1.) 

3163 ax.set_xlabel(r'$k_{0}/|\mathbf{{b}}_{0}|$'.format(flow + 1), fontsize=fs) 

3164 ax.set_ylabel(r'Wannier centres ($\mathbf{{a}}_{}$)'.format(direction + 1), fontsize=fs) 

3165 fig.set_layout_engine('tight') 

3166 plt.draw() 

3167 return fig 

3168 

3169 def plot_surface_spectral_function( 

3170 self, points: list[ArrayLike], energies: ArrayLike, direction: int, nk: int = 60, 

3171 side: int = 1, eta: float = 1e-2, bulk: bool = False, 

3172 node_labels: list[str] | None = None, log: bool = True, cmap: str = 'magma', 

3173 fs: float = 20, figsize: tuple[float, float] | None = None, 

3174 ) -> Figure: 

3175 r''' 

3176 Plot the surface spectral function of *surface_spectral_function* 

3177 along a path through the surface Brillouin zone (built as in 

3178 *k_path*), as an ARPES-like map of :math:`E` against 

3179 :math:`\mathbf{k}_\parallel`. Surface states are the sharp lines in 

3180 the bulk gaps. 

3181 

3182 :param points: List of at least two k-points, the nodes of the path 

3183 (see *k_path*). 

3184 :param energies: Real array. Energies :math:`E`. 

3185 :param direction: See *surface_spectral_function*. 

3186 :param nk: Positive integer. Default value 60. Number of k-points per path segment. 

3187 :param side: See *surface_spectral_function*. 

3188 :param eta: See *surface_spectral_function*. 

3189 :param bulk: See *surface_spectral_function*. 

3190 :param node_labels: List of strings. Default value None. Labels of *points*. 

3191 :param log: Boolean. Default value True. Logarithmic color scale, over 

3192 four decades below the maximum (the surface states are much 

3193 sharper than the bulk continuum). 

3194 :param cmap: Default value 'magma'. Colormap. 

3195 :param fs: Positive number. Default value 20. Fontsize. 

3196 :param figsize: Tuple. Default value None. Figure size. 

3197 

3198 :returns: 

3199 * **fig** -- Figure. 

3200 ''' 

3201 error_handling.k_path_points(points, self.dim) 

3202 error_handling.positive_int(nk, 'nk') 

3203 error_handling.boolean(log, 'log') 

3204 error_handling.positive_real(fs, 'fs') 

3205 error_handling.tuple_2elem(figsize, 'figsize') 

3206 if node_labels is not None: 

3207 error_handling.ndarray(np.array(node_labels), 'node_labels', len(points)) 

3208 ks, dist, nodes = _path(points, nk) 

3209 spec = self.surface_spectral_function(ks, energies, direction, side, eta, bulk) 

3210 energies = np.atleast_1d(np.asarray(energies, dtype='f8')) 

3211 vmax = spec.max() if spec.max() > 0 else 1. 

3212 norm = LogNorm(vmax * 1e-4, vmax) if log else Normalize(0., vmax) 

3213 fig, ax = plt.subplots(figsize=figsize) 

3214 mesh = ax.pcolormesh(dist, energies, np.maximum(spec, vmax * 1e-4).T, cmap=cmap, 

3215 norm=norm, shading='nearest') 

3216 fig.colorbar(mesh, ax=ax).set_label(r'$A(\mathbf{k}_\parallel, E)$', fontsize=fs) 

3217 for node in nodes: 

3218 ax.axvline(node, color='w', lw=0.5) 

3219 if node_labels is not None: 

3220 ax.set_xticks(nodes) 

3221 ax.set_xticklabels(node_labels, fontsize=fs) 

3222 ax.set_ylabel('$E$', fontsize=fs) 

3223 ax.set_title('Bulk' if bulk else 'Surface', fontsize=fs) 

3224 fig.set_layout_engine('tight') 

3225 plt.draw() 

3226 return fig 

3227 

3228 def plot_dos( 

3229 self, 

3230 nk: int | tuple[int, ...] = 30, 

3231 broadening: float = 0.05, 

3232 kernel: str = 'gaussian', 

3233 e_grid: ArrayLike | None = None, 

3234 fs: float = 20, 

3235 lw: float = 2., 

3236 figsize: tuple[float, float] | None = None, 

3237 ) -> Figure: 

3238 ''' 

3239 Plot the density of states, obtained by diagonalizing 

3240 :math:`H(\\mathbf{k})` over a uniform Brillouin-zone mesh: broadened 

3241 (see *tbkit.dos.density_of_states*), or by the linear tetrahedron 

3242 method (``kernel='tetrahedron'``, see *tbkit.dos.tetrahedron_dos*), 

3243 which needs no broadening and keeps band edges and van Hove 

3244 singularities sharp. 

3245 

3246 :param nk: Positive integer, or tuple of *dim* positive integers. 

3247 Default value 30. Number of k-points along each reciprocal 

3248 lattice vector. 

3249 :param broadening: Positive real number. Default value 0.05. Kernel 

3250 width (unused by the tetrahedron method). 

3251 :param kernel: String. Default value 'gaussian'. 'gaussian', 

3252 'lorentzian' or 'tetrahedron'. 

3253 :param e_grid: Real ndarray. Default value None. Energies at which to 

3254 evaluate the density of states. 

3255 :param fs: Positive number. Default value 20. Fontsize. 

3256 :param lw: Positive number. Default value 2. Linewidth. 

3257 :param figsize: Tuple. Default value None. Figure size. 

3258 

3259 :returns: 

3260 * **fig** -- Figure. 

3261 ''' 

3262 error_handling.positive_real(fs, 'fs') 

3263 error_handling.positive_real(lw, 'lw') 

3264 error_handling.tuple_2elem(figsize, 'figsize') 

3265 error_handling.kspace_dos_kernel(kernel) 

3266 en = self.mesh_bands(nk) 

3267 if kernel != 'tetrahedron': 

3268 return dos._plot_density_of_states(en, e_grid, broadening, kernel, fs, lw, figsize) 

3269 nk = (nk,) * self.dim if isinstance(nk, int) else nk 

3270 e_grid, rho = dos.tetrahedron_dos(en.reshape(*nk, self.norb), e_grid) 

3271 return dos._plot_dos_curve(e_grid, rho, fs, lw, figsize) 

3272 

3273 def plot_bands( 

3274 self, 

3275 node_labels: list[str] | None = None, 

3276 fs: float = 20, 

3277 lw: float = 2., 

3278 ms: float = 0., 

3279 c: str = 'b', 

3280 lims: tuple[float, float] | None = None, 

3281 figsize: tuple[float, float] | None = None, 

3282 weights: ArrayLike | None = None, 

3283 style: str = 'color', 

3284 cmap: str | None = None, 

3285 ) -> Figure: 

3286 ''' 

3287 Plot the band structure computed by *k_path* or *get_bands*, against 

3288 the cumulative distance along the k-points (vertical lines mark the 

3289 nodes of a *k_path*). For a non-Hermitian model, the real part of 

3290 the energies is plotted. With *weights* (e.g. from *band_weights*: 

3291 an orbital, sublattice or spin projection), a "fat band" plot: the 

3292 bands are colored by the weights (with a colorbar), or drawn with 

3293 dots sized by their absolute value. 

3294 

3295 :param node_labels: List of strings. Default value None. Labels of the 

3296 high-symmetry points passed to *k_path*. 

3297 :param fs: Positive number. Default value 20. Fontsize. 

3298 :param lw: Positive number. Default value 2. Linewidth. 

3299 :param ms: Positive number. Default value 0. Marker size. 

3300 :param c: Default value 'b'. Line color. 

3301 :param lims: List. Default value None. Energy plot limits. 

3302 :param figsize: Tuple. Default value None. Figure size. 

3303 :param weights: Real array, shape (nk, norb). Default value None. 

3304 One weight per k-point and band, e.g. from *band_weights*. 

3305 :param style: String. Default value 'color'. 'color': the bands 

3306 colored by *weights*; 'size': dots of size proportional to 

3307 :math:`|w|` (*ms*, or 8 if *ms* is 0, for the largest) on thin 

3308 lines of color *c*. 

3309 :param cmap: Default value None ('viridis', or 'RdBu_r', centred on 

3310 zero, if some weights are negative). Colormap of *style* 'color'. 

3311 

3312 :returns: 

3313 * **fig** -- Figure. 

3314 ''' 

3315 error_handling.empty_ndarray(self.en, 'get_bands or k_path') 

3316 error_handling.positive_real(fs, 'fs') 

3317 error_handling.positive_real(lw, 'lw') 

3318 error_handling.lims(lims) 

3319 error_handling.tuple_2elem(figsize, 'figsize') 

3320 fig, ax = plt.subplots(figsize=figsize) 

3321 if weights is None: 

3322 for n in range(self.norb): 

3323 ax.plot(self.ks_dist, self.en[:, n].real, c=c, lw=lw, marker='o', ms=ms) 

3324 else: 

3325 self._plot_fat_bands(fig, ax, weights, style, cmap, c, lw, ms) 

3326 for node in self.nodes: 

3327 ax.axvline(node, color='k', lw=0.5) 

3328 ax.set_xlim(self.ks_dist[0], self.ks_dist[-1]) 

3329 if lims is not None: 

3330 ax.set_ylim(lims) 

3331 if node_labels is not None: 

3332 error_handling.ndarray(np.array(node_labels), 'node_labels', len(self.nodes)) 

3333 ax.set_xticks(self.nodes) 

3334 ax.set_xticklabels(node_labels, fontsize=fs) 

3335 ax.set_ylabel('$E$', fontsize=fs) 

3336 for label in ax.yaxis.get_majorticklabels(): 

3337 label.set_fontsize(fs) 

3338 fig.set_layout_engine('tight') 

3339 plt.draw() 

3340 return fig 

3341 

3342 def _plot_fat_bands(self, fig, ax, weights, style, cmap, c, lw, ms) -> None: 

3343 ''' 

3344 Private method. The bands of *plot_bands* colored (or with dots 

3345 sized) by *weights*. 

3346 ''' 

3347 error_handling.band_weights(weights, self.en.shape) 

3348 error_handling.weight_style(style) 

3349 weights = np.asarray(weights, dtype='f8') 

3350 signed = weights.min() < 0 

3351 vmax = np.abs(weights).max() or 1. 

3352 if style == 'size': 

3353 size = (ms or 8.) ** 2 

3354 for n in range(self.norb): 

3355 ax.plot(self.ks_dist, self.en[:, n].real, c=c, lw=lw / 4) 

3356 ax.scatter(self.ks_dist, self.en[:, n].real, s=size * np.abs(weights[:, n]) / vmax, 

3357 c=c, lw=0) 

3358 return 

3359 norm = Normalize(-vmax if signed else 0., vmax) 

3360 cmap = cmap or ('RdBu_r' if signed else 'viridis') 

3361 for n in range(self.norb): 

3362 pts = np.stack([self.ks_dist, self.en[:, n].real], axis=-1) 

3363 lines = LineCollection(list(np.stack([pts[:-1], pts[1:]], axis=1)), cmap=cmap, norm=norm, lw=lw) 

3364 lines.set_array((weights[:-1, n] + weights[1:, n]) / 2) 

3365 ax.add_collection(lines) 

3366 ax.autoscale_view() 

3367 fig.colorbar(lines, ax=ax) 

3368 

3369 def show(self) -> None: 

3370 ''' 

3371 Emulate Matplotlib method plt.show(). 

3372 ''' 

3373 plt.show() 

3374 

3375 

3376def ribbon( 

3377 lat: Lattice, 

3378 list_hop: list[dict], 

3379 width: int, 

3380 direction: int = 1, 

3381 onsite: dict | None = None, 

3382 spin: bool = False, 

3383) -> KSpace: 

3384 r''' 

3385 Cut a ribbon out of a 2D periodic model: periodic along one primitive 

3386 vector, finite (open boundary, *width* unit cells) along the other. 

3387 This is the standard way to see edge states in a band structure (e.g. 

3388 the zero-energy edge band of a zigzag graphene ribbon, or the helical 

3389 edge states of a Kane-Mele ribbon). Cut out of a 3D model, it gives a 

3390 slab: periodic along the two other primitive vectors, the way to see 

3391 surface states (e.g. the Fermi arcs of a Weyl semimetal). 

3392 

3393 :param lat: **Lattice** class instance (2D or 3D, i.e. two or three primitive 

3394 vectors). Only *unit_cell* and *prim_vec* are used. 

3395 :param list_hop: List of dictionaries, in the same format passed to 

3396 *KSpace.set_hopping* -- the hoppings of the periodic (2D) model 

3397 that the ribbon is cut from. 

3398 :param width: Positive integer. Number of unit cells across the ribbon. 

3399 :param direction: 0 or 1 (or 2, in 3D). Default value 1. Which primitive 

3400 vector (``lat.prim_vec[direction]``) becomes finite; the others stay 

3401 periodic. 

3402 :param onsite: Dictionary. Default value None. Onsite energies, in the 

3403 same format passed to *KSpace.set_onsite* -- applied identically 

3404 on every row of the ribbon. 

3405 :param spin: Boolean. Default value False. See *KSpace*. 

3406 

3407 :returns: 

3408 * **rib** -- **KSpace** instance, 1D-periodic (2D-periodic for a slab), with 

3409 ``width * len(lat.unit_cell)`` sites (each site of *lat*, 

3410 repeated once per row across the ribbon; row *w*'s copy of site 

3411 *i* is orbital ``w*len(lat.unit_cell) + i``). 

3412 

3413 Example usage:: 

3414 

3415 # zigzag graphene ribbon, 20 unit cells wide 

3416 list_hop = [{'i': 0, 'j': 1, 'R': (0, 0), 't': 1.}, 

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

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

3419 rib = ribbon(lat, list_hop, width=20) 

3420 ''' 

3421 error_handling.lat(lat) 

3422 error_handling.dim_min(len(lat.prim_vec), 2) 

3423 error_handling.positive_int(width, 'width') 

3424 error_handling.direction(direction, len(lat.prim_vec)) 

3425 periodic = [d for d in range(len(lat.prim_vec)) if d != direction] 

3426 n_sites = len(lat.unit_cell) 

3427 a_dir = np.array(lat.prim_vec[direction]) 

3428 new_unit_cell = [] 

3429 for w in range(width): 

3430 for dic in lat.unit_cell: 

3431 r0 = np.array(dic['r0']) + w*a_dir 

3432 new_unit_cell.append({'tag': dic['tag'], 'r0': tuple(float(c) for c in r0)}) 

3433 new_lat = Lattice(unit_cell=new_unit_cell, prim_vec=[lat.prim_vec[d] for d in periodic]) 

3434 rib = KSpace(new_lat, spin=spin) 

3435 new_list_hop = [] 

3436 for dic in list_hop: 

3437 w2_shift = dic['R'][direction] 

3438 n_periodic = tuple(dic['R'][d] for d in periodic) 

3439 for w in range(width): 

3440 w2 = w + w2_shift 

3441 if 0 <= w2 < width: 

3442 new_list_hop.append({'i': w*n_sites + dic['i'], 

3443 'j': w2*n_sites + dic['j'], 

3444 'R': n_periodic, 

3445 't': dic['t']}) 

3446 rib.set_hopping(new_list_hop) 

3447 if onsite is not None: 

3448 rib.set_onsite(onsite) 

3449 return rib 

3450 

3451 

3452def magnetic_supercell( 

3453 lat: Lattice, 

3454 list_hop: list[dict], 

3455 p: int, 

3456 q: int, 

3457 onsite: dict | None = None, 

3458 spin: bool = False, 

3459) -> KSpace: 

3460 r''' 

3461 Thread a uniform magnetic field through a 2D periodic model, with a 

3462 rational flux :math:`\alpha = p/q` flux quanta per unit cell, and return 

3463 the Bloch Hamiltonian of its magnetic unit cell: *q* cells along 

3464 :math:`\mathbf{a}_1`. Each hopping gets its Peierls phase (see 

3465 *System.set_peierls_phase*) 

3466 

3467 .. math:: 

3468 

3469 \phi_{ij} = \frac{2\pi}{\Phi_0}\int_{\mathbf{r}_i}^{\mathbf{r}_j}\mathbf{A}\cdot d\mathbf{l} 

3470 = 2\pi\alpha\,\frac{s_{1,i}+s_{1,j}}{2}\,(s_{2,j}-s_{2,i}) 

3471 

3472 in the gauge :math:`\mathbf{A} = \alpha\Phi_0\, s_1\nabla s_2`, with 

3473 :math:`(s_1, s_2)` the fractional coordinates of a point along 

3474 :math:`(\mathbf{a}_1, \mathbf{a}_2)`, periodic along :math:`\mathbf{a}_2`. 

3475 A translation by :math:`q\mathbf{a}_1` is a symmetry only up to the 

3476 gauge transformation :math:`e^{-2\pi i p s_2}`, which the hoppings 

3477 crossing the magnetic cell boundary carry. The bands of the result are 

3478 the Hofstadter subbands, and their *chern_number* the TKNN integers. 

3479 

3480 :param lat: **Lattice** class instance, 2D (two primitive vectors in the plane). 

3481 :param list_hop: List of dictionaries, the hoppings of the field-free 

3482 model, in the format of *KSpace.set_hopping*. 

3483 :param p: Integer. Numerator of the flux per unit cell. 

3484 :param q: Positive integer. Denominator of the flux per unit cell: the 

3485 magnetic cell has *q* unit cells. 

3486 :param onsite: Dictionary. Default value None. Onsite energies, in the 

3487 format of *KSpace.set_onsite*, applied to every copy. 

3488 :param spin: Boolean. Default value False. See *KSpace*. 

3489 

3490 :returns: 

3491 * **mag** -- **KSpace** instance, with ``q * len(lat.unit_cell)`` 

3492 sites: copy *m* of site *i* is site ``m*len(lat.unit_cell) + i``, 

3493 displaced by :math:`m\mathbf{a}_1`. 

3494 

3495 Example usage:: 

3496 

3497 # square lattice, flux 1/3 per plaquette: three Hofstadter bands 

3498 mag = magnetic_supercell(lattices.square(), list_hop, p=1, q=3) 

3499 chern = [mag.chern_number(bands=[n]) for n in range(3)] 

3500 ''' 

3501 error_handling.lat(lat) 

3502 error_handling.planar_cell(lat.prim_vec) 

3503 error_handling.set_hopping_kspace(list_hop, len(lat.unit_cell), 2, spin) 

3504 error_handling.integer(p, 'p') 

3505 error_handling.positive_int(q, 'q') 

3506 alpha = p / q 

3507 n_sites = len(lat.unit_cell) 

3508 a = np.array(lat.prim_vec, dtype='f8') 

3509 frac = np.linalg.solve(a.T, np.array([dic['r0'] for dic in lat.unit_cell], dtype='f8').T).T 

3510 new_unit_cell = [] 

3511 for m in range(q): 

3512 for dic in lat.unit_cell: 

3513 r0 = np.array(dic['r0'], dtype='f8') + m * a[0] 

3514 new_unit_cell.append({'tag': dic['tag'], 'r0': (float(r0[0]), float(r0[1]))}) 

3515 new_lat = Lattice(unit_cell=new_unit_cell, prim_vec=[tuple(q * a[0]), lat.prim_vec[1]]) 

3516 mag = KSpace(new_lat, spin=spin) 

3517 new_list_hop = [] 

3518 for dic in list_hop: 

3519 n1, n2 = dic['R'] 

3520 for m in range(q): 

3521 s1_i, s2_i = m + frac[dic['i'], 0], frac[dic['i'], 1] 

3522 s1_j, s2_j = m + n1 + frac[dic['j'], 0], n2 + frac[dic['j'], 1] 

3523 big, m_j = divmod(m + n1, q) 

3524 phase = 2 * PI * alpha * 0.5 * (s1_i + s1_j) * (s2_j - s2_i) - 2 * PI * p * big * s2_j 

3525 new_list_hop.append({'i': m * n_sites + dic['i'], 'j': m_j * n_sites + dic['j'], 

3526 'R': (big, n2), 't': dic['t'] * np.exp(1j * phase)}) 

3527 mag.set_hopping(new_list_hop) 

3528 if onsite is not None: 

3529 mag.set_onsite(onsite) 

3530 return mag 

3531 

3532 

3533################################# 

3534# K-PATHS AND CONSTANT-ENERGY CONTOURS 

3535################################# 

3536 

3537 

3538def _response_weights( 

3539 en: NDArray[np.float64], mu: float, temperature: float, response: str, 

3540) -> NDArray[np.float64]: 

3541 r''' 

3542 Private. The weights :math:`P_{nm}` of the Kubo pair sum of 

3543 *KSpace._kubo*, shape (nk, n, n), from the bands *en* (nk, n). With 

3544 :math:`x_n = (E_n-\mu)/T`, the conductivities are 

3545 :math:`\sum_n w_n\Omega_n`, i.e. :math:`P_{nm} = -(w_n - w_m)`, with 

3546 :math:`w = f` ('hall'), the entropy per state 

3547 :math:`s = -f\ln f-(1-f)\ln(1-f)` ('nernst') and 

3548 :math:`c_2/T = T\int_x^\infty y^2(-f'(y))\,dy` ('thermal'). The orbital 

3549 magnetization ('magnetization'), 

3550 :math:`\sum_n[f_n\,\mathrm{Im}\langle\partial_xu_n|(H-E_n)|\partial_yu_n\rangle + g_n\Omega_n]` 

3551 with :math:`g = T\ln(1+e^{-x})` (:math:`\max(\mu-E, 0)` at :math:`T = 0`), 

3552 has :math:`P_{nm} = \frac12(f_n+f_m)(E_m-E_n) - (g_n-g_m)`. 

3553 ''' 

3554 if response == 'magnetization': 

3555 f = occupation.fermi_dirac(en, mu, temperature) 

3556 if temperature == 0: 

3557 g = np.maximum(mu - en, 0.) 

3558 else: 

3559 g = temperature * np.logaddexp(0., -(en - mu) / temperature) 

3560 return 0.5 * (f[:, :, None] + f[:, None, :]) * (en[:, None, :] - en[:, :, None]) \ 

3561 - (g[:, :, None] - g[:, None, :]) 

3562 if response == 'hall': 

3563 w = occupation.fermi_dirac(en, mu, temperature) 

3564 else: 

3565 a = np.abs(en - mu) / temperature 

3566 if response == 'nernst': 

3567 w = np.log1p(np.exp(-a)) + a * expit(-a) 

3568 else: 

3569 # int_a^inf y^2 (-f') dy = a^2 f(a) + 2 [a ln(1 + e^-a) - Li2(-e^-a)] 

3570 # for a >= 0, and pi^2/3 minus it below the Fermi level 

3571 # (Li2(z) = spence(1 - z) in scipy's convention) 

3572 c2 = a ** 2 * expit(-a) + 2 * (a * np.log1p(np.exp(-a)) - spence(1 + np.exp(-a))) 

3573 w = temperature * np.where(en >= mu, c2, PI ** 2 / 3 - c2) 

3574 return -(w[:, :, None] - w[:, None, :]) 

3575 

3576 

3577def _path( 

3578 points: list[ArrayLike], nk: int, 

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

3580 ''' 

3581 Private function. The k-points of a path through *points* (*nk* per 

3582 segment, the last point included), their cumulative distance, and the 

3583 positions of the nodes along it. 

3584 ''' 

3585 pts = np.atleast_2d(np.asarray(points, dtype='f8')) 

3586 ks = np.concatenate([np.linspace(pts[i], pts[i+1], nk, endpoint=False) 

3587 for i in range(len(pts) - 1)] + [pts[-1:]]) 

3588 dist = np.concatenate([[0.], np.cumsum(np.linalg.norm(np.diff(ks, axis=0), axis=1))]) 

3589 return ks, dist, dist[::nk][:len(pts)-1].tolist() + [dist[-1]] 

3590 

3591 

3592def _marching_table(dim: int) -> dict[int, list[list[tuple[int, int]]]]: 

3593 ''' 

3594 Private function. For each pattern of the vertices of a triangle 

3595 (*dim* = 2) or tetrahedron (*dim* = 3) above a level (bit m set if 

3596 vertex m is above), the pieces of the level set, as lists of the edges 

3597 (a, b) that they cross: one segment in 2D; one triangle, or two 

3598 triangles forming a quadrilateral when two vertices are above, in 3D. 

3599 ''' 

3600 table = {} 

3601 for pattern in range(1, 2 ** (dim + 1) - 1): 

3602 up = [m for m in range(dim + 1) if pattern >> m & 1] 

3603 down = [m for m in range(dim + 1) if not pattern >> m & 1] 

3604 if dim == 3 and len(up) == 2: 

3605 (a, b), (c, d) = up, down 

3606 table[pattern] = [[(a, c), (a, d), (b, d)], [(a, c), (b, d), (b, c)]] 

3607 else: 

3608 table[pattern] = [[(a, b) for a in up for b in down]] 

3609 return table 

3610 

3611 

3612def _level_set(values: NDArray[np.float64], level: float) -> NDArray[np.float64]: 

3613 ''' 

3614 Private function. The level set ``values == level`` of a function on a 

3615 periodic 2D or 3D mesh (*values* of shape (n1, n2) or (n1, n2, n3)), 

3616 linear in each simplex of *dos._mesh_simplices*: segments (shape 

3617 (n, 2, 2)) or triangles (shape (n, 3, 3)), in fractional coordinates 

3618 (not folded: between 0 and 1 + 1/n_d). 

3619 ''' 

3620 shape, dim = values.shape, values.ndim 

3621 offsets = dos._mesh_simplices(dim) 

3622 vals = dos._simplex_values(values - level, offsets).reshape(dim + 1, -1) 

3623 pattern = sum((vals[m] > 0).astype(int) << m for m in range(dim + 1)) 

3624 pieces = [np.empty((0, dim, dim))] 

3625 for pat, polygons in _marching_table(dim).items(): 

3626 sel = np.nonzero(pattern == pat)[0] 

3627 if not len(sel): 

3628 continue 

3629 simplex, cell = np.divmod(sel, int(np.prod(shape))) 

3630 corner = np.stack(np.unravel_index(cell, shape), axis=-1) 

3631 pos = corner[None] + offsets[simplex].transpose(1, 0, 2) 

3632 v = vals[:, sel] 

3633 for polygon in polygons: 

3634 # the crossing on edge (a, b): v[a] > 0 >= v[b] or the reverse 

3635 pieces.append(np.stack([pos[a] + (v[a] / (v[a] - v[b]))[:, None] * (pos[b] - pos[a]) 

3636 for a, b in polygon], axis=1)) 

3637 return np.concatenate(pieces) / np.array(shape) 

3638 

3639 

3640def _zone_polygon(rec: NDArray[np.float64]) -> NDArray[np.float64]: 

3641 ''' 

3642 Private function. Vertices, counterclockwise, of the first Brillouin 

3643 zone (Wigner-Seitz cell) of the 2D reciprocal lattice with basis *rec* 

3644 (rows): the points at least as close to the origin as to any other 

3645 reciprocal lattice vector, where two of the bisectors meet. 

3646 ''' 

3647 gs = np.array([n @ rec for n in product(range(-2, 3), repeat=2) if any(n)]) 

3648 half = np.sum(gs ** 2, axis=1) / 2 

3649 tol = 1e-9 * half.max() 

3650 found = [] 

3651 for i in range(len(gs)): 

3652 for j in range(i + 1, len(gs)): 

3653 mat = gs[[i, j]] 

3654 if abs(np.linalg.det(mat)) > tol: 

3655 k = np.linalg.solve(mat, half[[i, j]]) 

3656 if np.all(gs @ k <= half + tol): 

3657 found.append(k) 

3658 verts = np.unique(np.round(np.array(found), 9), axis=0) 

3659 return verts[np.argsort(np.arctan2(verts[:, 1], verts[:, 0]))] 

3660 

3661 

3662def _gauss_reduce(u: NDArray[np.float64], v: NDArray[np.float64]): 

3663 ''' 

3664 Private function. Gauss-reduced basis of a 2D lattice: the two 

3665 shortest independent vectors, |u| <= |v|, at an obtuse (or right) 

3666 angle, so that the edges of the Wigner-Seitz cell are normal to 

3667 u, v and u + v. 

3668 ''' 

3669 while True: 

3670 if u @ u > v @ v: 

3671 u, v = v, u 

3672 m = np.round(u @ v / (u @ u)) 

3673 if m == 0: 

3674 break 

3675 v = v - m * u 

3676 return (u, v - u) if u @ v > 0 else (u, v) 

3677 

3678 

3679def _successive_minima(vecs: NDArray[np.float64]): 

3680 ''' 

3681 Private function. The shortest lattice vectors of a 3D lattice with 

3682 basis *vecs* (rows): the three successive minima, the shells of 

3683 lattice vectors sorted by length (list of arrays), and their lengths. 

3684 ''' 

3685 pts = np.array([n @ vecs for n in product(range(-3, 4), repeat=3) if any(n)]) 

3686 lengths = np.linalg.norm(pts, axis=1) 

3687 # by length, then (among equal lengths) the most positive first: x, then y, then z 

3688 order = np.lexsort((-pts[:, 2], -pts[:, 1], -pts[:, 0], np.round(lengths / lengths.min(), 6))) 

3689 pts, lengths = pts[order], lengths[order] 

3690 minima: list = [] 

3691 for p in pts: 

3692 if np.linalg.matrix_rank(np.array(minima + [p]), tol=1e-8 * lengths[0]) == len(minima) + 1: 

3693 minima.append(p) 

3694 if len(minima) == 3: 

3695 break 

3696 bounds = np.nonzero(np.diff(lengths) > 1e-6 * lengths[0])[0] + 1 

3697 return np.array(minima), np.split(pts, bounds), [s[0] for s in np.split(lengths, bounds)] 

3698 

3699 

3700def _orthogonal_axes(shell: NDArray[np.float64]) -> NDArray[np.float64]: 

3701 ''' 

3702 Private function. Three mutually orthogonal unit vectors among the 

3703 vectors of *shell* (the conventional cubic axes). 

3704 ''' 

3705 x = shell[0] / np.linalg.norm(shell[0]) 

3706 y = next(s for s in shell if abs(s @ x) < 1e-6 * np.linalg.norm(s)) 

3707 y = y / np.linalg.norm(y) 

3708 return np.array([x, y, np.cross(x, y)]) 

3709 

3710 

3711def cubic_path( 

3712 points: dict[str, ArrayLike], names: list[str], a: float = 1., axes: ArrayLike | None = None, 

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

3714 r''' 

3715 Build a path through k-points given in the conventional coordinates of 

3716 a cubic crystal: in units of :math:`2\pi/a` along the cubic axes, with 

3717 :math:`a` the side of the conventional cube, as tables of high-symmetry 

3718 points list them (e.g. :math:`X = (0, 1, 0)` and 

3719 :math:`L = (\tfrac12, \tfrac12, \tfrac12)` for fcc). *high_symmetry_path* 

3720 builds the standard simple cubic, fcc and bcc paths with it; use it 

3721 for another path, or other points, of the same lattices. 

3722 

3723 :param points: Dictionary. key: label, val: tuple of 3 real numbers, 

3724 the point in units of :math:`2\pi/a`. 

3725 :param names: List of at least two labels (keys of *points*), the 

3726 nodes of the path in order. 

3727 :param a: Positive real. Default value 1. Side of the conventional cube. 

3728 :param axes: Real array, shape (3, 3). Default value None: the 

3729 Cartesian axes. Rows: the three orthonormal cubic axes, in the 

3730 coordinates of *KSpace.get_ham* (for a crystal whose cube is rotated). 

3731 

3732 :returns: 

3733 * **points** -- List of real ndarrays, the k-points, for *KSpace.k_path*. 

3734 * **labels** -- List of strings, *names*, for *node_labels*. 

3735 

3736 Example usage:: 

3737 

3738 # fcc, along Gamma - X - U and K - Gamma - L 

3739 pts = {'G': (0, 0, 0), 'X': (0, 1, 0), 'U': (.25, 1, .25), 

3740 'K': (.75, .75, 0), 'L': (.5, .5, .5)} 

3741 points, labels = cubic_path(pts, ['G', 'X', 'U'], a=3.6) 

3742 ''' 

3743 error_handling.cubic_path(points, names, axes) 

3744 error_handling.positive_real(a, 'a') 

3745 axes = np.eye(3) if axes is None else np.asarray(axes, dtype='f8') 

3746 return [2 * PI / a * np.asarray(points[n], dtype='f8') @ axes for n in names], list(names) 

3747 

3748 

3749def _bravais_3d(vecs: NDArray[np.float64]): 

3750 ''' 

3751 Private function. The points and labels of the high-symmetry path of a 

3752 3D lattice (basis *vecs*, rows), or (None, None) if it is not one of 

3753 the simple cubic, fcc, bcc, simple tetragonal or hexagonal lattices. 

3754 ''' 

3755 minima, shells, radii = _successive_minima(vecs) 

3756 lengths = np.linalg.norm(minima, axis=1) 

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

3758 cosine = {p: abs(minima[p[0]] @ minima[p[1]]) / (lengths[p[0]] * lengths[p[1]]) for p in pairs} 

3759 equal = {p: abs(lengths[p[0]] - lengths[p[1]]) < 1e-6 * lengths[2] for p in pairs} 

3760 right = {p: cosine[p] < 1e-6 for p in pairs} 

3761 G = r'$\Gamma$' 

3762 if all(right.values()): 

3763 if all(equal.values()): # simple cubic 

3764 return cubic_path({G: (0, 0, 0), 'X': (0, .5, 0), 'M': (.5, .5, 0), 'R': (.5, .5, .5)}, 

3765 [G, 'X', 'M', G, 'R', 'X'], lengths[0], minima / lengths[:, None]) 

3766 if sum(equal.values()) != 1: # orthorhombic 

3767 return None, None 

3768 # simple tetragonal: a, a, c 

3769 (i, j), = [p for p in pairs if equal[p]] 

3770 k = 3 - i - j 

3771 a, c = lengths[i], lengths[k] 

3772 axes = np.array([minima[i] / a, minima[j] / a, minima[k] / c]) 

3773 pts = {G: (0, 0, 0), 'X': (0, .5, 0), 'M': (.5, .5, 0), 'Z': (0, 0, .5), 

3774 'R': (0, .5, .5), 'A': (.5, .5, .5)} 

3775 names = [G, 'X', 'M', G, 'Z', 'R', 'A', 'Z'] 

3776 scale = 2 * PI * np.array([1 / a, 1 / a, 1 / c]) 

3777 return [(scale * pts[n]) @ axes for n in names], names 

3778 for (i, j) in pairs: 

3779 k = 3 - i - j 

3780 if equal[(i, j)] and abs(cosine[(i, j)] - .5) < 1e-6 and \ 

3781 all(right[p] for p in pairs if p != (i, j)): 

3782 # hexagonal: reciprocal vectors of (a1, a2, c), in-plane ones at 120 degrees 

3783 rec = 2 * PI * np.linalg.inv(minima[[i, j, k]]).T 

3784 b1, b2 = _gauss_reduce(rec[0], rec[1]) 

3785 M, K, A = b1 / 2, (2 * b1 + b2) / 3, rec[2] / 2 

3786 hex_pts = {G: np.zeros(3), 'M': M, 'K': K, 'A': A, 'L': M + A, 'H': K + A} 

3787 names = [G, 'M', 'K', G, 'A', 'L', 'H', 'A'] 

3788 return [hex_pts[n] for n in names], names 

3789 if not all(equal.values()): 

3790 return None, None 

3791 if len(shells[0]) == 12: # fcc: 12 nearest neighbours at a/sqrt(2), then 6 at a 

3792 return cubic_path({G: (0, 0, 0), 'X': (0, 1, 0), 'W': (.5, 1, 0), 'K': (.75, .75, 0), 

3793 'L': (.5, .5, .5), 'U': (.25, 1, .25)}, 

3794 [G, 'X', 'W', 'K', G, 'L', 'U', 'W', 'L', 'K'], 

3795 radii[1], _orthogonal_axes(shells[1])) 

3796 if len(shells[0]) == 8 and all(abs(c - 1 / 3) < 1e-6 for c in cosine.values()): 

3797 # bcc: 8 nearest neighbours at a sqrt(3)/2, then 6 at a 

3798 return cubic_path({G: (0, 0, 0), 'H': (0, 0, 1), 'N': (.5, 0, .5), 'P': (.5, .5, .5)}, 

3799 [G, 'H', 'N', G, 'P', 'H'], radii[1], _orthogonal_axes(shells[1])) 

3800 return None, None 

3801 

3802 

3803def high_symmetry_path(lat: Lattice) -> tuple[list[NDArray[np.float64]], list[str]]: 

3804 r''' 

3805 Get a standard path through the high-symmetry points of the Brillouin 

3806 zone of a lattice, for *KSpace.k_path* and the labels of 

3807 *KSpace.plot_bands*. The Bravais lattice is recognized from 

3808 *lat.prim_vec*, whatever the choice of primitive vectors and the 

3809 orientation of the lattice: 

3810 

3811 * 1D: :math:`-X, \Gamma, X` (the whole zone, :math:`X = \mathbf{b}/2`). 

3812 * 2D (the five Bravais lattices): square :math:`\Gamma X M \Gamma`; 

3813 rectangular :math:`\Gamma X S Y \Gamma`; hexagonal 

3814 :math:`\Gamma M K \Gamma`; centred rectangular and oblique 

3815 :math:`\Gamma X H C H_1 Y \Gamma`, along the edges of the 

3816 zone: :math:`X, Y, C` are the midpoints of the edges normal to the 

3817 reciprocal vectors :math:`\mathbf{b}_1, \mathbf{b}_2, 

3818 \mathbf{b}_1+\mathbf{b}_2` of the reduced basis (with 

3819 :math:`|\mathbf{b}_1| \le |\mathbf{b}_2|`), :math:`H` and 

3820 :math:`H_1` the corners between them. 

3821 * 3D: simple cubic :math:`\Gamma X M \Gamma R X`; fcc 

3822 :math:`\Gamma X W K \Gamma L U W L K`; bcc :math:`\Gamma H N \Gamma P H`; 

3823 simple tetragonal :math:`\Gamma X M \Gamma Z R A Z`; hexagonal 

3824 :math:`\Gamma M K \Gamma A L H A`. These are the paths of Setyawan 

3825 and Curtarolo (Comput. Mater. Sci. 49, 299 (2010)), up to their 

3826 first discontinuity (a path here is one continuous line). 

3827 

3828 The other 3D lattices (orthorhombic, monoclinic, body-centred 

3829 tetragonal, rhombohedral, ...) raise a ValueError: give their points 

3830 to *k_path* directly. 

3831 

3832 :param lat: **lattice** class instance (only *prim_vec* is used). 

3833 

3834 :returns: 

3835 * **points** -- List of real ndarrays, the k-points, in the 

3836 coordinates of *KSpace.get_ham*. 

3837 * **labels** -- List of strings (LaTeX), for *node_labels*. 

3838 

3839 Example usage:: 

3840 

3841 points, labels = high_symmetry_path(lat) 

3842 ks.k_path(points, nk=60) 

3843 ks.plot_bands(node_labels=labels) 

3844 ''' 

3845 ks = KSpace(lat) 

3846 vecs = np.array(lat.prim_vec, dtype='f8') @ ks.k_basis # in the k coordinates 

3847 G = r'$\Gamma$' 

3848 if ks.dim == 1: 

3849 x = PI / vecs[0] 

3850 return [-x, np.zeros(1), x], ['$-X$', G, 'X'] 

3851 if ks.dim == 3: 

3852 points, labels = _bravais_3d(vecs) 

3853 error_handling.bravais_lattice(labels, 3) 

3854 return points, labels 

3855 rec = 2 * PI * np.linalg.inv(vecs).T 

3856 b1, b2 = _gauss_reduce(rec[0], rec[1]) 

3857 l1, l2, l3 = (np.linalg.norm(b) for b in (b1, b2, b1 + b2)) 

3858 close = lambda a, b: abs(a - b) < 1e-6 * max(abs(a), abs(b)) 

3859 cosine = b1 @ b2 / (l1 * l2) 

3860 if abs(cosine) < 1e-6: 

3861 if close(l1, l2): 

3862 return [np.zeros(2), b1 / 2, (b1 + b2) / 2, np.zeros(2)], [G, 'X', 'M', G] 

3863 return [np.zeros(2), b1 / 2, (b1 + b2) / 2, b2 / 2, np.zeros(2)], [G, 'X', 'S', 'Y', G] 

3864 if close(l1, l2) and close(cosine, -.5): 

3865 return [np.zeros(2), b1 / 2, (2 * b1 + b2) / 3, np.zeros(2)], [G, 'M', 'K', G] 

3866 # centred rectangular and oblique: around half of the hexagonal zone 

3867 b3 = b1 + b2 

3868 corner = lambda u, v: np.linalg.solve(np.array([u, v]), np.array([u @ u, v @ v]) / 2) 

3869 return ([np.zeros(2), b1 / 2, corner(b1, b3), b3 / 2, corner(b2, b3), b2 / 2, np.zeros(2)], 

3870 [G, 'X', 'H', 'C', '$H_1$', 'Y', G])