Coverage for tbkit/lattice.py: 100%
197 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
3import numpy as np
4from numpy.typing import NDArray
5import matplotlib.pyplot as plt
6from matplotlib.figure import Figure
7import tbkit.error_handling as error_handling
10PI = np.pi
11COOR_DTYPE = [('x', 'f8'), ('y', 'f8'), ('tag', 'U1')]
14#################################
15# CLASS LATTICE
16#################################
19class Lattice():
20 r'''
21 Build up 1D or 2D lattice.
22 Lattice is defined by the discrete operation:
24 .. math::
26 \mathbf{R} = n_1\mathbf{a}_1 + n_2\mathbf{a}_2
28 where :math:`\mathbf{a}_1` and :math:`\mathbf{a}_2` are the two primitive
29 vectors and :math:`n_1` and :math:`n_2` are the number of unit cells along
30 :math:`\mathbf{a}_1` and :math:`\mathbf{a}_2`.
32 :param unit_cell: List of dictionaries.
33 One dictionary per site within the unit cell. Each dictionary has two keys:
35 * 'tag', one-character string. Label of the associated sublattice.
36 * 'r0', Tuple. Position.
37 :param prim_vec: List of tuples.
38 Define the primitive vectors. List of one/two tuples for 1D/2D respectively:
40 * Tuple, cartesian coordinate of the primitive vector :math:`\mathbf{a}_1`.
41 * Tuple, cartesian coordinate of the primitive vector :math:`\mathbf{a}_2`.
43 Example usage::
45 # Line-Centered Square lattice
46 unit_cell = [{'tag': 'a', 'r0': (0., 0.)}, {'tag': 'a', 'r0': (0., 1.)}]
47 prim_vec = [(0, 2), (2, 0)]
48 lat = lattice(unit_cell=unit_cell, prim_vec=prim_vec)
49 '''
51 def __init__(self, unit_cell: list[dict], prim_vec: list[tuple[float, float]]) -> None:
52 error_handling.unit_cell(unit_cell)
53 error_handling.prim_vec(prim_vec)
54 self.unit_cell = unit_cell
55 self.prim_vec = prim_vec
56 self.tags = np.unique(np.array([dic['tag'] for dic in self.unit_cell]))
57 self.n1, self.n2 = 0, 0
58 self.coor = np.array([], dtype=COOR_DTYPE)
59 self.sites = 0
61 def get_lattice(self, n1: int, n2: int = 1) -> None:
62 r'''
63 Get the lattice positions.
65 :param n1: Positive Integer.
66 Number of unit cells along :math:`\mathbf{a}_1`.
67 :param n2: Positive Integer. Default value 1.
68 Number of unit cells along :math:`\mathbf{a}_2`.
70 Example usage::
72 # Line-Centered Square lattice
73 unit_cell = [{'tag': 'a', 'r0': (0., 0.)}, {'tag': 'a', 'r0': (0., 1.)}]
74 prim_vec = [(0, 2), (2, 0)]
75 lat = lattice(unit_cell=unit_cell, prim_vec=prim_vec)
76 lat.get_lattice(n1=4, n2=5)
77 '''
78 error_handling.get_lattice(self.prim_vec, n1, n2)
79 sites_uc = len(self.unit_cell)
80 sites_tag = n1*n2
81 self.sites = sites_uc * sites_tag
82 self.coor = np.empty(self.sites, dtype=COOR_DTYPE)
83 self.n1, self.n2 = n1, n2
84 x = self.prim_vec[0][0] * np.arange(n1, dtype='f8')
85 y = self.prim_vec[0][1] * np.arange(n1, dtype='f8')
86 xx = np.empty(n1*n2)
87 yy = np.empty(n1*n2)
88 xx[:n1] = x
89 yy[:n1] = y
90 for i in range(1, n2):
91 xx[i*n1: (i+1)*n1] = x + i * self.prim_vec[1][0]
92 yy[i*n1: (i+1)*n1] = y + i * self.prim_vec[1][1]
93 for i, dic in enumerate(self.unit_cell):
94 self.coor['x'][i*sites_tag: (i+1)*sites_tag] = xx + dic['r0'][0]
95 self.coor['y'][i*sites_tag: (i+1)*sites_tag] = yy + dic['r0'][1]
96 self.coor['tag'][i*sites_tag: (i+1)*sites_tag] = dic['tag']
97 self.coor = np.sort(self.coor, order=('y', 'x'))
99 def add_sites(self, coor: NDArray) -> None:
100 '''
101 Add sites.
103 :param coor: Structured array with keys: {'x', 'y', 'tag'}.
105 Example usage::
107 # Square lattice
108 unit_cell = [{'tag': 'a', 'r0': (0., 0.)}]
109 prim_vec = [(0, 1), (1, 0)]
110 lat = lattice(unit_cell=unit_cell, prim_vec=prim_vec)
111 lat.get_lattice(n1=2, n2=2)
112 coor = np.array([(-1., -1, 'b'), (-2., -2, 'c')],
113 dtype=[('x', 'f8'), ('y', 'f8'), ('tag', 'U1')])
114 lat.add_sites(coor)
115 '''
116 error_handling.coor(coor)
117 self.coor = np.concatenate([self.coor, coor])
118 self.sites += len(coor)
119 self.tags = np.unique(np.concatenate([self.tags, coor['tag']]))
120 self.coor = np.sort(self.coor, order=('y', 'x'))
122 def remove_sites(self, index: list[int]) -> None:
123 '''
124 Remove sites defined by their indices
125 (use method lattice.plot(plt_index=True)
126 to get access to the site indices).
128 :param index: List. Site indices to be removed.
130 Example usage::
132 # Square lattice
133 unit_cell = [{'tag': 'a', 'r0': (0., 0.)}]
134 prim_vec = [(0, 1), (1, 0)]
135 lat = lattice(unit_cell=unit_cell, prim_vec=prim_vec)
136 lat.get_lattice(n1=2, n2=2)
137 lat.remove_sites([0, 2])
138 '''
139 error_handling.empty_coor(self.coor)
140 error_handling.remove_sites(index, self.sites)
141 mask = np.ones(self.sites, bool)
142 mask[index] = False
143 self.coor = self.coor[mask]
144 self.sites = self.coor.size
146 def remove_dangling(self) -> None:
147 '''
148 Remove dangling sites
149 (sites connected with just another site).
150 '''
151 error_handling.empty_coor(self.coor)
152 while True:
153 dif_x = self.coor['x'] - self.coor['x'].reshape(self.sites, 1)
154 dif_y = self.coor['y'] - self.coor['y'].reshape(self.sites, 1)
155 dis = np.sqrt(dif_x ** 2 + dif_y ** 2)
156 dis_unique = np.unique(dis)
157 len_hop = dis_unique[1]
158 ind = np.argwhere(np.isclose(dis, len_hop))
159 dang = []
160 for i in range(self.sites):
161 if (ind[:, 0] == i).sum() == 1:
162 dang.append(i)
163 self.coor = np.delete(self.coor, dang, axis=0)
164 self.sites -= len(dang)
165 if dang == []:
166 break
168 def shift_x(self, shift: float) -> None:
169 '''
170 Shift the x coordinates.
172 :param shift: Real number. Shift value.
173 '''
174 error_handling.empty_coor(self.coor)
175 error_handling.real_number(shift, 'shift')
176 self.coor['x'] += shift
178 def shift_y(self, shift: float) -> None:
179 '''
180 Shift by *delta_x* the x coordinates.
182 :param shift: Real number. Shift value.
183 '''
184 error_handling.empty_coor(self.coor)
185 error_handling.real_number(shift, 'shift')
186 self.coor['y'] += shift
188 def change_sign_x(self) -> None:
189 '''
190 Change x coordinates sign.
191 '''
192 error_handling.empty_coor(self.coor)
193 self.coor['x'] *= -1
195 def change_sign_y(self) -> None:
196 '''
197 Change y coordinates sign.
198 '''
199 error_handling.empty_coor(self.coor)
200 self.coor['y'] *= -1
202 def boundary_line(self, cx: float, cy: float, co: float) -> None:
203 r'''
204 Select sites according to :math:`c_yy+c_xx > c_0`.
206 :param cx: Real number. cx value.
207 :param cy: Real number. cy value.
208 :param co: Real number. co value.
209 '''
210 error_handling.empty_coor(self.coor)
211 error_handling.real_number(cx, 'cx')
212 error_handling.real_number(cy, 'cy')
213 error_handling.real_number(co, 'co')
214 self.coor = self.coor[cy * self.coor['y'] + cx * self.coor['x'] > co]
215 self.sites = len(self.coor)
217 def ellipse_in(self, rx: float, ry: float, x0: float, y0: float) -> None:
218 r'''
219 Select sites according to
221 .. math::
223 (x-x_0)^2/a^2+(y-y_0)^2/b^2 < 1\, .
225 :param list_hop: List of Dictionary (see set_hopping definition).
226 :param rx: Positive Real number. Radius along :math:`x`.
227 :param ry: Positive Real number. Radius along :math:`y`.
228 :param x0: Real number. :math:`x` center.
229 :param y0: Real number. :math:`y` center.
230 '''
231 error_handling.empty_coor(self.coor)
232 error_handling.positive_real(rx, 'rx')
233 error_handling.positive_real(ry, 'ry')
234 error_handling.real_number(x0, 'x0')
235 error_handling.real_number(y0, 'y0')
236 self.coor = self.coor[(self.coor['x'] -x0) ** 2 / rx ** 2 + \
237 (self.coor['y'] -y0) ** 2 / ry ** 2 < 1.]
238 self.sites = len(self.coor)
240 def ellipse_out(self, rx: float, ry: float, x0: float, y0: float) -> None:
241 r'''
242 Select sites according to
244 .. math::
246 (x-x_0)^2/a^2+(y-y_0)^2/b^2 > 1\, .
249 :param list_hop: List of Dictionary (see set_hopping definition).
250 :param rx: Positive Real number. Radius along :math:`x`.
251 :param ry: Positive Real number. Radius along :math:`y`.
252 :param x0: Real number. :math:`x` center.
253 :param y0: Real number. :math:`y` center.
254 '''
255 error_handling.empty_coor(self.coor)
256 error_handling.positive_real(rx, 'rx')
257 error_handling.positive_real(ry, 'ry')
258 error_handling.real_number(x0, 'x0')
259 error_handling.real_number(y0, 'y0')
260 self.coor = self.coor[(self.coor['x'] -x0) ** 2 / rx ** 2 + \
261 (self.coor['y'] -y0) ** 2 / ry ** 2 > 1.]
262 self.sites = len(self.coor)
264 def center(self) -> None:
265 '''
266 Fix the center of mass of the lattice at (0, 0).
267 '''
268 error_handling.empty_coor(self.coor)
269 self.coor['x'] -= np.mean(self.coor['x'])
270 self.coor['y'] -= np.mean(self.coor['y'])
272 def rotation(self, theta: float) -> None:
273 r'''
274 Rotate the lattice structure by the angle :math:`\theta`.
276 :param theta: Rotation angle in degrees.
277 '''
278 error_handling.empty_coor(self.coor)
279 error_handling.real_number(theta, 'theta')
280 theta *= PI / 360
281 for dic in self.unit_cell:
282 x = self.coor['x'] - dic['r0'][0]
283 y = self.coor['y'] - dic['r0'][1]
284 self.coor['x'] = x * np.cos(theta) - y * np.sin(theta) + dic['r0'][0]
285 self.coor['y'] = y * np.cos(theta) + x* np.sin(theta) + dic['r0'][1]
287 def clean_coor(self) -> None:
288 '''
289 Keep only the sites with different coordinates.
290 '''
291 error_handling.empty_coor(self.coor)
292 coor = self.coor[['x', 'y']].copy()
293 coor['x'], coor['y'] = self.coor['x'].round(4), self.coor['y'].round(4)
294 _, idx = np.unique(coor, return_index=True)
295 self.coor = self.coor[idx]
296 self.sites = len(self.coor)
298 def __add__(self, other: 'Lattice') -> 'Lattice':
299 '''
300 Overloading operator +.
301 '''
302 error_handling.lat(other)
303 error_handling.empty_coor(self.coor)
304 error_handling.empty_coor(other.coor)
305 coor = np.concatenate([self.coor, other.coor])
306 tags = np.concatenate([self.tags, other.tags])
307 lat = lattice(unit_cell=self.unit_cell, prim_vec=self.prim_vec)
308 lat.add_sites(coor)
309 lat.sites = self.sites + other.sites
310 lat.tags = np.unique(tags)
311 return lat
313 def __iadd__(self, other: 'Lattice') -> 'Lattice':
314 '''
315 Overloading operator +=.
316 '''
317 error_handling.lat(other)
318 error_handling.empty_coor(self.coor)
319 error_handling.empty_coor(other.coor)
320 self.coor = np.concatenate([self.coor, other.coor])
321 self.sites += other.sites
322 self.tags = np.unique(np.concatenate([self.tags, other.tags]))
323 return self
325 def __sub__(self, other: 'Lattice') -> 'Lattice':
326 '''
327 Overloading operator -.
329 .. note::
331 The tags are not considered in the lattice subtraction.
332 '''
333 error_handling.lat(other)
334 error_handling.empty_coor(self.coor)
335 error_handling.empty_coor(other.coor)
336 boo = np.zeros(self.sites, bool)
337 for c in other.coor:
338 boo += np.isclose(c['x'], self.coor['x']) & np.isclose(c['y'], self.coor['y'])
339 coor = self.coor[np.logical_not(boo)]
340 lat = lattice(unit_cell=self.unit_cell, prim_vec=self.prim_vec)
341 lat.add_sites(coor)
342 lat.sites = len(lat.coor)
343 lat.tags = self.tags
344 return lat
346 def __isub__(self, other: 'Lattice') -> 'Lattice':
347 '''
348 Overloading operator -=.
350 .. note::
352 The tags are not considered in the lattice subtraction.
353 '''
354 error_handling.lat(other)
355 error_handling.empty_coor(self.coor)
356 error_handling.empty_coor(other.coor)
357 boo = np.zeros(self.sites, bool)
358 for c in other.coor:
359 boo += np.isclose(c['x'], self.coor['x']) & np.isclose(c['y'], self.coor['y'])
360 self.coor = self.coor[np.logical_not(boo)]
361 self.sites = int(np.sum(np.logical_not(boo)))
362 return self
364 def plot(
365 self,
366 ms: float = 20,
367 fs: float = 20,
368 plt_index: bool = False,
369 axis: bool = False,
370 figsize: tuple[float, float] | None = None,
371 ) -> Figure:
372 '''
373 Plot lattice in hopping space.
375 :param ms: Positive number. Default value 20. Markersize.
376 :param fs: Positve number. Default value 20. Fontsize.
377 :param plt_index: Boolean. Default value False. Plot site labels.
378 :param axis: Boolean. Default value False. Plot axis.
379 :param figsize: Tuple. Default value None. Figsize.
381 :returns:
382 * **fig** -- Figure.
383 '''
384 error_handling.empty_coor(self.coor)
385 error_handling.positive_real(ms, 'ms')
386 error_handling.positive_real(fs, 'fs')
387 error_handling.boolean(plt_index, 'plt_index')
388 error_handling.boolean(axis, 'axis')
389 if figsize is None:
390 figsize = (5, 5)
391 error_handling.list_tuple_2elem(figsize, 'figsize')
392 error_handling.positive_real(figsize[0], 'figsize[0]')
393 error_handling.positive_real(figsize[1], 'figsize[1]')
394 fig, ax = plt.subplots(figsize=figsize)
395 # plot sites
396 colors = ['b', 'r', 'g', 'y', 'm', 'k']
397 for color, tag in zip(colors, self.tags):
398 plt.plot(self.coor['x'][self.coor['tag'] == tag],
399 self.coor['y'][self.coor['tag'] == tag],
400 'o', color=color, ms=ms, markeredgecolor='none')
401 ax.set_aspect('equal')
402 ax.set_xlim([np.min(self.coor['x'])-1., np.max(self.coor['x'])+1.])
403 ax.set_ylim([np.min(self.coor['y'])-1., np.max(self.coor['y'])+1.])
404 if not axis:
405 ax.axis('off')
406 # plot indices
407 if plt_index:
408 indices = ['{}'.format(i) for i in range(self.sites)]
409 for l, x, y in zip(indices, self.coor['x'], self.coor['y']):
410 plt.annotate(l, xy=(x, y), xytext=(0, 0),
411 textcoords='offset points',
412 ha='right', va='bottom', size=fs)
413 plt.draw()
414 return fig
416 def show(self) -> None:
417 """
418 Emulate Matplotlib method plt.show().
419 """
420 plt.show()
423# Backward-compatible lowercase alias (pre-0.2 API).
424lattice = Lattice