Coverage for tbkit/bridges.py: 100%

122 statements  

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

1r''' 

2Bridges between the two pipelines of tbkit: the real-space *System* (a 

3finite lattice, its sparse Hamiltonian) and the reciprocal-space *KSpace* 

4(the Bloch Hamiltonian :math:`H(\mathbf{k})` of the infinite lattice). 

5 

6* *kspace_from_system* reads the Bloch model off a translation-invariant 

7 *System* built the real-space way (*get_lattice*, *set_hopping* by 

8 neighbour order, *set_onsite*, ...). 

9* *finite_model* and *finite_system* cut a finite sample (open boundaries, 

10 or a torus) out of a *KSpace* model: its sparse Hamiltonian with the site 

11 positions, or a ready-to-use *System*. 

12 

13Both use the convention of *KSpace.finite_ham*: the hopping 

14:math:`t_{ij}(\mathbf{R}) = \langle i,\mathbf{0}|H|j,\mathbf{R}\rangle` 

15joins orbital *i* of every cell to orbital *j* of the cell :math:`\mathbf{R}` 

16further. So, for the same model, the spectrum of a System on an 

17:math:`N_1\times N_2` torus is the set of KSpace bands on the 

18:math:`N_1\times N_2` Brillouin-zone mesh. 

19''' 

20from __future__ import annotations 

21 

22import numpy as np 

23from numpy.typing import NDArray 

24import scipy.sparse as sp 

25 

26import tbkit.error_handling as error_handling 

27import tbkit.neighbours as neighbours 

28from tbkit.kspace import KSpace 

29from tbkit.lattice import Lattice 

30from tbkit.orbital import OrbitalSystem 

31from tbkit.system import System, HOP_DTYPE, ATOL, _upper 

32 

33 

34def _coords(lat: Lattice) -> NDArray[np.float64]: 

35 ''' 

36 Private function. Site coordinates of a finite lattice, shape (sites, space_dim). 

37 ''' 

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

39 

40 

41def cell_orbitals(lat: Lattice) -> tuple[NDArray[np.int64], NDArray[np.int64]]: 

42 r''' 

43 Get, for every site of a finite lattice (*lat.coor*), the unit-cell 

44 orbital it is a copy of and its cell: site *s* sits at 

45 :math:`\boldsymbol\tau_{o_s} + \sum_k n_{s,k}\mathbf{a}_k`, with the tag 

46 of orbital :math:`o_s` (within ``ATOL``). 

47 

48 Parameters 

49 ---------- 

50 lat 

51 **Lattice** instance, after *get_lattice* (sites may have 

52 been removed, but not moved). 

53 

54 Returns 

55 ------- 

56 cells : Integer ndarray, shape (sites, dim) 

57 :math:`n_{s,k}`. 

58 orbitals : Integer ndarray, shape (sites,) 

59 :math:`o_s`. 

60 ''' 

61 error_handling.lat(lat) 

62 error_handling.empty_coor(lat.coor) 

63 a, tau, tags = neighbours._geometry(lat) 

64 pos = _coords(lat) 

65 pinv = np.linalg.pinv(a) 

66 match = np.zeros((lat.sites, len(tau)), bool) 

67 cells = np.zeros((lat.sites, len(tau), len(a)), int) 

68 for o in range(len(tau)): 

69 rel = pos - tau[o] 

70 n = np.rint(rel @ pinv).astype(int) 

71 match[:, o] = (np.linalg.norm(n @ a - rel, axis=1) < ATOL) & (lat.coor['tag'] == tags[o]) 

72 cells[:, o] = n 

73 error_handling.lattice_sites(match.sum(axis=1)) 

74 orb = np.argmax(match, axis=1) 

75 return cells[np.arange(lat.sites), orb], orb 

76 

77 

78def kspace_from_system(sys: System, periodic: bool | None = None, tol: float = 1e-9) -> KSpace: 

79 r''' 

80 Get the Bloch Hamiltonian of a periodic model defined the real-space 

81 way: every matrix element :math:`H_{ab}` of the finite *System* (built 

82 by *get_ham*) joins orbital :math:`o_a` of cell :math:`\mathbf{n}_a` to 

83 orbital :math:`o_b` of cell :math:`\mathbf{n}_b` (see *cell_orbitals*), 

84 i.e. is the hopping :math:`t_{o_ao_b}(\mathbf{n}_b-\mathbf{n}_a)` of the 

85 infinite lattice. Each such hopping must take the same value in every 

86 cell where it appears (bonds cut by the boundary are simply missing). 

87 The lattice must be large enough to hold every bond of the model. 

88 

89 A System on a torus (*periodic*, e.g. from *finite_system*) has bonds 

90 that wrap around: their lattice vectors are then taken modulo the 

91 :math:`N_1\times N_2\times\dots` cells of *get_lattice*, as the shortest 

92 image (so the model's range must stay below half the torus). 

93 

94 A Hermitian hopping matrix gives a Hermitian model (one representative 

95 per bond, *set_hopping* adding the conjugate); otherwise every matrix 

96 element is kept, with ``hermitian=False``. Onsite energies are taken 

97 orbital by orbital. 

98 

99 Parameters 

100 ---------- 

101 sys : **System** instance with its hoppings set (one orbital per site) 

102 periodic : Boolean 

103 Default value None: True if *sys* has 

104 periodic boundaries along some primitive vector (``System(lat, 

105 periodic=...)``, or *finite_system* with ``periodic=True``). The 

106 System is a torus of the *lat.n1* x *lat.n2* (x *lat.n3*) cells of 

107 *get_lattice*. 

108 tol : Positive real number, default 1e-9 

109 Largest allowed 

110 difference between equivalent matrix elements. 

111 

112 Returns 

113 ------- 

114 ks 

115 **KSpace** instance (spinless), on a new **Lattice** 

116 with the *unit_cell* and *prim_vec* of *sys.lat*. 

117 

118 Examples 

119 -------- 

120 Example usage:: 

121 

122 lat = lattices.honeycomb() 

123 lat.get_lattice(6, 6) 

124 sys = System(lat) 

125 sys.set_hopping([{'n': 1, 't': -1.}, {'n': 2, 't': 0.1}]) 

126 gra = kspace_from_system(sys) # the Bloch model of the same graphene 

127 ''' 

128 error_handling.sys(sys) 

129 error_handling.not_orbital_system(sys, OrbitalSystem) 

130 if periodic is None: 

131 periodic = any(sys.periodic) 

132 error_handling.boolean(periodic, 'periodic') 

133 error_handling.positive_real(tol, 'tol') 

134 sys.get_ham() 

135 cells, orb = cell_orbitals(sys.lat) 

136 ham = sys.ham.tocoo() 

137 rows, cols, vals = ham.row, ham.col, ham.data.astype('c16') 

138 R = cells[cols] - cells[rows] 

139 if periodic: 

140 sizes = np.array((sys.lat.n1, sys.lat.n2, sys.lat.n3)[:R.shape[1]]) 

141 R = (R + sizes // 2) % sizes - sizes // 2 # shortest image, in [-N/2, N/2) 

142 error_handling.torus_range(R, sizes) 

143 keys = np.column_stack([orb[rows], orb[cols], R]) 

144 uniq, first, inv = np.unique(keys, axis=0, return_index=True, return_inverse=True) 

145 inv = inv.ravel() 

146 values = vals[first] 

147 error_handling.translation_invariant(float(np.abs(vals - values[inv]).max()), tol) 

148 off = ham - sp.diags(ham.diagonal()) 

149 hermitian = bool(abs(off - off.conj().T).max() <= tol) 

150 lat = Lattice(unit_cell=list(sys.lat.unit_cell), prim_vec=list(sys.lat.prim_vec)) 

151 ks = KSpace(lat) 

152 list_hop = [] 

153 for key, t in zip(uniq, values): 

154 i, j, R = int(key[0]), int(key[1]), tuple(int(n) for n in key[2:]) 

155 if i == j and not any(R): 

156 ks.onsite[i] = t 

157 continue 

158 nonzero = [n for n in R if n != 0] 

159 if hermitian and not ((nonzero and nonzero[0] > 0) or (not nonzero and i < j)): 

160 continue # the conjugate of a kept representative 

161 list_hop.append({'i': i, 'j': j, 'R': R, 't': complex(t)}) 

162 ks.set_hopping(list_hop, hermitian=hermitian) 

163 return ks 

164 

165 

166def _finite_sites(ks: KSpace, n_cells: tuple[int, ...]) -> tuple[NDArray[np.float64], NDArray]: 

167 ''' 

168 Private function. Positions and tags of the rows of *ks.finite_ham(n_cells)*. 

169 ''' 

170 n_tot = int(np.prod(n_cells)) 

171 cells = np.array(np.unravel_index(np.arange(n_tot), n_cells, order='F')).T 

172 a = np.array(ks.lat.prim_vec, dtype='f8') 

173 pos = (cells @ a)[:, None, :] + ks.orbital_positions()[None, :, :] 

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

175 return pos.reshape(n_tot * ks.norb, ks.space_dim), np.tile(tags, n_tot) 

176 

177 

178def finite_model( 

179 ks: KSpace, n_cells: int | tuple[int, ...], periodic: bool = False, 

180) -> tuple[sp.csr_matrix, NDArray[np.float64], NDArray]: 

181 r''' 

182 Get a finite sample of a *KSpace* model: its sparse Hamiltonian (that of 

183 *KSpace.finite_ham*, open or periodic boundaries), with the position 

184 and tag of every row. Spinful models are supported (the two spin 

185 components of a site share its position). 

186 

187 Parameters 

188 ---------- 

189 ks : **KSpace** instance 

190 n_cells : Positive integer, or tuple of *dim* positive integers 

191 Number of unit cells along each primitive vector. 

192 periodic : Boolean, default False 

193 Periodic boundaries (a torus). 

194 

195 Returns 

196 ------- 

197 ham : Complex CSR matrix, shape (N*norb, N*norb) 

198 positions : Real ndarray, shape (N*norb, space_dim) 

199 Row 

200 :math:`c\,\mathrm{norb}+o` sits at :math:`\boldsymbol\tau_o + \sum_k n_k\mathbf{a}_k` 

201 (cell *c* has :math:`c = n_1 + N_1 n_2 + N_1N_2 n_3`). 

202 tags : String ndarray, shape (N*norb,) 

203 Sublattice tags. 

204 ''' 

205 error_handling.kspace(ks, KSpace) 

206 error_handling.nk(n_cells, ks.dim) 

207 error_handling.boolean(periodic, 'periodic') 

208 n_cells = (n_cells,) * ks.dim if isinstance(n_cells, int) else n_cells 

209 pos, tags = _finite_sites(ks, n_cells) 

210 return ks.finite_ham(n_cells, periodic=periodic, sparse=True), pos, tags 

211 

212 

213def finite_system(ks: KSpace, n_cells: int | tuple[int, ...], periodic: bool = False) -> System: 

214 r''' 

215 Build a finite *System* out of a (spinless) *KSpace* model: *lat.coor* 

216 holds the sites of :math:`N_1\times N_2\times\dots` unit cells (in the 

217 order of *finite_model*), and *sys.hop* / *sys.onsite* the model's 

218 hoppings and onsite energies, so that *sys.get_ham()* gives 

219 *ks.finite_ham(n_cells, periodic)*. Every real-space tool (*get_eig*, 

220 *get_ldos*, *Plot*, disorder, ...) then applies. 

221 

222 In *sys.hop*, each bond is stored with the conventions of 

223 *System.set_hopping*: a Hermitian model keeps one orientation per bond 

224 (angle in [0, 180), *get_ham* adds the conjugate); a non-reciprocal 

225 one keeps both, the missing direction with a zero amplitude. 'n' is the 

226 neighbour order of the bond (0 for coinciding orbitals), 'tag' the 

227 sublattice pair. On a torus, a bond that wraps around keeps its short 

228 bond vector for its angle, and a hopping that wraps onto its own site 

229 is added to the onsite energy; the System has periodic boundaries 

230 (``sys.periodic``), so that further *set_hopping* calls, *Plot* and 

231 *get_bott_index* treat it as a torus. 

232 

233 Parameters 

234 ---------- 

235 ks : **KSpace** instance, with ``spin=False`` 

236 n_cells : Positive integer, or tuple of *dim* positive integers 

237 periodic : Boolean, default False 

238 Periodic boundaries (a torus). 

239 

240 Returns 

241 ------- 

242 sys : **System** instance, with its hoppings set 

243 

244 Examples 

245 -------- 

246 Example usage:: 

247 

248 sys = finite_system(gra, (30, 30), periodic=True) 

249 sys.get_ham() 

250 sys.get_eig() # = gra.mesh_bands((30, 30)), sorted 

251 ''' 

252 error_handling.kspace(ks, KSpace) 

253 error_handling.nk(n_cells, ks.dim) 

254 error_handling.boolean(periodic, 'periodic') 

255 error_handling.spinless(ks.spin) 

256 n_cells = (n_cells,) * ks.dim if isinstance(n_cells, int) else n_cells 

257 pos, tags = _finite_sites(ks, n_cells) 

258 lat = Lattice(unit_cell=list(ks.lat.unit_cell), prim_vec=list(ks.lat.prim_vec)) 

259 coor = np.zeros(len(pos), dtype=lat.dtype) 

260 for c, f in enumerate(('x', 'y', 'z')[:ks.space_dim]): 

261 coor[f] = pos[:, c] 

262 coor['tag'] = tags 

263 lat.coor, lat.sites = coor, len(coor) 

264 lat.n1, lat.n2, lat.n3 = (tuple(n_cells) + (1, 1))[:3] 

265 sys = System(lat, periodic=periodic) 

266 _, n_tot, entries = ks._finite_entries(n_cells, periodic) 

267 tau = ks.orbital_positions() 

268 rows = np.concatenate([np.zeros(0, int)] + [e[0] for e in entries]) 

269 cols = np.concatenate([np.zeros(0, int)] + [e[1] for e in entries]) 

270 t = np.concatenate([np.zeros(0, 'c16')] + [np.full(len(e[0]), e[6], 'c16') for e in entries]) 

271 d = np.concatenate([np.zeros((0, ks.space_dim))] 

272 + [np.repeat((e[5] + tau[e[3]] - tau[e[2]])[None], len(e[0]), axis=0) 

273 for e in entries]) 

274 n_sites = len(pos) 

275 diag = rows == cols 

276 sys.onsite = np.tile(ks.onsite, n_tot) + (np.bincount(rows[diag], t[diag].real, n_sites) 

277 + 1j * np.bincount(rows[diag], t[diag].imag, n_sites)) 

278 rows, cols, t, d = rows[~diag], cols[~diag], t[~diag], d[~diag] 

279 dist = np.linalg.norm(d, axis=1) 

280 zero = dist <= ATOL 

281 ang = neighbours._bond_angles(d) 

282 up = np.where(zero, rows < cols, _upper(ang)) 

283 ang_up = np.where(zero, 0., np.maximum(np.where(up, ang, neighbours._bond_angles(-d)), 0.)) 

284 if ks._nonreciprocal: 

285 i = np.concatenate([rows, cols]) 

286 j = np.concatenate([cols, rows]) 

287 t = np.concatenate([t, np.zeros(len(t), 'c16')]) 

288 ang = np.concatenate([np.where(up, ang_up, ang_up - 180.), np.where(up, ang_up - 180., ang_up)]) 

289 dist = np.concatenate([dist, dist]) 

290 else: 

291 i, j, t, ang, dist = rows[up], cols[up], t[up], ang_up[up], dist[up] 

292 hop = np.zeros(len(i), dtype=HOP_DTYPE) 

293 hop['n'] = neighbours.shell_index(ks.lat, dist) 

294 hop['i'], hop['j'], hop['t'], hop['ang'] = i, j, t, ang 

295 hop['tag'] = np.char.add(tags[i], tags[j]) 

296 sys.hop = hop 

297 return sys