Coverage for tbkit/system.py: 100%
330 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-22 13:16 +0100
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-22 13:16 +0100
1from __future__ import annotations
3from typing import Callable
5import numpy as np
6from numpy.typing import NDArray
7import scipy.sparse as sparse
8import scipy.linalg as LA
9import numpy.random as rand
10import numpy.char as npc
11from math import sin, cos
12import tbkit.error_handling as error_handling
13from tbkit.lattice import Lattice, COOR_DTYPE
16PI = np.pi
17ATOL = 1e-3
18HOP_DTYPE = [('n', 'u2'), ('i', 'u4'), ('j', 'u4'),
19 ('ang', 'f8'), ('tag', 'U2'), ('t', 'c16')]
22class System():
23 '''
24 Solve the Tight-Binding eigenvalue problem of a lattice defined
25 by the class **lattice**.
27 :param lat: **lattice** class instance.
28 '''
30 def __init__(self, lat: Lattice) -> None:
31 error_handling.lat(lat)
32 self.lat = lat
33 self.sites = self.lat.sites # used to check if sites changes
34 self.coor_hop = np.array([], dtype=COOR_DTYPE)
35 self.vec_hop = np.array([], dtype=[('dis', 'f8'), ('ang', 'f8')]) # Hopping distances and angles
36 self.dist_uni = np.array([], 'f8') # Different hopping distances
37 self.store_hop = {} # Store the relevant hoppings (dynamic programming)
38 self.hop = np.array([], dtype=HOP_DTYPE) # Hoppings to build-up the Hamiltonian
39 self.onsite = np.array([], 'c16') # Onsite energies
40 self.ham = sparse.csr_matrix(([], ([], [])), shape=(self.lat.sites, self.lat.sites)) # Hamiltonian
41 self.en = np.array([], 'c16') # Eigenenergies
42 self.rn = np.array([], 'c16') # Right eigenvectors: H |rn> = en |rn>
43 self.ln = np.array([], 'c16') # Left eigenvectors: <ln| H = en <ln|
44 self.intensity = np.array([], 'f8') # Intensities (|rn|**2)
45 self.pola = np.array([], 'f8') # sublattices polarisation (|rn^{(S)}|**2)
46 self.petermann = np.array([], 'f8') # Inverse Participation Ratio
47 self.nmax = 0 # number of different hoppings
49 def clear_hopping(self) -> None:
50 '''
51 Clear structured array *hop*.
52 '''
53 self.hop = np.array([], dtype=HOP_DTYPE)
55 def get_distances(self) -> None:
56 '''
57 Private method.
58 Get distances and angles of the edges.
59 '''
60 error_handling.sites(self.lat.sites)
61 dif_x = self.lat.coor['x'] - self.lat.coor['x'].reshape(self.lat.sites, 1)
62 dif_y = self.lat.coor['y'] - self.lat.coor['y'].reshape(self.lat.sites, 1)
63 dist = np.sqrt(dif_x ** 2 + dif_y ** 2)
64 ang = (180 / PI * np.arctan2(dif_y, dif_x))
65 self.vec_hop = np.zeros(dist.shape, dtype=[('dis', 'f8'), ('ang', 'f8')])
66 self.vec_hop['dis'] = dist
67 self.vec_hop['ang'] = ang
68 self.dist_uni = np.unique(self.vec_hop['dis'].round(4))
70 def print_distances(self, n: int = 1) -> None:
71 r'''
72 Print distances and positive angles (in degrees) :math:`\phi_+\in[0, 180)`
73 of the nth shortest edges. Negative angles are given by:
74 :math:`\phi_-= \phi_+-180` and :math:`\phi_+\in[-180, 0)`.
76 :param n: Positive integer. Number of shortest edges.
77 '''
78 error_handling.sites(self.lat.sites)
79 self.get_distances()
80 self.nmax = len(self.dist_uni) - 1
81 error_handling.positive_int_lim(n, 'n', self.nmax)
82 print('\n{} different distances between sites:'.format(self.nmax))
83 print('\nDistances between sites:')
84 for i, d in enumerate(self.dist_uni[1: n+1]):
85 if i == 0:
86 hop_name = 'st'
87 elif i == 1:
88 hop_name = 'nd'
89 elif i == 2:
90 hop_name = 'rd'
91 else:
92 hop_name = 'th'
93 print('{}{} hopping, length: {:.3f}'.format(i+1, hop_name, d))
94 print('\twith positive angles:')
95 positive_ang = self.vec_hop['ang'][np.isclose(d, self.vec_hop['dis'], atol=ATOL) &
96 (self.vec_hop['ang'] >= 0.) &
97 (self.vec_hop['ang'] < 180.)]
98 print('\t', np.unique(positive_ang.round(4)))
100 def set_onsite(self, dict_onsite: dict[str, complex]) -> None:
101 '''
102 Set onsite energies.
104 :param on: Array. Sublattice onsite energies.
106 Example usage::
108 # Line-Centered Square lattice
109 sys.set_onsite({'a': -1j, 'b': -2j})
110 '''
111 error_handling.sites(self.lat.sites)
112 error_handling.set_onsite(dict_onsite, self.lat.tags)
113 self.onsite = np.zeros(self.lat.sites, 'c16')
114 for tag, on in dict_onsite.items():
115 self.onsite[self.lat.coor['tag'] ==tag] = on
117 def fill_store_hop(self, n: int) -> None:
118 '''
119 Private method.
121 Store in *store_hop* indices (with :math:`i < j`), positive angles, and tags
122 of a given type of hopping.
123 '''
124 ind = np.argwhere(np.isclose(self.dist_uni[n], self.vec_hop['dis'], atol=ATOL))
125 ind_up = ind[ind[:, 1] > ind[:, 0]]
126 hop = np.zeros(len(ind_up), dtype=HOP_DTYPE[:-1])
127 hop['i'] = ind_up[:, 0]
128 hop['j'] = ind_up[:, 1]
129 hop['ang'] = self.vec_hop['ang'][ind_up[:, 0], ind_up[:, 1]]
130 hop['tag'] = npc.add(self.lat.coor['tag'][ind_up[:, 0]],
131 self.lat.coor['tag'][ind_up[:, 1]])
132 self.store_hop[n] = hop
134 def set_hopping(self, list_hop: list[dict], upper_part: bool = True) -> None:
135 r'''
136 Set lattice hoppings.
138 :param list_hop: List of Dictionaries.
139 Dictionary with keys ('n', 'ang', 'tag', 't') where:
141 * 'n' Positive integer, type of hoppings:
143 * 'n': 1 for nearest neighbours.
144 * 'n': 2 for next-nearest neighbours.
145 * 'n': 3 for next-next-nearest neighbours.
146 * etc...
148 * 'ang' value, float, angle, in deg, of the hoppings. (optional).
150 Hopping angles are given by the method *print_distances*.
152 * If :math:`ang \in[0, 180)`, fill the Hamiltonian upper part.
153 * If :math:`ang \in[-180, 0)`, fill the Hamiltonian lower part.
155 * 'tag' string of length 2 (optional).
157 Hopping tags.
159 * 't' Complex number.
161 Hopping value.
163 :param upper_part: Boolean. Default value True.
165 * True get hoppings with (:math:`i<j`) *i.e.* fill the Hamiltonian upper part.
166 * False get hoppings with (:math:`i>j`) *i.e.* fill the Hamiltonian lower part.
168 Example usage::
170 # fill upper part:
171 sys.set_hopping([{'n': 1, t: 1.}])
172 # fill lower part:
173 sys.set_hopping([{'n': 1, t: 1.}], upper_part=False)
174 # fill upper part: specifying the angles:
175 sys.set_hopping([{'n': 1, 'ang': 0., t: 1.}, {'n': 1, 'ang': 90, t: 2.}])
176 # fill lower part:
177 sys.set_hopping([{'n': 1, 'ang': -180., t: 1.}, {'n': 1, 'ang': -90, t: 2.}], upper_part=False)
178 # fill upper part: specifying the tags:
179 sys.set_hopping([{'n': 1, 'tag': 'ab', t: 1.}, {'n': 1, 'tag': 'ba', t: 2.}])
180 # fill lower part:
181 sys.set_hopping([{'n': 1, 'tag': 'ab', t: 1.}, {'n': 1, 'tag': 'ba', t: 2.}], upper_part=False)
182 # fill upper part: specifying the angles and tags:
183 sys.set_hopping([{'n': 1, 'ang': 0., 'tag': 'ab', t: 1.},
184 {'n': 1, 'ang': 0., 'tag': 'ba', t: 2.},
185 {'n': 1, 'ang': 90., 'tag': 'ab', t: 3.},
186 {'n': 1, 'ang': 90., 'tag': 'ba', t: 4.}])
187 # fill lower part:
188 sys.set_hopping([{'n': 1, 'ang': 0., 'tag': 'ab', t: 1.},
189 {'n': 1, 'ang': 0., 'tag': 'ba', t: 2.},
190 {'n': 1, 'ang': 90., 'tag': 'ab', t: 3.},
191 {'n': 1, 'ang': 90., 'tag': 'ba', t: 4.}]), upper_part=False)
193 .. note::
195 A Hermitian hopping matrix can be build-up only using
196 its upper part OR only using its lower part. The full matrix is then
197 automatic built by Hermitian conjugaison.
199 If both upper AND lower parts are used to build up the hopping matrix.
200 non Hermitian conjugaison is not performed *i.e.* non-Hermitian hopping matrix
201 can be built.
202 '''
203 error_handling.sites(self.lat.sites)
204 error_handling.boolean(upper_part, 'upper_part')
205 self.get_distances()
206 self.nmax = len(self.dist_uni) - 1
207 error_handling.set_hopping(list_hop, self.nmax)
208 list_n = np.unique([dic['n'] for dic in list_hop])
209 # fill, if needed self.store_hop
210 self.check_sites()
211 for n in list_n:
212 if n not in self.store_hop:
213 self.fill_store_hop(n)
214 # fill self.hop
215 for dic in list_hop:
216 if len(dic) == 2:
217 size = len(self.store_hop[dic['n']])
218 if upper_part:
219 mask = (self.hop['n'] == dic['n']) & (self.hop['i'] < self.hop['j'])
220 else:
221 mask = (self.hop['n'] == dic['n']) & (self.hop['i'] > self.hop['j'])
222 if np.sum(mask):
223 self.hop = self.hop[np.logical_not(mask)]
224 ind = np.ones(size, bool)
225 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part)
226 elif len(dic) == 3 and 'ang' in dic:
227 error_handling.angle(dic['ang'], np.unique(self.store_hop[dic['n']]['ang']), upper_part)
228 if dic['ang'] >= 0:
229 ang_store = dic['ang']
230 else:
231 ang_store = dic['ang'] + 180.
232 size = np.sum(np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL))
233 mask = (self.hop['n'] == dic['n']) & np.isclose(self.hop['ang'], dic['ang'], atol=ATOL)
234 if np.sum(mask):
235 self.hop = self.hop[np.logical_not(mask)]
236 ind = np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL)
237 error_handling.index(ind, dic)
238 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part)
239 elif len(dic) == 3 and 'tag' in dic:
240 if upper_part:
241 tag_store = dic['tag']
242 else:
243 tag_store = dic['tag'][::-1]
244 size = np.sum(self.store_hop[dic['n']]['tag'] == tag_store)
245 mask = (self.hop['n'] == dic['n']) & (self.hop['tag'] == dic['tag'])
246 if upper_part:
247 mask = self.hop['n'] == dic['n'] & (self.hop['tag'] == dic['tag']) & (self.hop['i'] < self.hop['j'])
248 else:
249 mask = self.hop['n'] == dic['n'] & (self.hop['tag'] == dic['tag']) & (self.hop['i'] > self.hop['j'])
250 if np.sum(mask):
251 self.hop = self.hop[np.logical_not(mask)]
252 ind = self.store_hop[dic['n']]['tag'] == tag_store
253 error_handling.index(ind, dic)
254 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part)
255 else:
256 error_handling.angle(dic['ang'], np.unique(self.store_hop[dic['n']]['ang']), upper_part=upper_part)
257 error_handling.tag(dic['tag'], np.unique(self.store_hop[dic['n']]['tag']))
258 if dic['ang'] >= 0:
259 ang_store = dic['ang']
260 else:
261 ang_store = dic['ang'] + 180.
262 if upper_part:
263 tag_store = dic['tag']
264 else:
265 tag_store = dic['tag'][::-1]
266 size = np.sum((self.store_hop[dic['n']]['tag'] == tag_store) &
267 (np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=ATOL)))
268 bool1 = (self.hop['n'] == dic['n']) & (self.hop['tag'] == dic['tag'])
269 bool2 = np.isclose(self.hop['ang'], dic['ang'], atol=ATOL)
270 mask = bool1 & bool2
271 if np.sum(mask):
272 self.hop = self.hop[np.logical_not(mask)]
273 ind = ((self.store_hop[dic['n']]['tag'] == tag_store) &
274 (np.isclose(ang_store, self.store_hop[dic['n']]['ang'], atol=1)))
275 error_handling.index(ind, dic)
276 hop = self.set_given_hopping(dic['n'], size, dic, ind, upper_part=upper_part)
277 self.hop = np.concatenate([self.hop, hop])
279 def check_sites(self) -> None:
280 '''
281 Private method.
282 Check if the number of sites was changed after calling the
283 method system.set_hopping().
284 '''
285 if self.sites != self.lat.sites:
286 self.store_hop = {}
287 self.sites = self.lat.sites
289 def set_given_hopping(
290 self, n: int, size: int, dic: dict, mask: NDArray, upper_part: bool,
291 ) -> NDArray:
292 '''
293 Private method.
294 Fill self.hop.
296 :param n: Integer. Hopping type.
297 :param size: Integer. Number of hoppings.
298 :param doc: Dictionary. Hopping dictionary.
299 :param mask: np.ndarray. Mask.
300 :param upper_part: Boolean. If True, self.hop['i'] < self.hop['j'].
301 '''
302 hop = np.empty(size, dtype=HOP_DTYPE)
303 hop['n'] = dic['n']
304 hop['t'] = dic['t']
305 if upper_part:
306 hop['i'] = self.store_hop[n]['i'][mask]
307 hop['j'] = self.store_hop[n]['j'][mask]
308 hop['ang'] = self.store_hop[n]['ang'][mask]
309 hop['tag'] = self.store_hop[n]['tag'][mask]
310 else:
311 hop['i'] = self.store_hop[n]['j'][mask]
312 hop['j'] = self.store_hop[n]['i'][mask]
313 hop['ang'] = self.store_hop[n]['ang'][mask] - 180
314 hop['tag'] = npc.add(self.lat.coor['tag'][hop['i']],
315 self.lat.coor['tag'][hop['j']])
316 return hop
318 def set_hopping_manual(self, dict_hop: dict[tuple[int, int], complex], upper_part: bool = True) -> None:
319 '''
320 Set hoppings manually.
322 :param dict_hop: Dictionary of hoppings.
323 key: hopping indices, val: hopping values.
325 :parameter upper_part: Boolean.
327 * True, fill the Hamiltonian upper part.
328 * False, fill the Hamiltonian lower part.
329 '''
330 hop = np.zeros(len(dict_hop), dtype=HOP_DTYPE)
331 i = [h[0] for h in dict_hop.keys()]
332 j = [h[1] for h in dict_hop.keys()]
333 t = [val for val in dict_hop.values()]
334 hop['i'], hop['j']= i, j
335 hop['t'] = t
336 hop['tag'] = npc.add(self.lat.coor['tag'][i],
337 self.lat.coor['tag'][j])
338 ang = 180 / PI * np.arctan2(self.lat.coor['y'][j]-self.lat.coor['y'][i],
339 self.lat.coor['x'][j]-self.lat.coor['x'][i])
340 if upper_part:
341 ang[ang < 0] += 180
342 else:
343 ang[ang >= 0] -= 180
344 hop['ang'] = ang
345 self.hop = np.concatenate([self.hop, hop])
347 def set_hopping_dis(self, alpha: complex) -> None:
348 '''
349 Set uniform hopping disorder.
351 :param alpha: Complex or Real number. Disorder stength.
353 Example usage::
355 sys.set_hopping_dis(alpha=0.1)
357 '''
358 error_handling.empty_hop(self.hop)
359 error_handling.number(alpha, 'alpha')
360 self.hop['t'] *= 1. + alpha * rand.uniform(-1., 1., len(self.hop))
362 def set_peierls_phase(self, phase: Callable[..., NDArray]) -> None:
363 r'''
364 Apply the Peierls substitution to the existing hoppings, to
365 capture the effect of an orbital magnetic field:
367 .. math::
369 t_{ij} \to t_{ij}\, e^{i\phi_{ij}}\, ,\quad
370 \phi_{ij} = \frac{2\pi}{\Phi_0}\int_{\mathbf{r}_i}^{\mathbf{r}_j}
371 \mathbf{A}\cdot d\mathbf{l}
373 where :math:`\mathbf{A}` is the vector potential, integrated along
374 the straight bond from site :math:`i` to site :math:`j`.
376 Must be called after *set_hopping* / *set_hopping_manual* (it
377 rescales the existing hoppings *in place*) and before *get_ham*.
378 For a uniform perpendicular field, use the convenience method
379 *set_magnetic_field* instead.
381 :param phase: Callable. ``phase(xi, yi, xj, yj)`` returns
382 :math:`\phi_{ij}`, the (real-valued) Peierls phase for the bond
383 from :math:`(x_i, y_i)` to :math:`(x_j, y_j)`. Called with
384 Numpy arrays (one value per hopping in *sys.hop*).
386 .. note::
388 *get_ham* automatically assigns the reversed bond its complex
389 conjugate, so the Hamiltonian stays Hermitian as long as
390 *phase* is antisymmetric under swapping :math:`i` and
391 :math:`j` -- true for the line integral of any vector
392 potential, since reversing the integration path negates it.
394 Example usage::
396 # Peierls phase from a uniform field via the Landau gauge
397 # A = (0, B x): captures the same physics as set_magnetic_field,
398 # just in a different (equally valid) gauge.
399 B = 0.05
400 sys.set_peierls_phase(lambda xi, yi, xj, yj: B * (xj - xi) * (xi + xj) / 2)
401 '''
402 error_handling.empty_hop(self.hop)
403 error_handling.is_callable(phase, 'phase')
404 xi = self.lat.coor['x'][self.hop['i']]
405 yi = self.lat.coor['y'][self.hop['i']]
406 xj = self.lat.coor['x'][self.hop['j']]
407 yj = self.lat.coor['y'][self.hop['j']]
408 self.hop['t'] = self.hop['t'] * np.exp(1j * phase(xi, yi, xj, yj))
410 def set_magnetic_field(self, alpha: float) -> None:
411 r'''
412 Set a uniform perpendicular magnetic field via the Peierls
413 substitution (see *set_peierls_phase*), using the symmetric gauge
414 :math:`\mathbf{A} = \frac{B}{2}(-y, x)`:
416 .. math::
418 \phi_{ij} = \pi\alpha\,(x_iy_j - x_jy_i)
420 :param alpha: Real number. Flux density :math:`B/\Phi_0`, in flux
421 quanta per unit area (in the lattice's length units) -- *i.e.*
422 the flux through a region of area :math:`S` is
423 :math:`\alpha S` flux quanta.
425 Example usage::
427 # one flux quantum per 100 unit cells of a lattice with
428 # lattice constant 1:
429 sys.set_magnetic_field(alpha=0.01)
430 '''
431 error_handling.real_number(alpha, 'alpha')
432 self.set_peierls_phase(lambda xi, yi, xj, yj: PI * alpha * (xi*yj - xj*yi))
434 def set_onsite_dis(self, alpha: complex) -> None:
435 '''
436 Set uniform onsite disorder.
438 :param alpha: Complex or Real number. Disorder stength.
440 Example usage::
442 sys.set_onsite_dis(alpha=0.1)
444 '''
445 error_handling.empty_onsite(self.onsite)
446 error_handling.number(alpha, 'alpha')
447 self.onsite += alpha * rand.uniform(-1., 1., self.lat.sites)
449 def set_onsite_def(self, onsite_def: dict[int, complex]) -> None:
450 '''
451 Set specific onsite energies.
453 :param onsite_def: Dictionary.
454 key: site indices, val: onsite values.
456 Example usage::
458 set_onsite_def(0: 1., 1: -1j)
459 '''
460 error_handling.empty_onsite(self.onsite)
461 error_handling.set_onsite_def(onsite_def, self.lat.sites)
462 for i, o in onsite_def.items():
463 self.onsite[i] = o
465 def set_hopping_def(self, hopping_def: dict[tuple[int, int], complex]) -> None:
466 '''
467 Set specific hoppings.
469 :param hopping_def: Dictionary of hoppings.
470 key: hopping indices, val: hopping values.
472 Example usage::
474 sys.set_hopping_def({(0, 1): 1., (1, 2): -1j})
475 '''
476 error_handling.empty_hop(self.hop)
477 error_handling.set_hopping_def(self.hop, hopping_def, self.lat.sites)
478 for key, val in hopping_def.items():
479 cond = (self.hop['i'] == key[0]) & (self.hop['j'] == key[1])
480 self.hop['t'][cond] = val
481 self.hop['ang'] = self.vec_hop['ang'][key[0], key[1]]
482 self.hop['tag'] = npc.add(self.lat.coor['tag'][key[0]],
483 self.lat.coor['tag'][key[1]])
485 def set_new_hopping(self, list_hop: list[dict], ind: NDArray) -> None:
486 '''
487 Private method.
488 Set new hoppings.
490 :param list_hop: List of Dictionary (see set_hopping definition).
491 :param ind: List. List of indices.
492 '''
493 for dic in list_hop:
494 if len(dic) == 2:
495 self.hop['t'][ind] = dic['t']
496 elif len(dic) == 3 and 'ang' in dic:
497 self.hop['t'][ind & (self.hop['ang'] == dic['ang'])] = dic['t']
498 elif len(dic) == 3 and 'tag' in dic:
499 self.hop['t'][ind & (self.hop['tag'] == dic['tag'])] = dic['t']
500 else:
501 self.hop['t'][ind & (self.hop['tag'] == dic['tag'])
502 & (self.hop['ang'] == dic['ang'])] = dic['t']
504 def find_square(self, xlims: tuple[float, float], ylims: tuple[float, float]) -> NDArray:
505 '''
506 Private method.
507 Find hoppings within the square.
509 :param xlims: List or Tuple. :math:`x` interval.
510 :param ylims: List or Tuple. :math:`y` interval.
511 '''
512 error_handling.lims(xlims)
513 error_handling.lims(ylims)
514 in1 = (self.lat.coor['x'][self.hop['i']] >= xlims[0]) & \
515 (self.lat.coor['y'][self.hop['i']] >= ylims[0]) & \
516 (self.lat.coor['x'][self.hop['j']] >= xlims[0]) & \
517 (self.lat.coor['y'][self.hop['j']] >= ylims[0])
518 in2 = (self.lat.coor['x'][self.hop['i']] <= xlims[1]) & \
519 (self.lat.coor['y'][self.hop['i']] <= ylims[1]) & \
520 (self.lat.coor['x'][self.hop['j']] <= xlims[1]) & \
521 (self.lat.coor['y'][self.hop['j']] <= ylims[1])
522 return in1 * in2
524 def find_ellipse(self, rx: float, ry: float, x0: float, y0: float) -> NDArray:
525 '''
526 Private method.
527 Find hoppings within the ellipse.
529 :param rx: Positive Float. Radius along :math:`x`.
530 :param ry: Positive Float. Radius along :math:`y`.
531 :param x0: Float. Defalut value 0. :math:`x` center.
532 :param y0: Float. Defalut value 0. :math:`x` center.
533 '''
534 in1 = (self.lat.coor['x'][self.hop['i']] - x0) ** 2 / rx ** 2 + \
535 (self.lat.coor['y'][self.hop['i']] - y0) ** 2 / ry ** 2 <= 1.
536 in2 = (self.lat.coor['x'][self.hop['j']] - x0) ** 2 / rx ** 2 + \
537 (self.lat.coor['y'][self.hop['j']] - y0) ** 2 / ry ** 2 <= 1.
538 return in1 * in2
540 def change_hopping_square(
541 self, list_hop: list[dict], xlims: tuple[float, float], ylims: tuple[float, float] = [-1., 1.],
542 ) -> None:
543 '''
544 Change hopping values.
546 :param list_hop: List of Dictionary (see set_hopping definition).
547 :param xlims: List or Tuple. :math:`x` interval.
548 :param ylims: List or Tuple. :math:`y` interval.
549 '''
550 error_handling.empty_hop(self.hop)
551 error_handling.set_hopping(list_hop, self.nmax)
552 ind = self.find_square(xlims, ylims)
553 self.set_new_hopping(list_hop, ind)
555 def change_hopping_ellipse(
556 self, list_hop: list[dict], rx: float, ry: float, x0: float = 0., y0: float = 0.,
557 ) -> None:
558 '''
559 Change hopping values.
561 :param list_hop: List of Dictionary (see set_hopping definition).
562 :param rx: Positive Float. Radius along :math:`x`.
563 :param ry: Positive Float. Radius along :math:`y`.
564 :param x0: Float. Default value 0. :math:`x` center.
565 :param y0: Float. Default value 0. :math:`y` center.
566 '''
567 error_handling.empty_hop(self.hop)
568 error_handling.set_hopping(list_hop, self.nmax)
569 error_handling.positive_real(rx, 'rx')
570 error_handling.positive_real(ry, 'rx')
571 error_handling.real_number(x0, 'x0')
572 error_handling.real_number(y0, 'y0')
573 ind = self.find_ellipse(rx, ry, x0, y0)
574 self.set_new_hopping(list_hop, ind)
576 def get_coor_hop(self) -> None:
577 '''
578 Get the site coordinates in hopping space
579 only considering the nearest neighbours hoppings.
580 '''
581 error_handling.empty_hop(self.hop)
582 visited = np.zeros(self.lat.sites, 'u2')
583 self.coor_hop = np.zeros(self.lat.sites, dtype=COOR_DTYPE)
584 self.coor_hop['tag'] = self.lat.coor['tag']
585 hop = self.hop[self.hop['n'] == 1]
586 hop_down = np.copy(hop)
587 hop_down['i'] = hop['j']
588 hop_down['j'] = hop[ 'i']
589 hop_down['ang'] = -180 + hop['ang']
590 hop = np.concatenate([hop, hop_down])
591 i_visit = np.min(hop['i'])
592 while True:
593 hs = hop[hop['i'] == i_visit]
594 for h in hs:
595 if visited[h['j']] == 2:
596 continue
597 self.coor_hop['x'][h['j']] = self.coor_hop['x'][i_visit] + \
598 h['t'].real*cos(PI / 180 * h['ang'])
599 self.coor_hop['y'][h['j']] = self.coor_hop['y'][i_visit] + \
600 h['t'].real*sin(PI / 180 * h['ang'])
601 visited[h['j']] = 1
602 visited[i_visit] = 2
603 explored = np.argwhere(visited == 1)
604 if not explored.any():
605 break
606 i_visit = explored[0, 0]
608 def get_ham(self) -> None:
609 '''
610 Get the Tight-Binding Hamiltonian using sys.hop.
611 '''
612 error_handling.empty_hop(self.hop)
613 error_handling.hop_sites(self.hop, self.lat.sites)
614 if np.all(self.hop['ang'] >= 0) or np.all(self.hop['ang'] < 0):
615 self.ham = sparse.csr_matrix((self.hop['t'], (self.hop['i'], self.hop['j'])),
616 shape=(self.lat.sites, self.lat.sites)) \
617 + sparse.csr_matrix((self.hop['t'].conj(), (self.hop['j'], self.hop['i'])),
618 shape=(self.lat.sites, self.lat.sites))
619 else:
620 self.ham = sparse.csr_matrix((self.hop['t'], (self.hop['i'], self.hop['j'])),
621 shape=(self.lat.sites, self.lat.sites))
622 if self.onsite.size == self.lat.sites:
623 self.ham += sparse.diags(self.onsite, 0)
625 def get_eig(self, eigenvec: bool = False, left: bool = False) -> None:
626 '''
627 Get the eigenergies, eigenvectors and polarisation.
629 :param eigenvec: Boolean. Default value False.
630 If True, get the eigenvectors.
631 :param left: Boolean. Default value False.
632 If True, get the left eigenvectors too.
633 Relevant for non-Hermitian matrices.
634 '''
635 error_handling.empty_ham(self.ham)
636 error_handling.boolean(eigenvec, 'eigenvec')
637 error_handling.boolean(left, 'left')
638 if eigenvec:
639 if (self.ham.conj().T != self.ham).nnz:
640 if not left:
641 self.en, self.rn = LA.eig(self.ham.toarray())
642 else:
643 self.en, self.rn, self.ln = LA.eig(self.ham.toarray(), left=left)
644 ind = np.argsort(self.en.real)
645 self.en = self.en[ind]
646 self.rn = self.rn[:, ind]
647 if self.ln.size:
648 self.ln = self.ln[:, ind]
649 else:
650 self.en, self.rn = LA.eigh(self.ham.toarray())
651 self.intensity = np.abs(self.rn) ** 2
652 self.pola = np.zeros((self.lat.sites, len(self.lat.tags)))
653 for i, tag in enumerate(self.lat.tags):
654 self.pola[:, i] = np.sum(self.intensity[self.lat.coor['tag'] == tag, :], axis=0)
655 else:
656 if (self.ham.conj().T != self.ham).nnz:
657 self.en = LA.eigvals(self.ham.toarray())
658 ind = np.argsort(self.en.real)
659 self.en = self.en[ind]
660 else:
661 self.en = LA.eigvalsh(self.ham.toarray())
663 def get_ipr(self) -> None:
664 r'''
665 Get the Inverse Participation Ratio:
667 .. math::
669 IPR_n = |\sum_i\psi_i^{n}|^4\, .
670 '''
671 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)')
672 self.ipr = np.sum(self.intensity ** 2, axis=0)
674 def get_petermann(self) -> None:
675 r'''
676 Get the Petermann factor:
678 .. math::
680 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}\, .
682 .. note::
684 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`.
685 '''
686 if not (self.ham.conj().T != self.ham).nnz:
687 self.petermann = np.ones(self.lat.sites)
688 return
689 error_handling.empty_ndarray(self.ln, 'sys.get_eig(eigenvec=True, left=True)')
690 left_right = np.sum(self.ln * np.conjugate(self.rn), axis=0).real
691 self.petermann = 1. / left_right ** 2
693 def get_intensity_pola_max(self, tag_pola: str) -> NDArray[np.float64]:
694 '''
695 Get the state with largest polarization on one sublattice.
697 :param tag_pola: One-character string. Sublattice tag.
699 :returns:
700 * **intensity** -- Intensity of max polarized state on *tag*.
701 '''
702 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)')
703 error_handling.tag(tag_pola, self.lat.tags)
704 i_tag = self.lat.tags == tag_pola
705 ind = np.argmax(self.pola[:, i_tag])
706 print('State with polarization: {:.5f}'.format(self.pola[ind, i_tag].item()))
707 return self.intensity[:, ind]
709 def get_intensity_pola_min(self, tag_pola: str) -> NDArray[np.float64]:
710 '''
711 Get the state with smallest polarization on one sublattice.
713 :param tag_pola: One-character string. Sublattice tag.
715 :returns:
716 * **intensity** -- Intensity of max polarized state on *tag*.
717 '''
718 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)')
719 error_handling.tag(tag_pola, self.lat.tags)
720 i_tag = self.lat.tags == tag_pola
721 ind = np.argmin(self.pola[:, i_tag])
722 print('State with polarization: {:.5f}'.format(self.pola[ind, i_tag].item()))
723 return self.intensity[:, ind]
725 def get_intensity_en(self, lims: tuple[float, float]) -> NDArray[np.float64]:
726 '''
727 Get, if any, the intensity of the sum of the states
728 between *lims[0]* and *lims[1]*.
730 :param lims: List. lims[0] energy min, lims[1] energy max.
732 :returns:
733 * **intensity** -- Sum of the intensities between (lims[0], lims[1]).
734 '''
735 error_handling.empty_ndarray(self.rn, 'sys.get_eig(eigenvec=True)')
736 error_handling.lims(lims)
737 ind = np.where((self.en > lims[0]) & (self.en < lims[1]))
738 ind = np.ravel(ind)
739 print('{} states between {} and {}'.format(len(ind), lims[0], lims[1]))
740 return np.sum(self.intensity[:, ind], axis=1)
743# Backward-compatible lowercase alias (pre-0.2 API).
744system = System