Coverage for tbkit/system.py: 100%

569 statements  

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

1from __future__ import annotations 

2 

3from typing import Callable 

4 

5from numpy.typing import ArrayLike 

6 

7import numpy as np 

8from numpy.typing import NDArray 

9import scipy.sparse as sparse 

10import scipy.linalg as LA 

11import scipy.sparse.linalg as SLA 

12from scipy.spatial import cKDTree 

13import numpy.random as rand 

14npc = np.char # `import numpy.char` needs NumPy >= 2.0 

15from itertools import product 

16from math import sin, cos 

17import tbkit.error_handling as error_handling 

18import tbkit.occupation as occupation 

19import tbkit.values as values 

20from tbkit.lattice import Lattice, COOR_DTYPE 

21 

22 

23PI = np.pi 

24ATOL = 1e-3 

25HOP_DTYPE = [('n', 'u2'), ('i', 'u4'), ('j', 'u4'), 

26 ('ang', 'f8'), ('tag', 'U2'), ('t', 'c16')] 

27 

28 

29def _upper(ang: NDArray) -> NDArray: 

30 ''' 

31 Private function. 

32 Mask of the bond angles (in degrees, from np.arctan2) that point into the 

33 half-plane :math:`[0, 180)`, within ATOL: a bond at -1e-14 degrees counts as 

34 horizontal (0), one at 179.99999 as pointing the other way. 

35 ''' 

36 return (ang >= -ATOL) & (ang < 180. - ATOL) 

37 

38 

39class System(): 

40 r''' 

41 Solve the Tight-Binding eigenvalue problem of a lattice defined  

42 by the class **lattice**. 

43 

44 :param lat: **lattice** class instance. 

45 :param periodic: Boolean, or tuple of booleans (one per primitive 

46 vector). Default value False. Periodic boundary conditions along the 

47 primitive vectors :math:`\mathbf{a}_i`: the sample is then the 

48 :math:`n_1\mathbf{a}_1\times n_2\mathbf{a}_2\,(\times n_3\mathbf{a}_3)` 

49 box of *lat.get_lattice* wrapped into a torus (True), or a cylinder 

50 (e.g. ``(True, False)``, periodic along :math:`\mathbf{a}_1` only). 

51 *lat.get_lattice* must have been called; sites may be removed 

52 afterwards (vacancies), but not moved. Every distance and bond 

53 angle is then that of the shortest image, so the neighbour orders of 

54 *set_hopping* include the bonds that wrap around, with their short 

55 bond vector; a bond order reaching half-way around the torus is 

56 refused (it would be ambiguous). A uniform magnetic field 

57 (*set_magnetic_field*) and the local Chern marker need open 

58 boundaries; *get_bott_index* is the Chern number of a torus. 

59 

60 Up to *dense_max* sites (default 5000), the distances between all pairs 

61 of sites are computed (and kept in *vec_hop*). Larger lattices only look 

62 for the neighbours they need, with a k-d tree, up to the largest 

63 neighbour order a call asks for: memory and time then grow linearly 

64 with the number of sites (*vec_hop* stays empty). 

65 ''' 

66 

67 #: Largest lattice (number of sites) whose pairwise distances are all computed. 

68 dense_max = 5000 

69 

70 def __init__(self, lat: Lattice, periodic: bool | tuple[bool, ...] = False) -> None: 

71 error_handling.lat(lat) 

72 self.periodic = error_handling.periodic(periodic, len(lat.prim_vec)) 

73 if any(self.periodic): 

74 error_handling.torus_lattice(lat.n1) 

75 self.lat = lat 

76 self.sites = self.lat.sites # used to check if sites changes 

77 self.coor_hop = np.array([], dtype=COOR_DTYPE) 

78 self.vec_hop = np.array([], dtype=[('dis', 'f8'), ('ang', 'f8')]) # Hopping distances and angles 

79 self.dist_uni = np.array([], 'f8') # Different hopping distances 

80 self.store_hop: dict = {} # Store the relevant hoppings (dynamic programming) 

81 self.hop = np.array([], dtype=HOP_DTYPE) # Hoppings to build-up the Hamiltonian 

82 self.onsite = np.array([], 'c16') # Onsite energies 

83 # Value functions (see tbkit.values): (directed bond keys, function) 

84 # of the hoppings whose row of `hop` holds a factor 't', and (site 

85 # indices, function) of the onsite terms added to `onsite`. 

86 self._hop_values: list = [] 

87 self._onsite_values: list = [] 

88 self.ham = sparse.csr_matrix(([], ([], [])), shape=(self.lat.sites, self.lat.sites)) # Hamiltonian 

89 self.en = np.array([], 'c16') # Eigenenergies 

90 self.rn = np.array([], 'c16') # Right eigenvectors: H |rn> = en |rn> 

91 self.ln = np.array([], 'c16') # Left eigenvectors: <ln| H = en <ln| 

92 self.intensity = np.array([], 'f8') # Intensities (|rn|**2) 

93 self.pola = np.array([], 'f8') # sublattices polarisation (|rn^{(S)}|**2) 

94 self.ipr = np.array([], 'f8') # Inverse Participation Ratio 

95 self.petermann = np.array([], 'f8') # Petermann factors 

96 self.nmax = 0 # number of different hoppings 

97 

98 def clear_hopping(self) -> None: 

99 ''' 

100 Clear structured array *hop*. 

101 ''' 

102 self.hop = np.array([], dtype=HOP_DTYPE) 

103 self._hop_values = [] 

104 

105 def get_distances(self, n_orders: int = 1) -> None: 

106 ''' 

107 Private method. 

108 Get distances and angles of the edges. The angle is that of the 

109 bond's projection on the (x, y) plane; in 3D space, a vertical bond 

110 gets angle 0 pointing up (+z) and -180 pointing down. 

111 

112 :param n_orders: Positive integer. Default value 1. Beyond *dense_max* 

113 sites, the number of neighbour orders (distinct distances) needed. 

114 ''' 

115 error_handling.sites(self.lat.sites) 

116 if self.lat.sites > self.dense_max: 

117 self._sparse_distances(n_orders) 

118 return 

119 self._pairs: NDArray | None = None 

120 if any(self.periodic): 

121 # shortest images, a block of rows at a time to bound the memory 

122 coords = self._coords() 

123 n = len(coords) 

124 d = np.empty((n, n, coords.shape[1])) 

125 step = max(1, 1000000 // n) 

126 for r0 in range(0, n, step): 

127 d[r0:r0 + step] = self._min_image(coords[None, :, :] - coords[r0:r0 + step, None, :]) 

128 dif = tuple(d[..., c] for c in range(d.shape[-1])) 

129 else: 

130 dif = self.lat.distances() 

131 dist = np.sqrt(sum(d ** 2 for d in dif)) 

132 ang = (180 / PI * np.arctan2(dif[1], dif[0])) 

133 if len(dif) == 3: 

134 # 'ang' is the angle of the bond's projection on the (x, y) plane. 

135 # A vertical bond has no such angle: orient it by dz instead, so 

136 # that the sign of 'ang' still tells the two directions apart. 

137 vertical = np.hypot(dif[0], dif[1]) < 1e-6 * np.maximum(dist, 1.) 

138 ang[vertical & (dif[2] > 0)] = 0. 

139 ang[vertical & (dif[2] <= 0)] = -180. 

140 self.vec_hop = np.zeros(dist.shape, dtype=[('dis', 'f8'), ('ang', 'f8')]) 

141 self.vec_hop['dis'] = dist 

142 self.vec_hop['ang'] = ang 

143 self.dist_uni = np.unique(self.vec_hop['dis'].round(4)) 

144 

145 def _coords(self) -> NDArray[np.float64]: 

146 ''' 

147 Private method. Site coordinates, shape (sites, space_dim). 

148 ''' 

149 return np.stack([self.lat.coor[f] for f in ('x', 'y', 'z')[:self.lat.space_dim]], axis=1) 

150 

151 def _box(self) -> NDArray[np.float64]: 

152 ''' 

153 Private method. The vectors n_i a_i of the sample of *get_lattice*, shape (dim, space_dim). 

154 ''' 

155 counts = np.array((self.lat.n1, self.lat.n2, self.lat.n3)[:len(self.lat.prim_vec)], dtype='f8') 

156 return counts[:, None] * np.array(self.lat.prim_vec, dtype='f8') 

157 

158 def _torus(self) -> NDArray[np.float64]: 

159 ''' 

160 Private method. The periodic vectors n_i a_i, shape (n_periodic, space_dim). 

161 ''' 

162 return self._box()[list(self.periodic)] 

163 

164 def _min_image(self, d: NDArray[np.float64]) -> NDArray[np.float64]: 

165 ''' 

166 Private method. Shortest images of displacements *d* (shape (..., space_dim)) 

167 under the periodic translations: the fractional coordinates along the 

168 periodic vectors are rounded, then the neighbouring images compared 

169 (rounding alone can miss the shortest one in a skewed cell). Only 

170 displacements longer than half the shortest period need that 

171 comparison: any other image of a shorter one is longer than it. 

172 ''' 

173 if not any(self.periodic): 

174 return d 

175 torus = self._torus() 

176 shape = d.shape 

177 d = d.reshape(-1, shape[-1]) # 2D, for fast matrix products 

178 frac = d @ np.linalg.pinv(self._box())[:, list(self.periodic)] 

179 d = d - np.rint(frac) @ torus 

180 sq = np.einsum('ij,ij->i', d, d) 

181 far = np.flatnonzero(sq > (self._shortest_period() / 2) ** 2) 

182 start = d[far] 

183 best, best_sq = start.copy(), sq[far] 

184 for m in product((-1, 0, 1), repeat=len(torus)): 

185 if any(m): 

186 cand = start + np.array(m, dtype='f8') @ torus 

187 cand_sq = np.einsum('ij,ij->i', cand, cand) 

188 shorter = cand_sq < best_sq - 1e-12 

189 best[shorter], best_sq[shorter] = cand[shorter], cand_sq[shorter] 

190 d[far] = best 

191 return d.reshape(shape) 

192 

193 def _bond_vectors(self, i: NDArray, j: NDArray) -> NDArray[np.float64]: 

194 ''' 

195 Private method. Bond vectors from sites *i* to sites *j*, shape 

196 (len(i), space_dim): the shortest image on a torus. 

197 ''' 

198 coords = self._coords() 

199 return self._min_image(coords[j] - coords[i]) 

200 

201 def _wrapped(self, i: NDArray, j: NDArray) -> NDArray: 

202 ''' 

203 Private method. Mask of the bonds i -> j that wrap around the torus 

204 (their shortest image is not the straight segment between the sites). 

205 ''' 

206 coords = self._coords() 

207 return np.linalg.norm(self._bond_vectors(i, j) - (coords[j] - coords[i]), axis=1) > ATOL 

208 

209 def _shortest_period(self) -> float: 

210 ''' 

211 Private method. Length of the shortest nonzero periodic translation 

212 (infinite with open boundaries). 

213 ''' 

214 if not any(self.periodic): 

215 return np.inf 

216 torus = self._torus() 

217 return min(np.linalg.norm(np.array(m, dtype='f8') @ torus) 

218 for m in product(range(-2, 3), repeat=len(torus)) if any(m)) 

219 

220 def _near_pairs(self, tree: cKDTree, coords: NDArray[np.float64], radius: float) -> NDArray: 

221 ''' 

222 Private method. Pairs i < j of sites closer than *radius* (shortest 

223 image on a torus, from the images of the sites under the periodic 

224 translations of -1, 0, +1 periods), sorted. 

225 ''' 

226 if not any(self.periodic): 

227 return tree.query_pairs(radius, output_type='ndarray') 

228 torus = self._torus() 

229 shifts = np.array(list(product((-1, 0, 1), repeat=len(torus))), dtype='f8') @ torus 

230 images = (coords[None, :, :] + shifts[:, None, :]).reshape(-1, coords.shape[1]) 

231 found = tree.sparse_distance_matrix(cKDTree(images), radius, output_type='ndarray') 

232 i, j = found['i'], found['j'] % len(coords) 

233 keep = i < j 

234 return np.unique(np.column_stack([i[keep], j[keep]]), axis=0).reshape(-1, 2) 

235 

236 def _sparse_distances(self, n_orders: int) -> None: 

237 ''' 

238 Private method. The pairs of sites up to the *n_orders*-th distinct 

239 distance (or all of them), from a k-d tree: the search radius grows 

240 until that many distinct distances are found, every pair within it 

241 being found. 

242 ''' 

243 coords = self._coords() 

244 tree = cKDTree(coords) 

245 near = tree.query(coords, k=2)[0][:, 1] 

246 radius = max(np.min(near[near > 1e-9]) if np.any(near > 1e-9) else 1., 1e-6) 

247 span = np.max(np.ptp(coords, axis=0)) * np.sqrt(coords.shape[1]) + radius 

248 while True: 

249 pairs = self._near_pairs(tree, coords, radius * (1. + 1e-6) + ATOL) 

250 dis = np.linalg.norm(self._bond_vectors(pairs[:, 0], pairs[:, 1]), axis=1) 

251 uni = np.unique(dis.round(4)) 

252 if len(uni) >= n_orders + 1 or radius > span: 

253 break 

254 radius *= 1.5 

255 self.vec_hop = np.array([], dtype=[('dis', 'f8'), ('ang', 'f8')]) 

256 self._pairs, self._pair_dis = pairs, dis 

257 # the (n+1)-th distance may be incomplete: keep the first n_orders 

258 self.dist_uni = np.concatenate([[0.], uni[:n_orders] if radius <= span else uni]) 

259 

260 def _angles(self, i: NDArray, j: NDArray) -> NDArray[np.float64]: 

261 ''' 

262 Private method. Angles (degrees) of the bonds from sites i to sites j, 

263 with the vertical-bond rule of *get_distances*. 

264 ''' 

265 d = self._bond_vectors(i, j) 

266 ang = 180 / PI * np.arctan2(d[:, 1], d[:, 0]) 

267 if d.shape[1] == 3: 

268 vertical = np.hypot(d[:, 0], d[:, 1]) < 1e-6 * np.maximum(np.linalg.norm(d, axis=1), 1.) 

269 ang[vertical & (d[:, 2] > 0)] = 0. 

270 ang[vertical & (d[:, 2] <= 0)] = -180. 

271 return ang 

272 

273 def print_distances(self, n: int = 1) -> None: 

274 r''' 

275 Print distances and positive angles (in degrees) :math:`\phi_+\in[0, 180)` 

276 of the nth shortest edges. Negative angles are given by: 

277 :math:`\phi_-= \phi_+-180` and :math:`\phi_-\in[-180, 0)`. 

278 

279 :param n: Positive integer. Number of shortest edges. 

280 ''' 

281 error_handling.sites(self.lat.sites) 

282 error_handling.positive_int(n, 'n') 

283 self.get_distances(n) 

284 self.nmax = len(self.dist_uni) - 1 

285 error_handling.positive_int_lim(n, 'n', self.nmax) 

286 print('\n{} different distances between sites:'.format(self.nmax)) 

287 print('\nDistances between sites:') 

288 for i, d in enumerate(self.dist_uni[1: n+1]): 

289 if i == 0: 

290 hop_name = 'st' 

291 elif i == 1: 

292 hop_name = 'nd' 

293 elif i == 2: 

294 hop_name = 'rd' 

295 else: 

296 hop_name = 'th' 

297 print('{}{} hopping, length: {:.3f}'.format(i+1, hop_name, d)) 

298 print('\twith positive angles:') 

299 print('\t', np.unique(self.get_bonds(i + 1)[2].round(4))) 

300 

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

302 ''' 

303 Set onsite energies. 

304 

305 :param dict_onsite: Dictionary. key: sublattice tag, val: onsite 

306 energy, or a value function ``onsite(site, **params)``, evaluated 

307 by ``get_ham(**params)``. *site* is a structured array of the 

308 sites of that tag (fields 'x', 'y', ('z'), 'tag', 'index'); the 

309 function returns one energy, or one per site. *set_onsite_dis* 

310 adds to it. 

311 

312 Example usage:: 

313 

314 # Line-Centered Square lattice 

315 sys.set_onsite({'a': -1j, 'b': -2j}) 

316 # a gate voltage V across the sample, set by get_ham(V=...) 

317 sys.set_onsite({'a': lambda site, V: V * site['x'] / 10, 'b': 0.}) 

318 sys.get_ham(V=0.5) 

319 ''' 

320 error_handling.sites(self.lat.sites) 

321 error_handling.set_onsite(dict_onsite, self.lat.tags) 

322 self.onsite = np.zeros(self.lat.sites, 'c16') 

323 self._onsite_values = [] 

324 for tag, on in dict_onsite.items(): 

325 if callable(on): 

326 self._onsite_values.append((np.flatnonzero(self.lat.coor['tag'] == tag), on)) 

327 else: 

328 self.onsite[self.lat.coor['tag'] ==tag] = on 

329 

330 def get_bonds(self, n: int) -> tuple[NDArray, NDArray, NDArray]: 

331 ''' 

332 Private method. 

333 Get every bond of the nth shortest length once, oriented from *i* to *j* 

334 so that its angle lies in :math:`[0, 180)`. 

335 

336 The sign of *hop['ang']* is what marks a hopping as belonging to the 

337 upper or the lower part of the Hamiltonian (see *get_ham*). Sites sorted 

338 by (y, x), as *get_lattice* leaves them, give that orientation for 

339 :math:`i < j`, but *rotation*, *clean_coor*, *+=*, ... reorder the 

340 sites, so the orientation is taken from the angle itself. 

341 

342 :returns: 

343 * **i**, **j** -- Integer ndarrays. Bond end points. 

344 * **ang** -- Real ndarray. Bond angles, in degrees, in :math:`[0, 180)`. 

345 ''' 

346 error_handling.torus_size(self.dist_uni[n], self._shortest_period(), n) 

347 if self._pairs is not None: 

348 pairs = self._pairs[np.isclose(self.dist_uni[n], self._pair_dis, atol=ATOL)] 

349 pairs = pairs[np.lexsort((pairs[:, 1], pairs[:, 0]))] 

350 i, j = pairs[:, 0], pairs[:, 1] 

351 up = _upper(self._angles(i, j)) 

352 i, j = np.where(up, i, j), np.where(up, j, i) 

353 order = np.lexsort((j, i)) 

354 i, j = i[order], j[order] 

355 return i, j, np.maximum(self._angles(i, j), 0.) 

356 ind = np.argwhere(np.isclose(self.dist_uni[n], self.vec_hop['dis'], atol=ATOL)) 

357 ind = ind[_upper(self.vec_hop['ang'][ind[:, 0], ind[:, 1]])] 

358 ang = np.maximum(self.vec_hop['ang'][ind[:, 0], ind[:, 1]], 0.) 

359 return ind[:, 0], ind[:, 1], ang 

360 

361 def fill_store_hop(self, n: int) -> None: 

362 ''' 

363 Private method. 

364 

365 Store in *store_hop* indices, positive angles, and tags 

366 of a given type of hopping (see *get_bonds*). 

367 ''' 

368 i, j, ang = self.get_bonds(n) 

369 hop = np.zeros(len(i), dtype=HOP_DTYPE[:-1]) 

370 hop['i'] = i 

371 hop['j'] = j 

372 hop['ang'] = ang 

373 hop['tag'] = npc.add(self.lat.coor['tag'][i], self.lat.coor['tag'][j]) 

374 self.store_hop[n] = hop 

375 

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

377 r''' 

378 Set lattice hoppings. 

379 

380 :param list_hop: List of Dictionaries. 

381 Dictionary with keys ('n', 'ang', 'tag', 't') where: 

382 

383 * 'n' Positive integer, type of hoppings: 

384 

385 * 'n': 1 for nearest neighbours. 

386 * 'n': 2 for next-nearest neighbours.  

387 * 'n': 3 for next-next-nearest neighbours.  

388 * etc... 

389 

390 * 'ang' value, float, angle, in deg, of the hoppings. (optional). 

391 

392 Hopping angles are given by the method *print_distances*. 

393 

394 * If :math:`ang \in[0, 180)`, fill the Hamiltonian upper part. 

395 * If :math:`ang \in[-180, 0)`, fill the Hamiltonian lower part. 

396 

397 * 'tag' string of length 2 (optional). 

398 

399 Hopping tags. 

400 

401 * 't' Complex number, or value function. 

402 

403 Hopping value. A value function ``t(site_i, site_j, **params)`` 

404 is evaluated by ``get_ham(**params)``: *site_i* and *site_j* 

405 are structured arrays of the bonds' end points (fields 'x', 

406 'y', ('z'), 'tag', 'index'), and it returns one value, or 

407 one per bond (see the example below). Modifiers applied 

408 afterwards (*set_peierls_phase*, *set_hopping_dis*, ...) 

409 multiply it. 

410 

411 :param upper_part: Boolean. Default value True. 

412 

413 * True, get the hoppings with angles in :math:`[0, 180)`, *i.e.* fill 

414 the Hamiltonian upper part. 

415 * False, get the reversed hoppings, with angles in :math:`[-180, 0)`, 

416 *i.e.* fill the Hamiltonian lower part. 

417 

418 Example usage:: 

419 

420 # fill upper part: 

421 sys.set_hopping([{'n': 1, 't': 1.}]) 

422 # fill lower part: 

423 sys.set_hopping([{'n': 1, 't': 1.}], upper_part=False) 

424 # fill upper part: specifying the angles: 

425 sys.set_hopping([{'n': 1, 'ang': 0., 't': 1.}, {'n': 1, 'ang': 90., 't': 2.}]) 

426 # fill lower part: 

427 sys.set_hopping([{'n': 1, 'ang': -180., 't': 1.}, {'n': 1, 'ang': -90., 't': 2.}], 

428 upper_part=False) 

429 # fill upper part: specifying the tags: 

430 sys.set_hopping([{'n': 1, 'tag': 'ab', 't': 1.}, {'n': 1, 'tag': 'ba', 't': 2.}]) 

431 # fill lower part: 

432 sys.set_hopping([{'n': 1, 'tag': 'ab', 't': 1.}, {'n': 1, 'tag': 'ba', 't': 2.}], 

433 upper_part=False) 

434 # fill upper part: specifying the angles and tags: 

435 sys.set_hopping([{'n': 1, 'ang': 0., 'tag': 'ab', 't': 1.}, 

436 {'n': 1, 'ang': 0., 'tag': 'ba', 't': 2.}, 

437 {'n': 1, 'ang': 90., 'tag': 'ab', 't': 3.}, 

438 {'n': 1, 'ang': 90., 'tag': 'ba', 't': 4.}]) 

439 # fill lower part: 

440 sys.set_hopping([{'n': 1, 'ang': -180., 'tag': 'ab', 't': 1.}, 

441 {'n': 1, 'ang': -180., 'tag': 'ba', 't': 2.}, 

442 {'n': 1, 'ang': -90., 'tag': 'ab', 't': 3.}, 

443 {'n': 1, 'ang': -90., 'tag': 'ba', 't': 4.}], 

444 upper_part=False) 

445 # value function: a hopping that decays along x, swept without 

446 # rebuilding the model 

447 sys.set_hopping([{'n': 1, 't': lambda si, sj, t0, xi: t0 * np.exp(-si['x'] / xi)}]) 

448 for xi in (5., 10., 20.): 

449 sys.get_ham(t0=1., xi=xi) 

450 

451 .. note:: 

452 

453 A Hermitian hopping matrix can be built using only 

454 its upper part OR only its lower part. The full matrix is then 

455 built automatically by Hermitian conjugation. 

456 

457 If both the upper AND the lower parts are used to build the hopping 

458 matrix, no Hermitian conjugation is performed, *i.e.* a non-Hermitian 

459 hopping matrix can be built. 

460 ''' 

461 error_handling.sites(self.lat.sites) 

462 error_handling.boolean(upper_part, 'upper_part') 

463 self.get_distances(error_handling.max_order(list_hop)) 

464 self.nmax = len(self.dist_uni) - 1 

465 error_handling.set_hopping(list_hop, self.nmax) 

466 list_n = np.unique([dic['n'] for dic in list_hop]) 

467 # fill, if needed self.store_hop 

468 self.check_sites() 

469 for n in list_n: 

470 if n not in self.store_hop: 

471 self.fill_store_hop(n) 

472 # fill self.hop 

473 for dic in list_hop: 

474 if len(dic) == 2: 

475 size = len(self.store_hop[dic['n']]) 

476 if upper_part: 

477 mask = (self.hop['n'] == dic['n']) & (self.hop['ang'] >= 0) 

478 else: 

479 mask = (self.hop['n'] == dic['n']) & (self.hop['ang'] < 0) 

480 if np.sum(mask): 

481 self.hop = self.hop[np.logical_not(mask)] 

482 ind = np.ones(size, bool) 

483 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part) 

484 elif len(dic) == 3 and 'ang' in dic: 

485 error_handling.angle(dic['ang'], np.unique(self.store_hop[dic['n']]['ang']), upper_part) 

486 if dic['ang'] >= 0: 

487 ang_store = dic['ang'] 

488 else: 

489 ang_store = dic['ang'] + 180. 

490 size = np.sum(np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL)) 

491 mask = (self.hop['n'] == dic['n']) & np.isclose(self.hop['ang'], dic['ang'], atol=ATOL) 

492 if np.sum(mask): 

493 self.hop = self.hop[np.logical_not(mask)] 

494 ind = np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL) 

495 error_handling.index(ind, dic) 

496 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part) 

497 elif len(dic) == 3 and 'tag' in dic: 

498 if upper_part: 

499 tag_store = dic['tag'] 

500 else: 

501 tag_store = dic['tag'][::-1] 

502 size = np.sum(self.store_hop[dic['n']]['tag'] == tag_store) 

503 mask = ((self.hop['n'] == dic['n']) & (self.hop['tag'] == dic['tag'])) 

504 if upper_part: 

505 mask &= self.hop['ang'] >= 0 

506 else: 

507 mask &= self.hop['ang'] < 0 

508 if np.sum(mask): 

509 self.hop = self.hop[np.logical_not(mask)] 

510 ind = self.store_hop[dic['n']]['tag'] == tag_store 

511 error_handling.index(ind, dic) 

512 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part) 

513 else: 

514 error_handling.angle(dic['ang'], np.unique(self.store_hop[dic['n']]['ang']), upper_part=upper_part) 

515 error_handling.tag(dic['tag'], np.unique(self.store_hop[dic['n']]['tag'])) 

516 if dic['ang'] >= 0: 

517 ang_store = dic['ang'] 

518 else: 

519 ang_store = dic['ang'] + 180. 

520 if upper_part: 

521 tag_store = dic['tag'] 

522 else: 

523 tag_store = dic['tag'][::-1] 

524 size = np.sum((self.store_hop[dic['n']]['tag'] == tag_store) & 

525 (np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL))) 

526 bool1 = (self.hop['n'] == dic['n']) & (self.hop['tag'] == dic['tag']) 

527 bool2 = np.isclose(self.hop['ang'], dic['ang'], atol=ATOL) 

528 mask = bool1 & bool2 

529 if np.sum(mask): 

530 self.hop = self.hop[np.logical_not(mask)] 

531 ind = ((self.store_hop[dic['n']]['tag'] == tag_store) & 

532 (np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL))) 

533 error_handling.index(ind, dic) 

534 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part) 

535 self.hop = np.concatenate([self.hop, hop]) 

536 self._assign_hopping(np.arange(len(self.hop) - len(hop), len(self.hop)), dic['t']) 

537 

538 def check_sites(self) -> None: 

539 ''' 

540 Private method. 

541 Check if the number of sites was changed after calling the  

542 method system.set_hopping(). 

543 ''' 

544 if self.sites != self.lat.sites: 

545 self.store_hop = {} 

546 self.sites = self.lat.sites 

547 

548 def set_given_hopping( 

549 self, n: int, size: int, dic: dict, mask: NDArray, upper_part: bool, 

550 ) -> NDArray: 

551 ''' 

552 Private method. 

553 Fill self.hop.  

554 

555 :param n: Integer. Hopping type. 

556 :param size: Integer. Number of hoppings. 

557 :param dic: Dictionary. Hopping dictionary. 

558 :param mask: np.ndarray. Mask. 

559 :param upper_part: Boolean. If True, keep the orientation of *store_hop* 

560 (angles in [0, 180)), else reverse it. 

561 ''' 

562 hop = np.empty(size, dtype=HOP_DTYPE) 

563 hop['n'] = dic['n'] 

564 hop['t'] = 1. if callable(dic['t']) else dic['t'] 

565 if upper_part: 

566 hop['i'] = self.store_hop[n]['i'][mask] 

567 hop['j'] = self.store_hop[n]['j'][mask] 

568 hop['ang'] = self.store_hop[n]['ang'][mask] 

569 hop['tag'] = self.store_hop[n]['tag'][mask] 

570 else: 

571 hop['i'] = self.store_hop[n]['j'][mask] 

572 hop['j'] = self.store_hop[n]['i'][mask] 

573 hop['ang'] = self.store_hop[n]['ang'][mask] - 180 

574 hop['tag'] = npc.add(self.lat.coor['tag'][hop['i']], 

575 self.lat.coor['tag'][hop['j']]) 

576 return hop 

577 

578 def set_hopping_manual(self, dict_hop: dict[tuple[int, int], complex], upper_part: bool = True) -> None: 

579 ''' 

580 Set hoppings manually. 

581 

582 :param dict_hop: Dictionary of hoppings. 

583 key: hopping indices, val: hopping values. 

584 

585 :parameter upper_part: Boolean. Default value True. 

586 

587 * True, fill the Hamiltonian upper part. 

588 * False, fill the Hamiltonian lower part. 

589 

590 Example usage:: 

591 

592 sys.set_hopping_manual({(0, 1): 1., (1, 2): 2.}) 

593 ''' 

594 error_handling.sites(self.lat.sites) 

595 error_handling.set_hopping_def(None, dict_hop, self.lat.sites, 'dict_hop', same_site=True) 

596 hop = np.zeros(len(dict_hop), dtype=HOP_DTYPE) 

597 i = [h[0] for h in dict_hop.keys()] 

598 j = [h[1] for h in dict_hop.keys()] 

599 t = [val for val in dict_hop.values()] 

600 hop['i'], hop['j']= i, j 

601 hop['t'] = t 

602 hop['tag'] = npc.add(self.lat.coor['tag'][i], 

603 self.lat.coor['tag'][j]) 

604 d = self._bond_vectors(np.array(i, int), np.array(j, int)) 

605 ang = 180 / PI * np.arctan2(d[:, 1], d[:, 0]) 

606 if upper_part: 

607 ang[ang < 0] += 180 

608 else: 

609 ang[ang >= 0] -= 180 

610 hop['ang'] = ang 

611 self.hop = np.concatenate([self.hop, hop]) 

612 self._assign_hopping(np.arange(len(self.hop) - len(hop), len(self.hop)), hop['t']) 

613 

614 def _assign_hopping(self, rows: NDArray, t) -> None: 

615 ''' 

616 Private method. Set the values of the rows *rows* (indices or mask) 

617 of *hop* to *t*: number(s), or a value function (the rows then hold 

618 the factor 1). It replaces any value function set on the same bonds. 

619 ''' 

620 keys = self._bond_keys(rows) 

621 self._hop_values = [(k[~np.isin(k, keys)], f) for k, f in self._hop_values] 

622 self._hop_values = [(k, f) for k, f in self._hop_values if k.size] 

623 if callable(t): 

624 self.hop['t'][rows] = 1. 

625 self._hop_values.append((keys, t)) 

626 else: 

627 self.hop['t'][rows] = t 

628 

629 def _bond_keys(self, rows: NDArray | slice) -> NDArray[np.int64]: 

630 ''' 

631 Private method. One integer per directed bond (i, j) of the rows *rows* of *hop*. 

632 ''' 

633 return (self.hop['i'][rows].astype('i8') << 32) | self.hop['j'][rows].astype('i8') 

634 

635 def _sites(self) -> NDArray: 

636 ''' 

637 Private method. All the sites, as passed to value functions (see *tbkit.values.sites*). 

638 ''' 

639 return values.sites(self._coords(), self.lat.coor['tag'], np.arange(self.lat.sites)) 

640 

641 def _hop_t(self, params: dict) -> NDArray[np.complex128]: 

642 ''' 

643 Private method. The hopping values of *hop*, value functions evaluated with *params*. 

644 ''' 

645 t = self.hop['t'].copy() 

646 if not self._hop_values: 

647 return t 

648 keys, sites = self._bond_keys(slice(None)), self._sites() 

649 for k, func in self._hop_values: 

650 rows = np.flatnonzero(np.isin(keys, k)) 

651 t[rows] *= values.evaluate(func, (sites[self.hop['i'][rows]], sites[self.hop['j'][rows]]), 

652 params, len(rows)) 

653 return t 

654 

655 def _onsite_e(self, params: dict) -> NDArray[np.complex128]: 

656 ''' 

657 Private method. The onsite energies, value functions evaluated with *params*. 

658 ''' 

659 onsite = self.onsite.copy() 

660 if self._onsite_values: 

661 sites = self._sites() 

662 for idx, func in self._onsite_values: 

663 onsite[idx] += values.evaluate(func, (sites[idx],), params, len(idx)) 

664 return onsite 

665 

666 def set_hopping_dis(self, alpha: complex) -> None: 

667 ''' 

668 Set uniform hopping disorder.  

669 

670 :param alpha: Complex or Real number. Disorder strength. 

671 

672 Example usage:: 

673 

674 sys.set_hopping_dis(alpha=0.1) 

675 

676 ''' 

677 error_handling.empty_hop(self.hop) 

678 error_handling.number(alpha, 'alpha') 

679 self.hop['t'] *= 1. + alpha * rand.uniform(-1., 1., len(self.hop)) 

680 

681 def set_peierls_phase(self, phase: Callable[..., NDArray]) -> None: 

682 r''' 

683 Apply the Peierls substitution to the existing hoppings, to 

684 capture the effect of an orbital magnetic field: 

685 

686 .. math:: 

687 

688 t_{ij} \to t_{ij}\, e^{i\phi_{ij}}\, ,\quad 

689 \phi_{ij} = \frac{2\pi}{\Phi_0}\int_{\mathbf{r}_i}^{\mathbf{r}_j} 

690 \mathbf{A}\cdot d\mathbf{l} 

691 

692 where :math:`\mathbf{A}` is the vector potential, integrated along 

693 the straight bond from site :math:`i` to site :math:`j`. 

694 

695 Must be called after *set_hopping* / *set_hopping_manual* (it 

696 rescales the existing hoppings *in place*) and before *get_ham*. 

697 For a uniform perpendicular field, use the convenience method 

698 *set_magnetic_field* instead. 

699 

700 :param phase: Callable. ``phase(xi, yi, xj, yj)`` returns 

701 :math:`\phi_{ij}`, the (real-valued) Peierls phase for the bond 

702 from :math:`(x_i, y_i)` to :math:`(x_j, y_j)`. Called with 

703 Numpy arrays (one value per hopping in *sys.hop*). With periodic 

704 boundaries, :math:`(x_j, y_j)` is the end of the short bond, i.e. 

705 the image of site :math:`j` next to site :math:`i`, which lies 

706 outside the sample for a bond that wraps around. The vector 

707 potential must then be consistent with the torus: periodic (a 

708 field of zero net flux), or uniform (a flux threading the holes 

709 of the torus, i.e. twisted boundary conditions). 

710 

711 .. note:: 

712 

713 *get_ham* automatically assigns the reversed bond its complex 

714 conjugate, so the Hamiltonian stays Hermitian as long as 

715 *phase* is antisymmetric under swapping :math:`i` and 

716 :math:`j` -- true for the line integral of any vector 

717 potential, since reversing the integration path negates it. 

718 

719 Example usage:: 

720 

721 # Peierls phase from a uniform field via the Landau gauge 

722 # A = (0, B x): the same field as set_magnetic_field(alpha), 

723 # just in a different (equally valid) gauge. 

724 alpha = 0.01 

725 sys.set_peierls_phase(lambda xi, yi, xj, yj: 

726 2 * np.pi * alpha * (yj - yi) * (xi + xj) / 2) 

727 ''' 

728 error_handling.empty_hop(self.hop) 

729 error_handling.is_callable(phase, 'phase') 

730 xi = self.lat.coor['x'][self.hop['i']] 

731 yi = self.lat.coor['y'][self.hop['i']] 

732 d = self._bond_vectors(self.hop['i'].astype(int), self.hop['j'].astype(int)) 

733 xj, yj = xi + d[:, 0], yi + d[:, 1] 

734 self.hop['t'] = self.hop['t'] * np.exp(1j * phase(xi, yi, xj, yj)) 

735 

736 def set_magnetic_field(self, alpha: float) -> None: 

737 r''' 

738 Set a uniform perpendicular magnetic field via the Peierls 

739 substitution (see *set_peierls_phase*), using the symmetric gauge 

740 :math:`\mathbf{A} = \frac{B}{2}(-y, x)`: 

741 

742 .. math:: 

743 

744 \phi_{ij} = \pi\alpha\,(x_iy_j - x_jy_i) 

745 

746 :param alpha: Real number. Flux density :math:`B/\Phi_0`, in flux 

747 quanta per unit area (in the lattice's length units) -- *i.e.* 

748 the flux through a region of area :math:`S` is 

749 :math:`\alpha S` flux quanta. 

750 

751 Example usage:: 

752 

753 # one flux quantum per 100 unit cells of a lattice with 

754 # lattice constant 1: 

755 sys.set_magnetic_field(alpha=0.01) 

756 

757 Open boundaries only: on a torus the symmetric gauge is not 

758 periodic, and a uniform field needs a quantized total flux and 

759 magnetic boundary conditions. 

760 ''' 

761 error_handling.not_periodic(self.periodic, 'set_magnetic_field') 

762 error_handling.real_number(alpha, 'alpha') 

763 self.set_peierls_phase(lambda xi, yi, xj, yj: PI * alpha * (xi*yj - xj*yi)) 

764 

765 def set_onsite_dis(self, alpha: complex) -> None: 

766 ''' 

767 Set uniform onsite disorder.  

768 

769 :param alpha: Complex or Real number. Disorder strength. 

770 

771 Example usage:: 

772 

773 sys.set_onsite_dis(alpha=0.1) 

774 

775 ''' 

776 error_handling.empty_onsite(self.onsite) 

777 error_handling.number(alpha, 'alpha') 

778 self.onsite += alpha * rand.uniform(-1., 1., self.lat.sites) 

779 

780 def set_onsite_def(self, onsite_def: dict[int, complex]) -> None: 

781 ''' 

782 Set specific onsite energies. 

783 

784 :param onsite_def: Dictionary.  

785 key: site indices, val: onsite values. 

786 

787 Example usage:: 

788 

789 sys.set_onsite_def({0: 1., 1: -1j}) 

790 ''' 

791 error_handling.empty_onsite(self.onsite) 

792 error_handling.set_onsite_def(onsite_def, self.lat.sites) 

793 for i, o in onsite_def.items(): 

794 self.onsite[i] = o 

795 self._onsite_values = [(idx[~np.isin(idx, list(onsite_def))], f) for idx, f in self._onsite_values] 

796 

797 def set_hopping_def(self, hopping_def: dict[tuple[int, int], complex]) -> None: 

798 ''' 

799 Set specific hoppings.  

800 

801 :param hopping_def: Dictionary of hoppings. 

802 key: hopping indices :math:`(i, j)`, val: hopping value :math:`t_{ij}`. 

803 If *sys.hop* stores the bond the other way round, as :math:`(j, i)` 

804 (only one direction is stored for a Hermitian Hamiltonian), that 

805 entry is set to :math:`t_{ij}^*`, so that :math:`H_{ij} = t_{ij}`. 

806 A pair that is not a hopping of *sys.hop* is ignored. 

807 

808 Example usage:: 

809 

810 sys.set_hopping_def({(0, 1): 1., (1, 2): -1j}) 

811 ''' 

812 error_handling.empty_hop(self.hop) 

813 error_handling.set_hopping_def(self.hop, hopping_def, self.lat.sites) 

814 for key, val in hopping_def.items(): 

815 cond = (self.hop['i'] == key[0]) & (self.hop['j'] == key[1]) 

816 if cond.any(): 

817 self._assign_hopping(cond, val) 

818 continue 

819 cond = (self.hop['i'] == key[1]) & (self.hop['j'] == key[0]) 

820 self._assign_hopping(cond, np.conj(val)) 

821 

822 def set_new_hopping(self, list_hop: list[dict], ind: NDArray) -> None: 

823 ''' 

824 Private method. 

825 Set new hoppings. 

826 

827 :param list_hop: List of Dictionary (see set_hopping definition). 

828 :param ind: List. List of indices. 

829 ''' 

830 for dic in list_hop: 

831 if len(dic) == 2: 

832 self._assign_hopping(ind, dic['t']) 

833 elif len(dic) == 3 and 'ang' in dic: 

834 self._assign_hopping(ind & np.isclose(self.hop['ang'], dic['ang'], atol=ATOL), dic['t']) 

835 elif len(dic) == 3 and 'tag' in dic: 

836 self._assign_hopping(ind & (self.hop['tag'] == dic['tag']), dic['t']) 

837 else: 

838 self._assign_hopping(ind & (self.hop['tag'] == dic['tag']) 

839 & np.isclose(self.hop['ang'], dic['ang'], atol=ATOL), dic['t']) 

840 

841 def find_square(self, xlims: tuple[float, float], ylims: tuple[float, float]) -> NDArray: 

842 ''' 

843 Private method. 

844 Find hoppings within the square. 

845 

846 :param xlims: List or Tuple. :math:`x` interval. 

847 :param ylims: List or Tuple. :math:`y` interval. 

848 ''' 

849 error_handling.lims(xlims) 

850 error_handling.lims(ylims) 

851 in1 = (self.lat.coor['x'][self.hop['i']] >= xlims[0]) & \ 

852 (self.lat.coor['y'][self.hop['i']] >= ylims[0]) & \ 

853 (self.lat.coor['x'][self.hop['j']] >= xlims[0]) & \ 

854 (self.lat.coor['y'][self.hop['j']] >= ylims[0]) 

855 in2 = (self.lat.coor['x'][self.hop['i']] <= xlims[1]) & \ 

856 (self.lat.coor['y'][self.hop['i']] <= ylims[1]) & \ 

857 (self.lat.coor['x'][self.hop['j']] <= xlims[1]) & \ 

858 (self.lat.coor['y'][self.hop['j']] <= ylims[1]) 

859 return in1 * in2 

860 

861 def find_ellipse(self, rx: float, ry: float, x0: float, y0: float) -> NDArray: 

862 ''' 

863 Private method. 

864 Find hoppings within the ellipse. 

865 

866 :param rx: Positive Float. Radius along :math:`x`. 

867 :param ry: Positive Float. Radius along :math:`y`. 

868 :param x0: Float. Default value 0. :math:`x` center. 

869 :param y0: Float. Default value 0. :math:`y` center. 

870 ''' 

871 in1 = (self.lat.coor['x'][self.hop['i']] - x0) ** 2 / rx ** 2 + \ 

872 (self.lat.coor['y'][self.hop['i']] - y0) ** 2 / ry ** 2 <= 1. 

873 in2 = (self.lat.coor['x'][self.hop['j']] - x0) ** 2 / rx ** 2 + \ 

874 (self.lat.coor['y'][self.hop['j']] - y0) ** 2 / ry ** 2 <= 1. 

875 return in1 * in2 

876 

877 def change_hopping_square( 

878 self, list_hop: list[dict], xlims: tuple[float, float], ylims: tuple[float, float] = (-1., 1.), 

879 ) -> None: 

880 ''' 

881 Change the values of the hoppings whose two sites both lie in 

882 the rectangle *xlims* x *ylims*. 

883 

884 :param list_hop: List of Dictionary (see set_hopping definition). 

885 :param xlims: List or Tuple. :math:`x` interval. 

886 :param ylims: List or Tuple. Default value [-1, 1]. :math:`y` interval. 

887 ''' 

888 error_handling.empty_hop(self.hop) 

889 error_handling.set_hopping(list_hop, self.nmax) 

890 ind = self.find_square(xlims, ylims) 

891 self.set_new_hopping(list_hop, ind) 

892 

893 def change_hopping_ellipse( 

894 self, list_hop: list[dict], rx: float, ry: float, x0: float = 0., y0: float = 0., 

895 ) -> None: 

896 ''' 

897 Change the values of the hoppings whose two sites both lie in 

898 the ellipse of radii *rx*, *ry* centered at (*x0*, *y0*). 

899 

900 :param list_hop: List of Dictionary (see set_hopping definition). 

901 :param rx: Positive Float. Radius along :math:`x`.  

902 :param ry: Positive Float. Radius along :math:`y`. 

903 :param x0: Float. Default value 0. :math:`x` center.  

904 :param y0: Float. Default value 0. :math:`y` center. 

905 ''' 

906 error_handling.empty_hop(self.hop) 

907 error_handling.set_hopping(list_hop, self.nmax) 

908 error_handling.positive_real(rx, 'rx') 

909 error_handling.positive_real(ry, 'ry') 

910 error_handling.real_number(x0, 'x0') 

911 error_handling.real_number(y0, 'y0') 

912 ind = self.find_ellipse(rx, ry, x0, y0) 

913 self.set_new_hopping(list_hop, ind) 

914 

915 def get_coor_hop(self) -> None: 

916 ''' 

917 Get the site coordinates in hopping space 

918 only considering the nearest neighbours hoppings. 

919 ''' 

920 error_handling.empty_hop(self.hop) 

921 visited = np.zeros(self.lat.sites, 'u2') 

922 self.coor_hop = np.zeros(self.lat.sites, dtype=COOR_DTYPE) 

923 self.coor_hop['tag'] = self.lat.coor['tag'] 

924 hop = self.hop[self.hop['n'] == 1] 

925 hop_down = np.copy(hop) 

926 hop_down['i'] = hop['j'] 

927 hop_down['j'] = hop[ 'i'] 

928 hop_down['ang'] = -180 + hop['ang'] 

929 hop = np.concatenate([hop, hop_down]) 

930 i_visit = np.min(hop['i']) 

931 while True: 

932 hs = hop[hop['i'] == i_visit] 

933 for h in hs: 

934 if visited[h['j']] == 2: 

935 continue 

936 self.coor_hop['x'][h['j']] = self.coor_hop['x'][i_visit] + \ 

937 h['t'].real*cos(PI / 180 * h['ang']) 

938 self.coor_hop['y'][h['j']] = self.coor_hop['y'][i_visit] + \ 

939 h['t'].real*sin(PI / 180 * h['ang']) 

940 visited[h['j']] = 1 

941 visited[i_visit] = 2 

942 explored = np.argwhere(visited == 1) 

943 if not explored.any(): 

944 break 

945 i_visit = explored[0, 0] 

946 

947 def get_ham(self, **params) -> None: 

948 ''' 

949 Get the Tight-Binding Hamiltonian using sys.hop. 

950 

951 :param params: Values of the parameters of the value functions given 

952 to *set_hopping* / *set_onsite*: each function gets the ones its 

953 signature names (all of them with ``**kwargs``); the others are 

954 ignored. Only the values are recomputed: sweeping a parameter 

955 costs one call per point, not a new model. 

956 

957 Example usage:: 

958 

959 for B in np.linspace(0., 0.1, 50): 

960 sys.get_ham(B=B) 

961 sys.get_eig() 

962 ''' 

963 error_handling.empty_hop(self.hop) 

964 error_handling.hop_sites(self.hop, self.lat.sites) 

965 t = self._hop_t(params) 

966 if np.all(self.hop['ang'] >= 0) or np.all(self.hop['ang'] < 0): 

967 self.ham = sparse.csr_matrix((t, (self.hop['i'], self.hop['j'])), 

968 shape=(self.lat.sites, self.lat.sites)) \ 

969 + sparse.csr_matrix((t.conj(), (self.hop['j'], self.hop['i'])), 

970 shape=(self.lat.sites, self.lat.sites)) 

971 else: 

972 self.ham = sparse.csr_matrix((t, (self.hop['i'], self.hop['j'])), 

973 shape=(self.lat.sites, self.lat.sites)) 

974 if self.onsite.size == self.lat.sites: 

975 self.ham += sparse.diags(self._onsite_e(params), 0) 

976 

977 def get_eig(self, eigenvec: bool = False, left: bool = False) -> None: 

978 ''' 

979 Get the eigenergies, eigenvectors and polarisation. 

980 

981 :param eigenvec: Boolean. Default value False.  

982 If True, get the eigenvectors. 

983 :param left: Boolean. Default value False.  

984 If True, get the left eigenvectors too.  

985 Relevant for non-Hermitian matrices. 

986 ''' 

987 error_handling.empty_ham(self.ham) 

988 error_handling.boolean(eigenvec, 'eigenvec') 

989 error_handling.boolean(left, 'left') 

990 # Drop any left eigenvectors from a previous call: they belong to the 

991 # previous diagonalization and must not be re-sorted with this one's 

992 # ordering (get_petermann would then pair the wrong vectors). 

993 self.ln = np.array([], 'c16') 

994 if eigenvec: 

995 if (self.ham.conj().T != self.ham).nnz: 

996 if not left: 

997 self.en, self.rn = LA.eig(self.ham.toarray()) 

998 else: 

999 # scipy.linalg.eig returns (w, vl, vr) -- the LEFT 

1000 # eigenvectors come back before the right ones. 

1001 self.en, self.ln, self.rn = LA.eig(self.ham.toarray(), left=left) 

1002 ind = np.argsort(self.en.real) 

1003 self.en = self.en[ind] 

1004 self.rn = self.rn[:, ind] 

1005 if self.ln.size: 

1006 self.ln = self.ln[:, ind] 

1007 else: 

1008 self.en, self.rn = LA.eigh(self.ham.toarray()) 

1009 self._set_states() 

1010 else: 

1011 if (self.ham.conj().T != self.ham).nnz: 

1012 self.en = LA.eigvals(self.ham.toarray()) 

1013 ind = np.argsort(self.en.real) 

1014 self.en = self.en[ind] 

1015 else: 

1016 ham = self.ham.toarray() 

1017 # A real symmetric matrix (no fluxes, no spin-orbit) has the same 

1018 # eigenvalues from the real solver, about twice as fast. 

1019 if not ham.imag.any(): 

1020 ham = ham.real 

1021 self.en = LA.eigvalsh(ham) 

1022 

1023 def _set_states(self) -> None: 

1024 ''' 

1025 Private method. Intensities and sublattice polarizations of the 

1026 eigenvectors *rn*. 

1027 ''' 

1028 self.intensity = np.abs(self.rn) ** 2 

1029 self.pola = np.zeros((self.rn.shape[1], len(self.lat.tags))) 

1030 for i, tag in enumerate(self.lat.tags): 

1031 self.pola[:, i] = np.sum(self.intensity[self.lat.coor['tag'] == tag, :], axis=0) 

1032 

1033 def get_eig_sparse(self, n_eig: int = 10, sigma: complex = 0., eigenvec: bool = False) -> None: 

1034 r''' 

1035 Get the *n_eig* eigenenergies closest to *sigma*, and optionally 

1036 their eigenvectors, with the sparse shift-invert Lanczos (Hermitian) 

1037 or Arnoldi (non-Hermitian) solver of ARPACK. Unlike *get_eig*, it 

1038 never builds the dense matrix, so it reaches lattices of 

1039 :math:`10^5` sites and more -- e.g. the states near a band centre, or 

1040 the zero modes of a large flake. The results replace *en* (sorted by 

1041 real part) and, with *eigenvec*, *rn*, *intensity* and *pola*, as for 

1042 *get_eig*. 

1043 

1044 :param n_eig: Positive integer. Default value 10. Number of eigenpairs 

1045 (at most sites - 2). 

1046 :param sigma: Real or complex number. Default value 0. Target energy. 

1047 The factorization uses *sigma* shifted by about 1e-9 (relative to 

1048 the largest matrix element), so that it may be exactly an 

1049 eigenvalue, e.g. zero modes at the default *sigma* = 0; the 

1050 eigenvalues themselves are not shifted. 

1051 :param eigenvec: Boolean. Default value False. 

1052 ''' 

1053 error_handling.empty_ham(self.ham) 

1054 error_handling.n_eig(n_eig, self.lat.sites) 

1055 error_handling.number(sigma, 'sigma') 

1056 error_handling.boolean(eigenvec, 'eigenvec') 

1057 self.ln = np.array([], 'c16') 

1058 ham = self.ham.tocsc() 

1059 hermitian = not (self.ham.conj().T != self.ham).nnz 

1060 # Never factorize H - sigma at sigma itself: when sigma is exactly an 

1061 # eigenvalue, SuperLU usually raises, but with a two-fold one it 

1062 # passes a negative size to BLAS, and Apple's Accelerate then aborts 

1063 # the whole process (no exception to catch). 

1064 target = sigma + 1e-9 * (1. + abs(sigma) + abs(ham).max()) 

1065 if hermitian: 

1066 out = SLA.eigsh(ham, k=n_eig, sigma=float(np.real(target)), return_eigenvectors=eigenvec) 

1067 else: 

1068 out = SLA.eigs(ham, k=n_eig, sigma=target, return_eigenvectors=eigenvec) 

1069 en, vec = out if eigenvec else (out, None) 

1070 ind = np.argsort(en.real, kind='stable') 

1071 self.en = en[ind] 

1072 if vec is not None: 

1073 self.rn = vec[:, ind] 

1074 self._set_states() 

1075 

1076 def get_green(self, energy: complex, eta: float = 1e-6) -> NDArray[np.complex128]: 

1077 r''' 

1078 Get the retarded Green's function (resolvent) 

1079 

1080 .. math:: 

1081 

1082 G(E) = \left[(E + i\eta) - H\right]^{-1}\, . 

1083 

1084 :param energy: Real (or complex) number. Energy :math:`E`. 

1085 :param eta: Positive real or zero. Default value 1e-6. Broadening :math:`\eta`. 

1086 

1087 :returns: 

1088 * **G** -- Complex ndarray, shape (sites, sites). 

1089 ''' 

1090 error_handling.empty_ham(self.ham) 

1091 error_handling.number(energy, 'energy') 

1092 error_handling.positive_real_zero(eta, 'eta') 

1093 z = (energy + 1j * eta) * np.eye(self.ham.shape[0]) 

1094 return LA.inv(z - self.ham.toarray()) 

1095 

1096 def get_ldos(self, energies: ArrayLike, eta: float = 0.05) -> NDArray[np.float64]: 

1097 r''' 

1098 Get the local density of states 

1099 

1100 .. math:: 

1101 

1102 \rho_i(E) = -\frac{1}{\pi}\,\mathrm{Im}\,G_{ii}(E) 

1103 = \sum_n |\psi_n(i)|^2\,\frac{\eta/\pi}{(E - E_n)^2 + \eta^2} 

1104 

1105 (the second form, from the eigenvectors, for a Hermitian 

1106 Hamiltonian). Summed over sites it is the Lorentzian-broadened 

1107 density of states. 

1108 

1109 :param energies: Real array. Energies. 

1110 :param eta: Positive real. Default value 0.05. Broadening. 

1111 

1112 :returns: 

1113 * **ldos** -- Real ndarray, shape (len(energies), sites). 

1114 ''' 

1115 error_handling.empty_ham(self.ham) 

1116 error_handling.positive_real(eta, 'eta') 

1117 energies = np.atleast_1d(np.asarray(energies, dtype='f8')) 

1118 error_handling.ndarray_empty(energies, 'energies') 

1119 ham = self.ham.toarray() 

1120 if np.allclose(ham, ham.conj().T, atol=1e-14): 

1121 en, vec = LA.eigh(ham) 

1122 lor = (eta / PI) / ((energies[:, None] - en[None, :]) ** 2 + eta ** 2) 

1123 return lor @ (np.abs(vec) ** 2).T 

1124 eye = np.eye(len(ham)) 

1125 return np.array([-np.diag(LA.inv((e + 1j * eta) * eye - ham)).imag / PI 

1126 for e in energies]) 

1127 

1128 def get_fermi_level(self, n_electrons: float, temperature: float = 0.) -> float: 

1129 ''' 

1130 Get the Fermi level holding *n_electrons* electrons in the spectrum 

1131 *en* (one electron per state; see *tbkit.occupation.fermi_level*). 

1132 

1133 :param n_electrons: Positive real or zero. Number of electrons. 

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

1135 

1136 :returns: 

1137 * **e_fermi** -- Real number. 

1138 ''' 

1139 error_handling.empty_ndarray(self.en, 'sys.get_eig') 

1140 return occupation.fermi_level(self.en, n_electrons, temperature) 

1141 

1142 def get_occupations(self, e_fermi: float, temperature: float = 0.) -> NDArray[np.float64]: 

1143 ''' 

1144 Get the Fermi-Dirac occupation of every eigenstate of *en*. 

1145 

1146 :param e_fermi: Real number. Fermi level (see *get_fermi_level*). 

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

1148 

1149 :returns: 

1150 * **f** -- Real ndarray, same length as *en*. 

1151 ''' 

1152 error_handling.empty_ndarray(self.en, 'sys.get_eig') 

1153 return occupation.fermi_dirac(self.en, e_fermi, temperature) 

1154 

1155 def get_charge_density(self, e_fermi: float, temperature: float = 0.) -> NDArray[np.float64]: 

1156 r''' 

1157 Get the electron density on every site, 

1158 :math:`n_i = \sum_n f(E_n)\,|\psi_n(i)|^2`. 

1159 

1160 :param e_fermi: Real number. Fermi level. 

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

1162 

1163 :returns: 

1164 * **density** -- Real ndarray, shape (sites,). 

1165 ''' 

1166 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)') 

1167 return self.intensity @ self.get_occupations(e_fermi, temperature) 

1168 

1169 def get_total_energy(self, e_fermi: float, temperature: float = 0.) -> float: 

1170 r''' 

1171 Get the band energy :math:`E = \sum_n f(E_n)\,E_n`. 

1172 

1173 :param e_fermi: Real number. Fermi level. 

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

1175 

1176 :returns: 

1177 * **energy** -- Real number. 

1178 ''' 

1179 return float(np.sum(self.get_occupations(e_fermi, temperature) * self.en.real)) 

1180 

1181 def get_ipr(self) -> None: 

1182 r''' 

1183 Get the Inverse Participation Ratio:  

1184 

1185 .. math::  

1186 

1187 IPR_n = \sum_i|\psi_i^{n}|^4\, . 

1188 ''' 

1189 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)') 

1190 self.ipr = np.sum(self.intensity ** 2, axis=0) 

1191 

1192 def get_petermann(self) -> None: 

1193 r''' 

1194 Get the Petermann factor:  

1195  

1196 .. math:: 

1197 

1198 K_n = \frac{\langle\psi_L^{n}|\psi_L^{n}\rangle\langle\psi_R^{n}|\psi_R^{n}\rangle}{|\langle\psi_L^{n}|\psi_R^{n}\rangle|^2}\, . 

1199 

1200 .. note:: 

1201 

1202 LA.eig fixes the norm such that :math:`\langle\psi_L^{n}|\psi_L^{n}\rangle = 1` and :math:`\langle\psi_R^{n}|\psi_R^{n}\rangle = 1`. 

1203 ''' 

1204 if not (self.ham.conj().T != self.ham).nnz: 

1205 self.petermann = np.ones(self.lat.sites) 

1206 return 

1207 error_handling.empty_ndarray(self.ln, 'sys.get_eig(eigenvec=True, left=True)') 

1208 # LA.eig fixes each eigenvector's norm but not the relative phase 

1209 # between the left and right ones, so the overlap carries an 

1210 # arbitrary phase: take its modulus, not its real part. 

1211 left_right = np.abs(np.sum(self.ln * np.conjugate(self.rn), axis=0)) 

1212 self.petermann = 1. / left_right ** 2 

1213 

1214 def get_intensity_pola_max(self, tag_pola: str) -> NDArray[np.float64]: 

1215 ''' 

1216 Get the state with largest polarization on one sublattice. 

1217 

1218 :param tag_pola: One-character string. Sublattice tag. 

1219 

1220 :returns: 

1221 * **intensity** -- Intensity of max polarized state on *tag*. 

1222 ''' 

1223 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)') 

1224 error_handling.tag(tag_pola, self.lat.tags) 

1225 i_tag = self.lat.tags == tag_pola 

1226 ind = np.argmax(self.pola[:, i_tag]) 

1227 print('State with polarization: {:.5f}'.format(self.pola[ind, i_tag].item())) 

1228 return self.intensity[:, ind] 

1229 

1230 def get_intensity_pola_min(self, tag_pola: str) -> NDArray[np.float64]: 

1231 ''' 

1232 Get the state with smallest polarization on one sublattice. 

1233 

1234 :param tag_pola: One-character string. Sublattice tag. 

1235 

1236 :returns: 

1237 * **intensity** -- Intensity of min polarized state on *tag*. 

1238 ''' 

1239 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)') 

1240 error_handling.tag(tag_pola, self.lat.tags) 

1241 i_tag = self.lat.tags == tag_pola 

1242 ind = np.argmin(self.pola[:, i_tag]) 

1243 print('State with polarization: {:.5f}'.format(self.pola[ind, i_tag].item())) 

1244 return self.intensity[:, ind] 

1245 

1246 def get_intensity_en(self, lims: tuple[float, float]) -> NDArray[np.float64]: 

1247 ''' 

1248 Get, if any, the intensity of the sum of the states whose 

1249 energies (real part) lie between *lims[0]* and *lims[1]*. 

1250 

1251 :param lims: List. lims[0] energy min, lims[1] energy max. 

1252 

1253 :returns: 

1254 * **intensity** -- Sum of the intensities between (lims[0], lims[1]). 

1255 ''' 

1256 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)') 

1257 error_handling.lims(lims) 

1258 ind = np.ravel(np.where((self.en.real > lims[0]) & (self.en.real < lims[1]))) 

1259 print('{} states between {} and {}'.format(len(ind), lims[0], lims[1])) 

1260 return np.sum(self.intensity[:, ind], axis=1) 

1261 

1262 

1263 def get_local_chern_marker(self, e_fermi: float = 0., area: float | None = None) -> NDArray[np.float64]: 

1264 r''' 

1265 Get the local Chern marker of the states below *e_fermi* (Bianco and 

1266 Resta, Phys. Rev. B 84, 241106 (2011)), 

1267 

1268 .. math:: 

1269 

1270 C(\mathbf{r}_i) = -\frac{4\pi}{a}\,\mathrm{Im}\, 

1271 \langle i|\,P\,x\,Q\,y\,P\,|i\rangle\, , 

1272 

1273 with :math:`P` the projector on the occupied states, :math:`Q = 1-P`, 

1274 and :math:`a` the area per site. Averaged over the bulk of a finite 

1275 flake -- even a disordered or amorphous one, where no Brillouin zone 

1276 exists -- it gives the Chern number, in the sign convention of 

1277 *KSpace.chern_number*; it sums to zero over the whole flake, the 

1278 edges carrying the opposite value. 

1279 

1280 :param e_fermi: Real number. Default value 0. Fermi energy: the states 

1281 with energies below it are occupied. 

1282 :param area: Positive real number. Default value None: the area of 

1283 the unit cell of *lat* divided by its number of sites. 

1284 

1285 :returns: 

1286 * **marker** -- Real ndarray, shape (sites,). 

1287 ''' 

1288 error_handling.not_periodic(self.periodic, 'get_local_chern_marker') 

1289 error_handling.empty_ham(self.ham) 

1290 error_handling.real_number(e_fermi, 'e_fermi') 

1291 error_handling.hermitian(self.ham) 

1292 if area is None: 

1293 error_handling.planar_cell(self.lat.prim_vec) 

1294 (a1x, a1y), (a2x, a2y) = self.lat.prim_vec 

1295 area = abs(a1x * a2y - a1y * a2x) / len(self.lat.unit_cell) 

1296 error_handling.positive_real(area, 'area') 

1297 en, vec = LA.eigh(self.ham.toarray()) 

1298 occ = vec[:, en < e_fermi] 

1299 proj = occ @ occ.conj().T 

1300 rows = self._row_sites() 

1301 x = self.lat.coor['x'][rows] 

1302 y = self.lat.coor['y'][rows] 

1303 # P x Q y P = P x y P - P x P y P, diagonal only 

1304 pxq = proj * x[None, :] - (proj * x[None, :]) @ proj 

1305 qyp = y[:, None] * proj - proj @ (y[:, None] * proj) 

1306 per_row = -4 * PI / area * np.imag(np.sum(proj * (pxq @ qyp).T, axis=1)) 

1307 marker = np.zeros(self.lat.sites) 

1308 np.add.at(marker, rows, per_row) 

1309 return marker 

1310 

1311 def get_bott_index(self, e_fermi: float = 0.) -> float: 

1312 r''' 

1313 Get the Bott index of the states below *e_fermi* of a 2D lattice 

1314 with periodic boundaries along both primitive vectors (Loring and 

1315 Hastings, EPL 92, 67004 (2010); see *tbkit.topology.bott_index*): 

1316 the Chern number of a torus, in the sign convention of 

1317 *KSpace.chern_number*, and an integer for any disordered sample 

1318 whose occupied states are gapped or localized. 

1319 

1320 :param e_fermi: Real number. Default value 0. Fermi energy. 

1321 

1322 :returns: 

1323 * **bott** -- Real number, close to an integer. 

1324 

1325 Example usage:: 

1326 

1327 lat = lattices.honeycomb() 

1328 lat.get_lattice(12, 12) 

1329 sys = System(lat, periodic=True) 

1330 sys.set_hopping([{'n': 1, 't': 1.}]) 

1331 ... 

1332 sys.get_ham() 

1333 sys.get_bott_index() 

1334 ''' 

1335 from tbkit.topology import bott_index 

1336 error_handling.empty_ham(self.ham) 

1337 error_handling.torus_2d(self.periodic) 

1338 return bott_index(self.ham, self._coords()[self._row_sites()], self._box(), e_fermi) 

1339 

1340 def _row_sites(self) -> NDArray: 

1341 ''' 

1342 Private method. Site of every row of the Hamiltonian (one row per site here). 

1343 ''' 

1344 return np.arange(self.lat.sites) 

1345 

1346 

1347# Backward-compatible lowercase alias (pre-0.2 API). 

1348system = System