Coverage for tbkit/system.py: 100%

569 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-30 11:04 +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 Parameters 

45 ---------- 

46 lat : **lattice** class instance 

47 periodic : Boolean, or tuple of booleans (one per primitive vector), default False 

48 Periodic boundary conditions along the 

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

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

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

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

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

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

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

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

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

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

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

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

61 

62 Notes 

63 ----- 

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

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

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

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

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

69 ''' 

70 

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

72 dense_max = 5000 

73 

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

75 error_handling.lat(lat) 

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

77 if any(self.periodic): 

78 error_handling.torus_lattice(lat.n1) 

79 self.lat = lat 

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

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

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

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

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

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

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

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

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

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

90 self._hop_values: list = [] 

91 self._onsite_values: list = [] 

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

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

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

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

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

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

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

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

100 self.nmax = 0 # number of different hoppings 

101 

102 def clear_hopping(self) -> None: 

103 ''' 

104 Clear structured array *hop*. 

105 ''' 

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

107 self._hop_values = [] 

108 

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

110 ''' 

111 Private method. 

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

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

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

115 

116 Parameters 

117 ---------- 

118 n_orders : Positive integer, default 1 

119 Beyond *dense_max* 

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

121 ''' 

122 error_handling.sites(self.lat.sites) 

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

124 self._sparse_distances(n_orders) 

125 return 

126 self._pairs: NDArray | None = None 

127 if any(self.periodic): 

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

129 coords = self._coords() 

130 n = len(coords) 

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

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

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

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

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

136 else: 

137 dif = self.lat.distances() 

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

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

140 if len(dif) == 3: 

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

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

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

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

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

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

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

148 self.vec_hop['dis'] = dist 

149 self.vec_hop['ang'] = ang 

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

151 

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

153 ''' 

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

155 ''' 

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

157 

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

159 ''' 

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

161 ''' 

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

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

164 

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

166 ''' 

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

168 ''' 

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

170 

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

172 ''' 

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

174 under the periodic translations: the fractional coordinates along the 

175 periodic vectors are rounded, then the neighbouring images compared 

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

177 displacements longer than half the shortest period need that 

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

179 ''' 

180 if not any(self.periodic): 

181 return d 

182 torus = self._torus() 

183 shape = d.shape 

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

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

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

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

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

189 start = d[far] 

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

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

192 if any(m): 

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

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

195 shorter = cand_sq < best_sq - 1e-12 

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

197 d[far] = best 

198 return d.reshape(shape) 

199 

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

201 ''' 

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

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

204 ''' 

205 coords = self._coords() 

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

207 

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

209 ''' 

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

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

212 ''' 

213 coords = self._coords() 

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

215 

216 def _shortest_period(self) -> float: 

217 ''' 

218 Private method. Length of the shortest nonzero periodic translation 

219 (infinite with open boundaries). 

220 ''' 

221 if not any(self.periodic): 

222 return np.inf 

223 torus = self._torus() 

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

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

226 

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

228 ''' 

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

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

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

232 ''' 

233 if not any(self.periodic): 

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

235 torus = self._torus() 

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

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

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

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

240 keep = i < j 

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

242 

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

244 ''' 

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

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

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

248 being found. 

249 ''' 

250 coords = self._coords() 

251 tree = cKDTree(coords) 

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

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

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

255 while True: 

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

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

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

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

260 break 

261 radius *= 1.5 

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

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

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

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

266 

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

268 ''' 

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

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

271 ''' 

272 d = self._bond_vectors(i, j) 

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

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

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

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

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

278 return ang 

279 

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

281 r''' 

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

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

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

285 

286 Parameters 

287 ---------- 

288 n : Positive integer 

289 Number of shortest edges. 

290 ''' 

291 error_handling.sites(self.lat.sites) 

292 error_handling.positive_int(n, 'n') 

293 self.get_distances(n) 

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

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

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

297 print('\nDistances between sites:') 

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

299 if i == 0: 

300 hop_name = 'st' 

301 elif i == 1: 

302 hop_name = 'nd' 

303 elif i == 2: 

304 hop_name = 'rd' 

305 else: 

306 hop_name = 'th' 

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

308 print('\twith positive angles:') 

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

310 

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

312 ''' 

313 Set onsite energies. 

314 

315 Parameters 

316 ---------- 

317 dict_onsite : Dictionary 

318 key: sublattice tag, val: onsite 

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

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

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

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

323 adds to it. 

324 

325 Examples 

326 -------- 

327 Example usage:: 

328 

329 # Line-Centered Square lattice 

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

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

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

333 sys.get_ham(V=0.5) 

334 ''' 

335 error_handling.sites(self.lat.sites) 

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

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

338 self._onsite_values = [] 

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

340 if callable(on): 

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

342 else: 

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

344 

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

346 ''' 

347 Private method. 

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

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

350 

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

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

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

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

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

356 

357 Returns 

358 ------- 

359 i, j : Integer ndarrays 

360 Bond end points. 

361 ang : Real ndarray 

362 Bond angles, in degrees, in :math:`[0, 180)`. 

363 ''' 

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

365 if self._pairs is not None: 

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

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

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

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

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

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

372 i, j = i[order], j[order] 

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

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

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

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

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

378 

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

380 ''' 

381 Private method. 

382 

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

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

385 ''' 

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

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

388 hop['i'] = i 

389 hop['j'] = j 

390 hop['ang'] = ang 

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

392 self.store_hop[n] = hop 

393 

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

395 r''' 

396 Set lattice hoppings. 

397 

398 Parameters 

399 ---------- 

400 list_hop : List of Dictionaries 

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

402 

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

404 

405 * 'n': 1 for nearest neighbours. 

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

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

408 * etc... 

409 

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

411 

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

413 

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

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

416 

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

418 

419 Hopping tags. 

420 

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

422 

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

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

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

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

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

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

429 multiply it. 

430 upper_part : Boolean, default True 

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

432 the Hamiltonian upper part. 

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

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

435 

436 Examples 

437 -------- 

438 Example usage:: 

439 

440 # fill upper part: 

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

442 # fill lower part: 

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

444 # fill upper part: specifying the angles: 

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

446 # fill lower part: 

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

448 upper_part=False) 

449 # fill upper part: specifying the tags: 

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

451 # fill lower part: 

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

453 upper_part=False) 

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

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

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

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

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

459 # fill lower part: 

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

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

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

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

464 upper_part=False) 

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

466 # rebuilding the model 

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

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

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

470 

471 .. note:: 

472 

473 A Hermitian hopping matrix can be built using only 

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

475 built automatically by Hermitian conjugation. 

476 

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

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

479 hopping matrix can be built. 

480 ''' 

481 error_handling.sites(self.lat.sites) 

482 error_handling.boolean(upper_part, 'upper_part') 

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

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

485 error_handling.set_hopping(list_hop, self.nmax) 

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

487 # fill, if needed self.store_hop 

488 self.check_sites() 

489 for n in list_n: 

490 if n not in self.store_hop: 

491 self.fill_store_hop(n) 

492 # fill self.hop 

493 for dic in list_hop: 

494 if len(dic) == 2: 

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

496 if upper_part: 

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

498 else: 

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

500 if np.sum(mask): 

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

502 ind = np.ones(size, bool) 

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

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

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

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

507 ang_store = dic['ang'] 

508 else: 

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

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

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

512 if np.sum(mask): 

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

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

515 error_handling.index(ind, dic) 

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

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

518 if upper_part: 

519 tag_store = dic['tag'] 

520 else: 

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

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

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

524 if upper_part: 

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

526 else: 

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

528 if np.sum(mask): 

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

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

531 error_handling.index(ind, dic) 

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

533 else: 

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

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

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

537 ang_store = dic['ang'] 

538 else: 

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

540 if upper_part: 

541 tag_store = dic['tag'] 

542 else: 

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

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

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

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

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

548 mask = bool1 & bool2 

549 if np.sum(mask): 

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

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

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

553 error_handling.index(ind, dic) 

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

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

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

557 

558 def check_sites(self) -> None: 

559 ''' 

560 Private method. 

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

562 method system.set_hopping(). 

563 ''' 

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

565 self.store_hop = {} 

566 self.sites = self.lat.sites 

567 

568 def set_given_hopping( 

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

570 ) -> NDArray: 

571 ''' 

572 Private method. 

573 Fill self.hop.  

574 

575 Parameters 

576 ---------- 

577 n : Integer 

578 Hopping type. 

579 size : Integer 

580 Number of hoppings. 

581 dic : Dictionary 

582 Hopping dictionary. 

583 mask : np.ndarray 

584 Mask. 

585 upper_part : Boolean 

586 If True, keep the orientation of *store_hop* 

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

588 ''' 

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

590 hop['n'] = dic['n'] 

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

592 if upper_part: 

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

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

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

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

597 else: 

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

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

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

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

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

603 return hop 

604 

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

606 ''' 

607 Set hoppings manually. 

608 

609 Parameters 

610 ---------- 

611 dict_hop : Dictionary of hoppings 

612 key: hopping indices, val: hopping values. 

613 upper_part : Boolean, default True 

614 * True, fill the Hamiltonian upper part. 

615 * False, fill the Hamiltonian lower part. 

616 

617 Examples 

618 -------- 

619 Example usage:: 

620 

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

622 ''' 

623 error_handling.sites(self.lat.sites) 

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

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

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

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

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

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

630 hop['t'] = t 

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

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

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

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

635 if upper_part: 

636 ang[ang < 0] += 180 

637 else: 

638 ang[ang >= 0] -= 180 

639 hop['ang'] = ang 

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

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

642 

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

644 ''' 

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

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

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

648 ''' 

649 keys = self._bond_keys(rows) 

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

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

652 if callable(t): 

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

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

655 else: 

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

657 

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

659 ''' 

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

661 ''' 

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

663 

664 def _sites(self) -> NDArray: 

665 ''' 

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

667 ''' 

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

669 

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

671 ''' 

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

673 ''' 

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

675 if not self._hop_values: 

676 return t 

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

678 for k, func in self._hop_values: 

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

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

681 params, len(rows)) 

682 return t 

683 

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

685 ''' 

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

687 ''' 

688 onsite = self.onsite.copy() 

689 if self._onsite_values: 

690 sites = self._sites() 

691 for idx, func in self._onsite_values: 

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

693 return onsite 

694 

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

696 ''' 

697 Set uniform hopping disorder.  

698 

699 Parameters 

700 ---------- 

701 alpha : Complex or Real number 

702 Disorder strength. 

703 

704 Examples 

705 -------- 

706 Example usage:: 

707 

708 sys.set_hopping_dis(alpha=0.1) 

709 ''' 

710 error_handling.empty_hop(self.hop) 

711 error_handling.number(alpha, 'alpha') 

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

713 

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

715 r''' 

716 Apply the Peierls substitution to the existing hoppings, to 

717 capture the effect of an orbital magnetic field: 

718 

719 .. math:: 

720 

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

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

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

724 

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

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

727 

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

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

730 For a uniform perpendicular field, use the convenience method 

731 *set_magnetic_field* instead. 

732 

733 Parameters 

734 ---------- 

735 phase : Callable 

736 ``phase(xi, yi, xj, yj)`` returns 

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

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

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

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

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

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

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

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

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

746 

747 Notes 

748 ----- 

749 .. note:: 

750 

751 *get_ham* automatically assigns the reversed bond its complex 

752 conjugate, so the Hamiltonian stays Hermitian as long as 

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

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

755 potential, since reversing the integration path negates it. 

756 

757 Examples 

758 -------- 

759 Example usage:: 

760 

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

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

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

764 alpha = 0.01 

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

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

767 ''' 

768 error_handling.empty_hop(self.hop) 

769 error_handling.is_callable(phase, 'phase') 

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

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

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

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

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

775 

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

777 r''' 

778 Set a uniform perpendicular magnetic field via the Peierls 

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

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

781 

782 .. math:: 

783 

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

785 

786 Parameters 

787 ---------- 

788 alpha : Real number 

789 Flux density :math:`B/\Phi_0`, in flux 

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

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

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

793 

794 Examples 

795 -------- 

796 Example usage:: 

797 

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

799 # lattice constant 1: 

800 sys.set_magnetic_field(alpha=0.01) 

801 

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

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

804 magnetic boundary conditions. 

805 ''' 

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

807 error_handling.real_number(alpha, 'alpha') 

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

809 

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

811 ''' 

812 Set uniform onsite disorder.  

813 

814 Parameters 

815 ---------- 

816 alpha : Complex or Real number 

817 Disorder strength. 

818 

819 Examples 

820 -------- 

821 Example usage:: 

822 

823 sys.set_onsite_dis(alpha=0.1) 

824 ''' 

825 error_handling.empty_onsite(self.onsite) 

826 error_handling.number(alpha, 'alpha') 

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

828 

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

830 ''' 

831 Set specific onsite energies. 

832 

833 Parameters 

834 ---------- 

835 onsite_def : Dictionary 

836 key: site indices, val: onsite values. 

837 

838 Examples 

839 -------- 

840 Example usage:: 

841 

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

843 ''' 

844 error_handling.empty_onsite(self.onsite) 

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

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

847 self.onsite[i] = o 

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

849 

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

851 ''' 

852 Set specific hoppings.  

853 

854 Parameters 

855 ---------- 

856 hopping_def : Dictionary of hoppings 

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

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

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

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

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

862 

863 Examples 

864 -------- 

865 Example usage:: 

866 

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

868 ''' 

869 error_handling.empty_hop(self.hop) 

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

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

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

873 if cond.any(): 

874 self._assign_hopping(cond, val) 

875 continue 

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

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

878 

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

880 ''' 

881 Private method. 

882 Set new hoppings. 

883 

884 Parameters 

885 ---------- 

886 list_hop : List of Dictionary (see set_hopping definition) 

887 ind : List 

888 List of indices. 

889 ''' 

890 for dic in list_hop: 

891 if len(dic) == 2: 

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

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

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

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

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

897 else: 

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

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

900 

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

902 ''' 

903 Private method. 

904 Find hoppings within the square. 

905 

906 Parameters 

907 ---------- 

908 xlims : List or Tuple 

909 :math:`x` interval. 

910 ylims : List or Tuple 

911 :math:`y` interval. 

912 ''' 

913 error_handling.lims(xlims) 

914 error_handling.lims(ylims) 

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

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

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

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

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

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

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

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

923 return in1 * in2 

924 

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

926 ''' 

927 Private method. 

928 Find hoppings within the ellipse. 

929 

930 Parameters 

931 ---------- 

932 rx : Positive Float 

933 Radius along :math:`x`. 

934 ry : Positive Float 

935 Radius along :math:`y`. 

936 x0 : Float, default 0 

937 :math:`x` center. 

938 y0 : Float, default 0 

939 :math:`y` center. 

940 ''' 

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

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

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

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

945 return in1 * in2 

946 

947 def change_hopping_square( 

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

949 ) -> None: 

950 ''' 

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

952 the rectangle *xlims* x *ylims*. 

953 

954 Parameters 

955 ---------- 

956 list_hop : List of Dictionary (see set_hopping definition) 

957 xlims : List or Tuple 

958 :math:`x` interval. 

959 ylims : List or Tuple, default [-1, 1] 

960 :math:`y` interval. 

961 ''' 

962 error_handling.empty_hop(self.hop) 

963 error_handling.set_hopping(list_hop, self.nmax) 

964 ind = self.find_square(xlims, ylims) 

965 self.set_new_hopping(list_hop, ind) 

966 

967 def change_hopping_ellipse( 

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

969 ) -> None: 

970 ''' 

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

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

973 

974 Parameters 

975 ---------- 

976 list_hop : List of Dictionary (see set_hopping definition) 

977 rx : Positive Float 

978 Radius along :math:`x`. 

979 ry : Positive Float 

980 Radius along :math:`y`. 

981 x0 : Float, default 0 

982 :math:`x` center. 

983 y0 : Float, default 0 

984 :math:`y` center. 

985 ''' 

986 error_handling.empty_hop(self.hop) 

987 error_handling.set_hopping(list_hop, self.nmax) 

988 error_handling.positive_real(rx, 'rx') 

989 error_handling.positive_real(ry, 'ry') 

990 error_handling.real_number(x0, 'x0') 

991 error_handling.real_number(y0, 'y0') 

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

993 self.set_new_hopping(list_hop, ind) 

994 

995 def get_coor_hop(self) -> None: 

996 ''' 

997 Get the site coordinates in hopping space 

998 only considering the nearest neighbours hoppings. 

999 ''' 

1000 error_handling.empty_hop(self.hop) 

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

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

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

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

1005 hop_down = np.copy(hop) 

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

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

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

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

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

1011 while True: 

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

1013 for h in hs: 

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

1015 continue 

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

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

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

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

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

1021 visited[i_visit] = 2 

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

1023 if not explored.any(): 

1024 break 

1025 i_visit = explored[0, 0] 

1026 

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

1028 ''' 

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

1030 

1031 Parameters 

1032 ---------- 

1033 params 

1034 Values of the parameters of the value functions given 

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

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

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

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

1039 

1040 Examples 

1041 -------- 

1042 Example usage:: 

1043 

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

1045 sys.get_ham(B=B) 

1046 sys.get_eig() 

1047 ''' 

1048 error_handling.empty_hop(self.hop) 

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

1050 t = self._hop_t(params) 

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

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

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

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

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

1056 else: 

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

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

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

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

1061 

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

1063 ''' 

1064 Get the eigenergies, eigenvectors and polarisation. 

1065 

1066 Parameters 

1067 ---------- 

1068 eigenvec : Boolean, default False 

1069 If True, get the eigenvectors. 

1070 left : Boolean, default False 

1071 If True, get the left eigenvectors too.  

1072 Relevant for non-Hermitian matrices. 

1073 ''' 

1074 error_handling.empty_ham(self.ham) 

1075 error_handling.boolean(eigenvec, 'eigenvec') 

1076 error_handling.boolean(left, 'left') 

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

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

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

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

1081 if eigenvec: 

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

1083 if not left: 

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

1085 else: 

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

1087 # eigenvectors come back before the right ones. 

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

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

1090 self.en = self.en[ind] 

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

1092 if self.ln.size: 

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

1094 else: 

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

1096 self._set_states() 

1097 else: 

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

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

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

1101 self.en = self.en[ind] 

1102 else: 

1103 ham = self.ham.toarray() 

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

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

1106 if not ham.imag.any(): 

1107 ham = ham.real 

1108 self.en = LA.eigvalsh(ham) 

1109 

1110 def _set_states(self) -> None: 

1111 ''' 

1112 Private method. Intensities and sublattice polarizations of the 

1113 eigenvectors *rn*. 

1114 ''' 

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

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

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

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

1119 

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

1121 r''' 

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

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

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

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

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

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

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

1129 *get_eig*. 

1130 

1131 Parameters 

1132 ---------- 

1133 n_eig : Positive integer, default 10 

1134 Number of eigenpairs 

1135 (at most sites - 2). 

1136 sigma : Real or complex number, default 0 

1137 Target energy. 

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

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

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

1141 eigenvalues themselves are not shifted. 

1142 eigenvec : Boolean, default False 

1143 ''' 

1144 error_handling.empty_ham(self.ham) 

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

1146 error_handling.number(sigma, 'sigma') 

1147 error_handling.boolean(eigenvec, 'eigenvec') 

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

1149 ham = self.ham.tocsc() 

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

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

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

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

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

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

1156 if hermitian: 

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

1158 else: 

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

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

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

1162 self.en = en[ind] 

1163 if vec is not None: 

1164 self.rn = vec[:, ind] 

1165 self._set_states() 

1166 

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

1168 r''' 

1169 Get the retarded Green's function (resolvent) 

1170 

1171 .. math:: 

1172 

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

1174 

1175 Parameters 

1176 ---------- 

1177 energy : Real (or complex) number 

1178 Energy :math:`E`. 

1179 eta : Positive real or zero, default 1e-6 

1180 Broadening :math:`\eta`. 

1181 

1182 Returns 

1183 ------- 

1184 G : Complex ndarray, shape (sites, sites) 

1185 ''' 

1186 error_handling.empty_ham(self.ham) 

1187 error_handling.number(energy, 'energy') 

1188 error_handling.positive_real_zero(eta, 'eta') 

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

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

1191 

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

1193 r''' 

1194 Get the local density of states 

1195 

1196 .. math:: 

1197 

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

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

1200 

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

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

1203 density of states. 

1204 

1205 Parameters 

1206 ---------- 

1207 energies : Real array 

1208 Energies. 

1209 eta : Positive real, default 0.05 

1210 Broadening. 

1211 

1212 Returns 

1213 ------- 

1214 ldos : Real ndarray, shape (len(energies), sites) 

1215 ''' 

1216 error_handling.empty_ham(self.ham) 

1217 error_handling.positive_real(eta, 'eta') 

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

1219 error_handling.ndarray_empty(energies, 'energies') 

1220 ham = self.ham.toarray() 

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

1222 en, vec = LA.eigh(ham) 

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

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

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

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

1227 for e in energies]) 

1228 

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

1230 ''' 

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

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

1233 

1234 Parameters 

1235 ---------- 

1236 n_electrons : Positive real or zero 

1237 Number of electrons. 

1238 temperature : Positive real or zero, default 0 

1239 

1240 Returns 

1241 ------- 

1242 e_fermi : Real number 

1243 ''' 

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

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

1246 

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

1248 ''' 

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

1250 

1251 Parameters 

1252 ---------- 

1253 e_fermi : Real number 

1254 Fermi level (see *get_fermi_level*). 

1255 temperature : Positive real or zero, default 0 

1256 

1257 Returns 

1258 ------- 

1259 f : Real ndarray, same length as *en* 

1260 ''' 

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

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

1263 

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

1265 r''' 

1266 Get the electron density on every site, 

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

1268 

1269 Parameters 

1270 ---------- 

1271 e_fermi : Real number 

1272 Fermi level. 

1273 temperature : Positive real or zero, default 0 

1274 

1275 Returns 

1276 ------- 

1277 density : Real ndarray, shape (sites,) 

1278 ''' 

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

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

1281 

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

1283 r''' 

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

1285 

1286 Parameters 

1287 ---------- 

1288 e_fermi : Real number 

1289 Fermi level. 

1290 temperature : Positive real or zero, default 0 

1291 

1292 Returns 

1293 ------- 

1294 energy : Real number 

1295 ''' 

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

1297 

1298 def get_ipr(self) -> None: 

1299 r''' 

1300 Get the Inverse Participation Ratio:  

1301 

1302 .. math::  

1303 

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

1305 ''' 

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

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

1308 

1309 def get_petermann(self) -> None: 

1310 r''' 

1311 Get the Petermann factor:  

1312  

1313 .. math:: 

1314 

1315 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}\, . 

1316 

1317 .. note:: 

1318 

1319 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`. 

1320 ''' 

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

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

1323 return 

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

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

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

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

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

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

1330 

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

1332 ''' 

1333 Get the state with largest polarization on one sublattice. 

1334 

1335 Parameters 

1336 ---------- 

1337 tag_pola : One-character string 

1338 Sublattice tag. 

1339 

1340 Returns 

1341 ------- 

1342 intensity 

1343 Intensity of max polarized state on *tag*. 

1344 ''' 

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

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

1347 i_tag = self.lat.tags == tag_pola 

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

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

1350 return self.intensity[:, ind] 

1351 

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

1353 ''' 

1354 Get the state with smallest polarization on one sublattice. 

1355 

1356 Parameters 

1357 ---------- 

1358 tag_pola : One-character string 

1359 Sublattice tag. 

1360 

1361 Returns 

1362 ------- 

1363 intensity 

1364 Intensity of min polarized state on *tag*. 

1365 ''' 

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

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

1368 i_tag = self.lat.tags == tag_pola 

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

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

1371 return self.intensity[:, ind] 

1372 

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

1374 ''' 

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

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

1377 

1378 Parameters 

1379 ---------- 

1380 lims : List 

1381 lims[0] energy min, lims[1] energy max. 

1382 

1383 Returns 

1384 ------- 

1385 intensity 

1386 Sum of the intensities between (lims[0], lims[1]). 

1387 ''' 

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

1389 error_handling.lims(lims) 

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

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

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

1393 

1394 

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

1396 r''' 

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

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

1399 

1400 .. math:: 

1401 

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

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

1404 

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

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

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

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

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

1410 edges carrying the opposite value. 

1411 

1412 Parameters 

1413 ---------- 

1414 e_fermi : Real number, default 0 

1415 Fermi energy: the states 

1416 with energies below it are occupied. 

1417 area : Positive real number 

1418 Default value None: the area of 

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

1420 

1421 Returns 

1422 ------- 

1423 marker : Real ndarray, shape (sites,) 

1424 ''' 

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

1426 error_handling.empty_ham(self.ham) 

1427 error_handling.real_number(e_fermi, 'e_fermi') 

1428 error_handling.hermitian(self.ham) 

1429 if area is None: 

1430 error_handling.planar_cell(self.lat.prim_vec) 

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

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

1433 error_handling.positive_real(area, 'area') 

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

1435 occ = vec[:, en < e_fermi] 

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

1437 rows = self._row_sites() 

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

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

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

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

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

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

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

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

1446 return marker 

1447 

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

1449 r''' 

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

1451 with periodic boundaries along both primitive vectors (Loring and 

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

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

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

1455 whose occupied states are gapped or localized. 

1456 

1457 Parameters 

1458 ---------- 

1459 e_fermi : Real number, default 0 

1460 Fermi energy. 

1461 

1462 Returns 

1463 ------- 

1464 bott : Real number, close to an integer 

1465 

1466 Examples 

1467 -------- 

1468 Example usage:: 

1469 

1470 lat = lattices.honeycomb() 

1471 lat.get_lattice(12, 12) 

1472 sys = System(lat, periodic=True) 

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

1474 ... 

1475 sys.get_ham() 

1476 sys.get_bott_index() 

1477 ''' 

1478 from tbkit.topology import bott_index 

1479 error_handling.empty_ham(self.ham) 

1480 error_handling.torus_2d(self.periodic) 

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

1482 

1483 def _row_sites(self) -> NDArray: 

1484 ''' 

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

1486 ''' 

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

1488 

1489 

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

1491system = System