moirepy 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
moirepy-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aritra Mukhopadhyay, Jabed Umar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ include requirements.txt
moirepy-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.2
2
+ Name: moirepy
3
+ Version: 0.0.1
4
+ Summary: Simulate moire lattice systems in both real and momentum space and calculate various related observables.
5
+ Home-page: https://github.com/jabed-umar/MoirePy
6
+ Author: Aritra Mukhopadhyay, Jabed Umar
7
+ Author-email: amukherjeeniser@gmail.com, jabedumar12@gmail.com
8
+ Keywords: python,moire,lattice,physics,materials,condensed matter
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Education
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Education
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Scientific/Engineering :: Physics
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy
30
+ Requires-Dist: scipy
31
+ Requires-Dist: matplotlib
32
+ Requires-Dist: tqdm
33
+ Requires-Dist: notebook
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: keywords
41
+ Dynamic: requires-dist
42
+ Dynamic: summary
43
+
44
+ # MoirePy: Twist It, Solve It, Own It!
45
+
46
+ MoirePy is a Python package for the analysis of moiré lattice. It is designed to be a user-friendly tool for studying bilayer moiré lattices.
47
+
48
+
49
+ <!-- @jabed write here, the license should go at the bottom (I will write within this week)-->
50
+
51
+
52
+
53
+ ## License
54
+
55
+ This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
56
+
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+
59
+
60
+
61
+ ## Cite This Work
62
+
63
+ If you use this software or a modified version in academic or scientific research, please cite:
64
+
65
+ ```BibTeX
66
+ @misc{MoirePy2025,
67
+ author = {Aritra Mukhopadhyay, Jabed Umar},
68
+ title = {MoirePy: Python package for efficient tight binding simulation of bilayer moiré lattices},
69
+ year = {2025},
70
+ url = {https://jabed-umar.github.io/MoirePy/},
71
+ }
72
+ ```
@@ -0,0 +1,29 @@
1
+ # MoirePy: Twist It, Solve It, Own It!
2
+
3
+ MoirePy is a Python package for the analysis of moiré lattice. It is designed to be a user-friendly tool for studying bilayer moiré lattices.
4
+
5
+
6
+ <!-- @jabed write here, the license should go at the bottom (I will write within this week)-->
7
+
8
+
9
+
10
+ ## License
11
+
12
+ This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
13
+
14
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
15
+
16
+
17
+
18
+ ## Cite This Work
19
+
20
+ If you use this software or a modified version in academic or scientific research, please cite:
21
+
22
+ ```BibTeX
23
+ @misc{MoirePy2025,
24
+ author = {Aritra Mukhopadhyay, Jabed Umar},
25
+ title = {MoirePy: Python package for efficient tight binding simulation of bilayer moiré lattices},
26
+ year = {2025},
27
+ url = {https://jabed-umar.github.io/MoirePy/},
28
+ }
29
+ ```
@@ -0,0 +1,7 @@
1
+ from moire import MoireLattice
2
+ from layers import (
3
+ Layer,
4
+ SquareLayer,
5
+ TriangularLayer,
6
+ Rhombus60Layer,
7
+ )
@@ -0,0 +1,577 @@
1
+ import numpy as np
2
+ from typing import List, Tuple
3
+ import matplotlib.pyplot as plt
4
+ import math
5
+
6
+ from utils import get_rotation_matrix
7
+
8
+ from scipy.spatial import KDTree
9
+
10
+
11
+ # - Lattice vectors must have a +ve y component (both of them need to be in the first or second quadrant)
12
+ # - lv1 must be along the x-axis (i.e. have y = 0)
13
+ # - accordingly lv2 must be writen
14
+ # - If lv2 should not have a negative y component... however if it is desired to have a negative y component, that case is similar to having an lv2 which is
15
+ # 180 degrees rotated (diagonally opposite, -lv2) which has a +ve y component and should be used instead
16
+
17
+
18
+ class Layer: # parent class
19
+ def __init__(self, pbc=False, study_proximity = 1) -> None:
20
+ self.toll_scale = max(
21
+ np.linalg.norm(self.lv1),
22
+ np.linalg.norm(self.lv2)
23
+ )
24
+
25
+ if self.lv1[1] != 0 or self.lv2[1] < 0:
26
+ raise ValueError(
27
+ """lv1 was expected to be along the x-axis, and lv2 should have a +ve y component
28
+ Please refer to the documentation for more information: https://example.com
29
+ """ # @jabed add link to documentation
30
+ )
31
+
32
+ self.rot_m = np.eye(2)
33
+ self.pbc = pbc
34
+ self.points = None
35
+ self.kdtree = None
36
+ self.study_proximity = study_proximity
37
+
38
+ def perform_rotation(self, rot=None) -> None:
39
+ rot_m = get_rotation_matrix(rot)
40
+ self.rot_m = rot_m
41
+
42
+ # Rotate lv1 and lv2 vectors
43
+ self.lv1 = rot_m @ self.lv1
44
+ self.lv2 = rot_m @ self.lv2
45
+
46
+ # Rotate lattice_points
47
+ self.lattice_points = [
48
+ [*(rot_m @ np.array([x, y])), atom_type]
49
+ for x, y, atom_type in self.lattice_points
50
+ ]
51
+
52
+ # Rotate neighbours
53
+ self.neighbours = {
54
+ atom_type: [rot_m @ np.array(neighbour) for neighbour in neighbour_list]
55
+ for atom_type, neighbour_list in self.neighbours.items()
56
+ }
57
+
58
+ def generate_points(
59
+ self,
60
+ mlv1: np.array,
61
+ mlv2: np.array,
62
+ mln1: int=1,
63
+ mln2: int=1,
64
+ # bring_to_center=False
65
+ ) -> None:
66
+ self.mlv1 = mlv1 # Moire lattice vector 1
67
+ self.mlv2 = mlv2 # Moire lattice vector 2
68
+ self.mln1 = mln1 # Number of moire unit cells along mlv1
69
+ self.mln2 = mln2 # Number of moire unit cells along mlv2
70
+
71
+ # Step 1: Find the maximum distance to determine the grid resolution
72
+ points = [np.array([0, 0]), mlv1, mlv2, mlv1 + mlv2]
73
+ max_distance = max(
74
+ np.linalg.norm(points[0] - points[1]),
75
+ np.linalg.norm(points[0] - points[2]),
76
+ np.linalg.norm(points[0] - points[3]),
77
+ )
78
+
79
+ # Calculate number of grid points based on maximum distance and lattice vectors
80
+ n = math.ceil(max_distance / min(np.linalg.norm(self.lv1), np.linalg.norm(self.lv2))) * 2
81
+
82
+ # print(f"Calculated grid size: {n}")
83
+
84
+ # Step 2: Generate points inside one moire unit cell (based on `lv1` and `lv2`)
85
+ step1_points = [] # List to hold points inside the unit cell
86
+ step1_names = [] # List to hold the names of the points
87
+ for i in range(-n, n+1): # Iterate along mlv1
88
+ for j in range(-n, n+1): # Iterate along mlv2
89
+ # Calculate the lattice point inside the unit cell
90
+ point_o = i * self.lv1 + j * self.lv2
91
+ for xpos, ypos, name in self.lattice_points:
92
+ point = point_o + np.array([xpos, ypos])
93
+ step1_points.append(point)
94
+ step1_names.append(name)
95
+
96
+ step1_points = np.array(step1_points)
97
+ step1_names = np.array(step1_names)
98
+
99
+ # Apply the boundary check method (_inside_boundaries) to filter the points
100
+ mask = self._inside_boundaries(step1_points, 1, 1)
101
+ step1_points = step1_points[mask]
102
+ step1_names = step1_names[mask]
103
+
104
+ # Step 3: Copy and translate the unit cell to create the full moire pattern
105
+ points = [] # List to hold all the moire points
106
+ names = []
107
+ for i in range(self.mln1): # Translate along mlv1 direction
108
+ for j in range(self.mln2): # Translate along mlv2 direction
109
+ translation_vector = i * mlv1 + j * mlv2
110
+ translated_points = step1_points + translation_vector # Translate points
111
+ points.append(translated_points)
112
+ names.append(step1_names)
113
+
114
+ self.points = np.vstack(points)
115
+ self.point_types = np.hstack(names)
116
+ # print(f"{self.point_types.shape=}, {self.points.shape=}")
117
+ self.generate_kdtree()
118
+
119
+ def _point_positions(self, points: np.ndarray, A: np.ndarray, B: np.ndarray) -> np.ndarray:
120
+ # for each point this returns it's position corresponding to the parallelogram of interest
121
+ # - if the point is inside, returns (0, 0)
122
+ # - for outside, left side and right side will give -1 and 1 respectively
123
+ # - for outside, top side and bottom side will give -1 and 1 respectively
124
+
125
+ # Compute determinants for positions relative to OA and BC
126
+ det_OA = (points[:, 0] * A[1] - points[:, 1] * A[0]) <= self.toll_scale * 1e-2
127
+ det_BC = ((points[:, 0] - B[0]) * A[1] - (points[:, 1] - B[1]) * A[0]) <= self.toll_scale * 1e-2
128
+ position_y = det_OA.astype(float) + det_BC.astype(float)
129
+
130
+ # Compute determinants for positions relative to OB and AC
131
+ det_OB = (points[:, 0] * B[1] - points[:, 1] * B[0]) > -self.toll_scale * 1e-2
132
+ det_AC = ((points[:, 0] - A[0]) * B[1] - (points[:, 1] - A[1]) * B[0]) > -self.toll_scale * 1e-2
133
+ position_x = det_OB.astype(float) + det_AC.astype(float)
134
+
135
+ return np.column_stack((position_x, position_y)) - 1
136
+
137
+ def _inside_polygon(self, points: np.ndarray, polygon: np.ndarray) -> np.ndarray:
138
+ # find the points inside the polygon using the ray casting method
139
+ x, y = points[:, 0], points[:, 1]
140
+ px, py = polygon[:, 0], polygon[:, 1]
141
+ px_next, py_next = np.roll(px, -1), np.roll(py, -1)
142
+ edge_cond = (y[:, None] > np.minimum(py, py_next)) & (y[:, None] <= np.maximum(py, py_next))
143
+ with np.errstate(divide='ignore', invalid='ignore'):
144
+ xinters = np.where(py != py_next, (y[:, None] - py) * (px_next - px) / (py_next - py) + px, np.inf)
145
+ ray_crosses = edge_cond & (x[:, None] <= xinters)
146
+ inside = np.sum(ray_crosses, axis=1) % 2 == 1
147
+ return inside # mask
148
+
149
+ def _inside_boundaries(self, points: np.ndarray, mln1=None, mln2=None) -> np.ndarray:
150
+
151
+ v1 = (mln1 if mln1 else self.mln1) * self.mlv1
152
+ v2 = (mln2 if mln2 else self.mln2) * self.mlv2
153
+
154
+ p1 = np.array([0, 0])
155
+ p2 = np.array([v1[0], v1[1]])
156
+ p3 = np.array([v2[0], v2[1]])
157
+ p4 = np.array([v1[0] + v2[0], v1[1] + v2[1]])
158
+
159
+ return self._inside_polygon(
160
+ points,
161
+ np.array([p1, p2, p4, p3]) - self.toll_scale * 1e-4
162
+ )
163
+
164
+ def generate_kdtree(self) -> None:
165
+ if not self.pbc: # OBC is easy
166
+ self.kdtree = KDTree(self.points)
167
+ return
168
+
169
+ # in case of periodic boundary conditions, we need to generate a bigger set of points
170
+ all_points = []
171
+ all_point_names = []
172
+ for i in range(-1, 2):
173
+ for j in range(-1, 2):
174
+ all_points.append(self.points + i * self.mln1 * self.mlv1 + j * self.mln2 * self.mlv2)
175
+ all_point_names.append(self.point_types)
176
+
177
+ all_points = np.vstack(all_points)
178
+ all_point_names = np.hstack(all_point_names)
179
+
180
+ v1 = self.mln1 * self.mlv1
181
+ v2 = self.mln2 * self.mlv2
182
+
183
+ neigh_pad_1 = (1 + self.study_proximity) * np.linalg.norm(self.lv1) / np.linalg.norm(v1)
184
+ neigh_pad_2 = (1 + self.study_proximity) * np.linalg.norm(self.lv2) / np.linalg.norm(v2)
185
+
186
+ mask = self._inside_polygon(all_points, np.array([
187
+ ( -neigh_pad_1) * v1 + ( -neigh_pad_2) * v2,
188
+ (1+neigh_pad_1) * v1 + ( -neigh_pad_2) * v2,
189
+ (1+neigh_pad_1) * v1 + (1+neigh_pad_2) * v2,
190
+ ( -neigh_pad_1) * v1 + (1+neigh_pad_2) * v2,
191
+ ])
192
+ )
193
+ print(mask.shape, mask.dtype)
194
+ points = all_points[mask]
195
+ point_names = all_point_names[mask]
196
+
197
+ self.bigger_points = points
198
+ self.bigger_point_types = point_names
199
+
200
+
201
+ self.kdtree = KDTree(points)
202
+
203
+
204
+
205
+
206
+ # # plot the points but with colours based on the point_positions
207
+ # # - point_positions = [0, 0] -> black
208
+ # # - point_positions = [1, 0] -> red
209
+ # # - do not plot the rest of the points at all
210
+
211
+ # plt.plot(points[point_positions[:, 0] == 0][:, 0], points[point_positions[:, 0] == 0][:, 1], 'k.')
212
+ # plt.plot(points[point_positions[:, 0] == 1][:, 0], points[point_positions[:, 0] == 1][:, 1], 'r.')
213
+
214
+ # plt.plot(*all_points.T, "ro")
215
+ # plt.plot(*points.T, "b.")
216
+
217
+ # # parallellogram around the whole lattice
218
+ # plt.plot([0, self.mln1*self.mlv1[0]], [0, self.mln1*self.mlv1[1]], 'k', linewidth=1)
219
+ # plt.plot([0, self.mln2*self.mlv2[0]], [0, self.mln2*self.mlv2[1]], 'k', linewidth=1)
220
+ # plt.plot([self.mln1*self.mlv1[0], self.mln1*self.mlv1[0] + self.mln2*self.mlv2[0]], [self.mln1*self.mlv1[1], self.mln1*self.mlv1[1] + self.mln2*self.mlv2[1]], 'k', linewidth=1)
221
+ # plt.plot([self.mln2*self.mlv2[0], self.mln1*self.mlv1[0] + self.mln2*self.mlv2[0]], [self.mln2*self.mlv2[1], self.mln1*self.mlv1[1] + self.mln2*self.mlv2[1]], 'k', linewidth=1)
222
+
223
+ # # just plot mlv1 and mlv2 parallellogram
224
+ # plt.plot([0, self.mlv1[0]], [0, self.mlv1[1]], 'k', linewidth=1)
225
+ # plt.plot([0, self.mlv2[0]], [0, self.mlv2[1]], 'k', linewidth=1)
226
+ # plt.plot([self.mlv1[0], self.mlv1[0] + self.mlv2[0]], [self.mlv1[1], self.mlv1[1] + self.mlv2[1]], 'k', linewidth=1)
227
+ # plt.plot([self.mlv2[0], self.mlv1[0] + self.mlv2[0]], [self.mlv2[1], self.mlv1[1] + self.mlv2[1]], 'k', linewidth=1)
228
+
229
+ # plt.grid()
230
+ # plt.show()
231
+
232
+ self._generate_mapping()
233
+
234
+ def _generate_mapping(self) -> None:
235
+ self.mappings = {}
236
+ tree = KDTree(self.points)
237
+ translations = self._point_positions(
238
+ self.bigger_points,
239
+ self.mln1 * self.mlv1,
240
+ self.mln2 * self.mlv2
241
+ )
242
+
243
+
244
+ for i, (dx, dy) in enumerate(translations):
245
+ point = self.bigger_points[i] - (dx * self.mlv1 * self.mln1 + dy * self.mlv2 * self.mln2)
246
+ distance, index = tree.query(point)
247
+ if distance >= self.toll_scale * 1e-3:
248
+ print(f"Distance {distance} exceeds tolerance for point {i} at location {point} with translation ({dx}, {dy}).")
249
+
250
+
251
+ plt.plot(*self.bigger_points.T, "ko", alpha=0.3)
252
+ plt.plot(*self.points.T, "k.")
253
+
254
+
255
+
256
+ # plt.plot(*self.bigger_points[i], "b.")
257
+ # plt.plot(*point, "r.")
258
+ # plt.plot(*self.points[index], "g.")
259
+
260
+
261
+ # parallellogram around the whole lattice
262
+ plt.plot([0, self.mln1*self.mlv1[0]], [0, self.mln1*self.mlv1[1]], 'k', linewidth=1)
263
+ plt.plot([0, self.mln2*self.mlv2[0]], [0, self.mln2*self.mlv2[1]], 'k', linewidth=1)
264
+ plt.plot([self.mln1*self.mlv1[0], self.mln1*self.mlv1[0] + self.mln2*self.mlv2[0]], [self.mln1*self.mlv1[1], self.mln1*self.mlv1[1] + self.mln2*self.mlv2[1]], 'k', linewidth=1)
265
+ plt.plot([self.mln2*self.mlv2[0], self.mln1*self.mlv1[0] + self.mln2*self.mlv2[0]], [self.mln2*self.mlv2[1], self.mln1*self.mlv1[1] + self.mln2*self.mlv2[1]], 'k', linewidth=1)
266
+
267
+ # just plot mlv1 and mlv2 parallellogram
268
+ plt.plot([0, self.mlv1[0]], [0, self.mlv1[1]], 'k', linewidth=1)
269
+ plt.plot([0, self.mlv2[0]], [0, self.mlv2[1]], 'k', linewidth=1)
270
+ plt.plot([self.mlv1[0], self.mlv1[0] + self.mlv2[0]], [self.mlv1[1], self.mlv1[1] + self.mlv2[1]], 'k', linewidth=1)
271
+ plt.plot([self.mlv2[0], self.mlv1[0] + self.mlv2[0]], [self. mlv2[1], self.mlv1[1] + self.mlv2[1]], 'k', linewidth=1)
272
+ # for index, point in enumerate(self.bigger_points):
273
+ # plt.text(*point, f"{index}", fontsize=6)
274
+ plt.gca().add_patch(plt.Circle(point, distance/2, color='r', fill=False))
275
+
276
+ plt.grid()
277
+ plt.show()
278
+
279
+ raise ValueError(f"FATAL ERROR: Distance {distance} exceeds tolerance for point {i} at location {point}.")
280
+ self.mappings[i] = index
281
+
282
+ # point positions... for each point in self.point, point position is a array of length 2 (x, y)
283
+ # where the elemnts are -1, 0 and 1... this is what their value mean about their position
284
+ #
285
+ # (-1, 1) | (0, 1) | (1, 1)
286
+ # -----------------------------
287
+ # (-1, 0) | (0, 0) | (1, 0)
288
+ # -----------------------------
289
+ # (-1,-1) | (0,-1) | (1,-1)
290
+ #
291
+ # (0, 0) is our actual lattice part...
292
+ # do this for all points in self.bigger_points:
293
+ # all point with point_positions = (x, y) need to be translated by
294
+ # (-x*self.mlv1*self.mln1 - y*self.mlv2*self.mln2) to get the corresponding point inside the lattice
295
+ # then you would need to run a query on a newly kdtree of the smaller points...
296
+ # to the get the index of the corresponding point inside the lattice (distance should be zero, just saying)
297
+ # now we already know the index of the point in the self.bigger_points... so we can map that to the index of the point in the self.points
298
+ # then we will store that in `self.mappings``
299
+ # self.mapppings will be a dictionary with keys as the indices in the
300
+ # self.bigger_points (unique) and values as the indices in the self.points (not unique)
301
+
302
+
303
+ # def kth_nearest_neighbours(self, points, types, k = 1) -> None:
304
+ # distance_matrix = self.kdtree.sparse_distance_matrix(self.kdtree, k)
305
+
306
+
307
+ def first_nearest_neighbours(self, points: np.ndarray, types: np.ndarray):
308
+ assert self.kdtree is not None, "Generate the KDTree first by calling `Layer.generate_kdtree()`."
309
+ assert points.shape[0] == types.shape[0], "Mismatch between number of points and types."
310
+
311
+ distances_list, indices_list = [], []
312
+
313
+ for point, t in zip(points, types):
314
+ if t not in self.neighbours:
315
+ raise ValueError(f"Point type '{t}' is not defined in self.neighbours.")
316
+
317
+ relative_neighbours = np.array(self.neighbours[t])
318
+ absolute_neighbours = point + relative_neighbours
319
+ distances, indices = self.kdtree.query(absolute_neighbours, k=1)
320
+
321
+ filtered_distances, filtered_indices = [], []
322
+ for dist, idx in zip(distances, indices):
323
+ if self.pbc:
324
+ if dist > 1e-2 * self.toll_scale:
325
+ raise ValueError(f"Distance {dist} exceeds tolerance.")
326
+ filtered_distances.append(dist)
327
+ filtered_indices.append(self.mappings[idx])
328
+ else:
329
+ # if dist > 1e-2 * self.toll_scale:
330
+ # raise ValueError(f"Distance {dist} exceeds tolerance.")
331
+ filtered_distances.append(dist)
332
+ filtered_indices.append(idx)
333
+
334
+ distances_list.append(filtered_distances)
335
+ indices_list.append(filtered_indices)
336
+
337
+ return distances_list, indices_list
338
+
339
+
340
+ def query(self, points: np.ndarray, k: int = 1) -> Tuple[np.ndarray, np.ndarray]:
341
+ # Step 1:
342
+ # - get a normal query from KDTree
343
+ # - distance, index = self.kdtree.query(points, k=k)
344
+ # - remove all the points farther than (1+0.1*toll_scale) * min distance
345
+ # - return here just that if OBC
346
+
347
+ # Step 2: it will come here if PBC is True
348
+ # - for all the points map them using self.mappings
349
+ # - replace the indices with the mapped indices
350
+ # - return the mapped indices and distances (distance will be the same)
351
+
352
+ assert self.kdtree is not None, "Generate the KDTree first by calling `Layer.generate_kdtree()`."
353
+ distances, indices = self.kdtree.query(points, k=k)
354
+
355
+ # for k=1, it returns squeezed arrays... so we need to unsqueeze them
356
+ if k == 1:
357
+ distances = distances[:, None]
358
+ indices = indices[:, None]
359
+
360
+ distances_list, indices_list = distances.tolist(), indices.tolist()
361
+ if k > 1:
362
+ # Set minimum distance threshold
363
+ min_distance = distances[:, 1].min()
364
+ threshold = (1 + 1e-2 * self.toll_scale) * min_distance
365
+ # print(f"{min_distance = }, {threshold = }")
366
+
367
+ # Filter distances and indices based on thresholds
368
+ for i in range(len(distances_list)):
369
+ while distances_list[i] and distances_list[i][-1] > threshold:
370
+ distances_list[i].pop()
371
+ indices_list[i].pop()
372
+
373
+ if not self.pbc:
374
+ return distances_list, indices_list
375
+
376
+
377
+ # Convert lists back to numpy arrays for PBC
378
+ try:
379
+ distances = np.array(distances_list)
380
+ indices = np.array(indices_list)
381
+ except ValueError as e:
382
+ raise RuntimeError("FATAL ERROR: Uneven row lengths in PBC.") from e
383
+
384
+ # Apply mappings
385
+ try:
386
+ vectorized_fn = np.vectorize(self.mappings.get)
387
+ remapped_indices = vectorized_fn(indices)
388
+ except TypeError as e:
389
+ raise RuntimeError("FATAL ERROR: Mapping failed during vectorization. Check if all indices are valid.") from e
390
+ return distances, remapped_indices
391
+
392
+ def query_non_self(self, points: np.ndarray, k: int = 1) -> Tuple[np.ndarray, np.ndarray]:
393
+ distances, indices = self.query(points, k=k+1)
394
+
395
+ if self.pbc is False:
396
+ for i in range(len(indices)):
397
+ indices[i] = indices[i][1:]
398
+ distances[i] = distances[i][1:]
399
+ else:
400
+ indices = indices[:, 1:]
401
+ distances = distances[:, 1:]
402
+
403
+ # return distances[:, 1:], indices[:, 1:]
404
+ return distances, indices
405
+
406
+
407
+ def plot_lattice(self, plot_connections: bool = True, plot_unit_cell: bool = False) -> None:
408
+ # plt.figure(figsize=(8, 8))
409
+
410
+ for atom_type, atom_points in self.lattice_points.items():
411
+ x_coords = [point[0] for point in atom_points]
412
+ y_coords = [point[1] for point in atom_points]
413
+ plt.scatter(x_coords, y_coords, s=50)
414
+
415
+ if plot_connections:
416
+ for point in atom_points:
417
+ for neighbor in self.neighbours[atom_type]:
418
+ connection = point + np.array(neighbor)
419
+ plt.plot(
420
+ [point[0], connection[0]],
421
+ [point[1], connection[1]],
422
+ "r--",
423
+ alpha=0.5,
424
+ )
425
+
426
+ if plot_unit_cell:
427
+ for i in range(self.ny + 1):
428
+ # line from (lv1*0 + lv2*i) to (lv1*nx + lv2*i)
429
+ plt.plot(
430
+ [self.lv1[0] * 0 + self.lv2[0] * i, self.lv1[0] * self.nx + self.lv2[0] * i],
431
+ [self.lv1[1] * 0 + self.lv2[1] * i, self.lv1[1] * self.nx + self.lv2[1] * i],
432
+ "k:",
433
+ alpha=0.3,
434
+ )
435
+
436
+ for i in range(self.nx + 1):
437
+ # line from (lv1*i + lv2*0) to (lv1*i + lv2*ny)
438
+ plt.plot(
439
+ [self.lv1[0] * i + self.lv2[0] * 0, self.lv1[0] * i + self.lv2[0] * self.ny],
440
+ [self.lv1[1] * i + self.lv2[1] * 0, self.lv1[1] * i + self.lv2[1] * self.ny],
441
+ "k:",
442
+ alpha=0.3,
443
+ )
444
+
445
+ plt.title("Lattice Points")
446
+ plt.xlabel("X Coordinate")
447
+ plt.ylabel("Y Coordinate")
448
+ plt.axis("equal")
449
+
450
+
451
+
452
+ # ===============================================
453
+ # ======== some example layers ========
454
+ # ===============================================
455
+
456
+
457
+
458
+ class SquareLayer(Layer):
459
+ def __init__(self, pbc=False) -> None:
460
+ self.lv1 = np.array([1, 0]) # Lattice vector in the x-direction
461
+ self.lv2 = np.array([0, 1]) # Lattice vector in the y-direction
462
+ self.lattice_points = (
463
+ [0, 0, "A"],
464
+ )
465
+ self.neighbours = {
466
+ "A": [
467
+ [1, 0], # Right
468
+ [0, 1], # Up
469
+ [-1, 0], # Left
470
+ [0, -1], # Down
471
+ ],
472
+ }
473
+ self.study_proximity = 1
474
+ # study_proximity = 1 means only studying nearest neighbours will be eabled, 2 means study of next nearest neighbours will be enabled too and so on
475
+ super().__init__(pbc=pbc) # this has to go at the end
476
+
477
+ class TriangularLayer(Layer):
478
+ def __init__(self, pbc=False) -> None:
479
+ self.lv1 = np.array([1, 0]) # Lattice vector in the x-direction
480
+ self.lv2 = np.array([0.5, np.sqrt(3)/2]) # Lattice vector at 60 degrees
481
+ self.lattice_points = (
482
+ [0, 0, "A"],
483
+ )
484
+ self.neighbours = {
485
+ "A": [
486
+ [1, 0], # Right
487
+ [0.5, np.sqrt(3)/2], # Right-up
488
+ [-0.5, np.sqrt(3)/2], # Left-up
489
+ [-1, 0], # Left
490
+ [-0.5, -np.sqrt(3)/2], # Left-down
491
+ [0.5, -np.sqrt(3)/2], # Right-down
492
+ ],
493
+ }
494
+ self.study_proximity = 1
495
+ # study_proximity = 1 means only studying nearest neighbours will be eabled, 2 means study of next nearest neighbours will be enabled too and so on
496
+ super().__init__(pbc=pbc) # this has to go at the end
497
+
498
+ class Rhombus60Layer(Layer):
499
+ def __init__(self, pbc=False) -> None:
500
+ angle = 60 # hardcoded angle... make a copy of the whole class for different angles
501
+ self.lv1 = np.array([1, 0]) # Lattice vector in the x-direction
502
+ cos_angle = np.cos(np.radians(angle))
503
+ sin_angle = np.sin(np.radians(angle))
504
+ self.lv2 = np.array([cos_angle, sin_angle]) # Lattice vector at specified angle
505
+ self.lattice_points = np.array(
506
+ [0, 0, "A"],
507
+ )
508
+ self.neighbours = {
509
+ "A": [
510
+ [1, 0], # Right
511
+ [cos_angle, sin_angle], # Up
512
+ [-1, 0], # Left
513
+ [-cos_angle, -sin_angle], # Down
514
+ ],
515
+ }
516
+ self.study_proximity = 1
517
+ # study_proximity = 1 means only studying nearest neighbours will be eabled, 2 means study of next nearest neighbours will be enabled too and so on
518
+ super().__init__(pbc=pbc) # this has to go at the end
519
+
520
+
521
+ # class KagomeLayer(Layer):
522
+ # def __init__(self, pbc=False) -> None:
523
+ # self.lv1 = np.array([1, 0]) # Lattice vector in the x-direction
524
+ # self.lv2 = np.array([0.5, np.sqrt(3)/2]) # Lattice vector at 60 degrees
525
+
526
+ # self.lattice_points = (
527
+ # [0, 0, "A"],
528
+ # [0.5, 0, "B"],
529
+ # [0.25, np.sqrt(3)/4, "C"],
530
+ # )
531
+
532
+ # self.neighbours = {
533
+ # "A": [
534
+ # [ 0.5, 0], # Right
535
+ # [ 0.25, np.sqrt(3)/4], # Right-up
536
+ # [-0.5, 0], # Left
537
+ # [-0.25, -np.sqrt(3)/4], # Left-down
538
+ # ],
539
+ # "B": [
540
+ # [ 0.5, 0], # Right
541
+ # [-0.25, np.sqrt(3)/4], # Left-up
542
+ # [-0.5, 0], # Left
543
+ # [ 0.25, -np.sqrt(3)/4], # Right-down
544
+ # ],
545
+ # "C": [
546
+ # [ 0.25, np.sqrt(3)/4], # Right-up
547
+ # [-0.25, np.sqrt(3)/4], # Left-up
548
+ # [-0.25, -np.sqrt(3)/4], # Left-down
549
+ # [ 0.25, -np.sqrt(3)/4], # Right-down
550
+ # ],
551
+ # }
552
+ # super().__init__(pbc=pbc)
553
+
554
+
555
+ # class HexagonalLayer(Layer):
556
+ # def __init__(self, pbc=False) -> None:
557
+ # self.lv1 = np.array([1, 0]) # Lattice vector in the x-direction
558
+ # self.lv2 = np.array([0.5, np.sqrt(3) / 2])
559
+
560
+ # self.lattice_points = (
561
+ # # coo_x, coo_y, atom_type (unique)
562
+ # [0, 0, "A"],
563
+ # [1, 1/np.sqrt(3), "B"],
564
+ # )
565
+ # self.neighbours = {
566
+ # "A": [
567
+ # [0, 1/np.sqrt(3)],
568
+ # [-0.5, -1/(2 * np.sqrt(3))],
569
+ # [ 0.5, -1/(2 * np.sqrt(3))],
570
+ # ],
571
+ # "B": [
572
+ # [0.5, 1/(2 * np.sqrt(3))],
573
+ # [-0.5, 1/(2 * np.sqrt(3))],
574
+ # [0, -1/np.sqrt(3)],
575
+ # ],
576
+ # }
577
+ # super().__init__(pbc=pbc)
@@ -0,0 +1,34 @@
1
+ import numpy as np
2
+
3
+ from layers import (
4
+ Layer,
5
+ HexagonalLayer,
6
+ SquareLayer,
7
+ TriangularLayer,
8
+ RhombusLayer,
9
+ KagomeLayer,
10
+ )
11
+
12
+
13
+ class MoireLattice:
14
+ def __init__(self, layer1: Layer, layer2: Layer, a: int, b: int, m: int, n: int) -> None:
15
+ self.layer1 = layer1()
16
+ self.layer2 = layer2()
17
+
18
+ self.mlv1 = a * self.layer1.lv1 + b * self.layer1.lv2
19
+ self.mlv2 = m * self.layer1.lv1 + n * self.layer1.lv2
20
+
21
+ # print(f"{self.mlv1 = }")
22
+ # print(f"{self.mlv2 = }")
23
+ # print(f"{m * self.layer2.lv1 + n * self.layer2.lv2}")
24
+ # print(np.isclose(self.mlv2, m * self.layer2.lv1 + n * self.layer2.lv2))
25
+
26
+ if not np.isclose(self.mlv2, m * self.layer2.lv1 + n * self.layer2.lv2).all():
27
+ raise ValueError(f"Invalid lattice vectors {a = }, {b = }, {m = }, {n = }. {self.mlv2} != {m * self.layer2.lv1 + n * self.layer2.lv2}")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ layer1 = RhombusLayer
32
+ layer2 = RhombusLayer
33
+
34
+ lattice = MoireLattice(layer1, layer2, 10, 9, 9, 10)
@@ -0,0 +1,225 @@
1
+ from typing import Union, Callable
2
+ import numpy as np
3
+ from layers import Layer, SquareLayer, Rhombus60Layer, TriangularLayer
4
+ import matplotlib.pyplot as plt
5
+ from utils import get_rotation_matrix
6
+ import time
7
+
8
+ class MoireLattice:
9
+ def __init__(self, latticetype: Layer, a:int, b:int, nx:int=1, ny:int=1, interlayer_distance=1, pbc = False):
10
+ # study_proximity = 1 means only studying nearest neighbours will be eabled, 2 means study of next nearest neighbours will be enabled too and so on
11
+ lower_lattice = latticetype(pbc=pbc)
12
+ upper_lattice = latticetype(pbc=pbc)
13
+
14
+ lv1, lv2 = lower_lattice.lv1, lower_lattice.lv2
15
+
16
+ # c = cos(theta) between lv1 and lv2
17
+ c = np.dot(lv1, lv2) / (np.linalg.norm(lv1) * np.linalg.norm(lv2))
18
+ beta = np.arccos(c)
19
+ mlv1 = lv1 * a + lv2 * b
20
+ mlv2 = get_rotation_matrix(beta).dot(mlv1)
21
+
22
+ # the actual theta is the angle between a*lv1 + b*lv2 and b*lv1 + a*lv2
23
+ one = a * lv1 + b * lv2
24
+ two = b * lv1 + a * lv2
25
+ c = np.dot(one, two) / (np.linalg.norm(one) * np.linalg.norm(two))
26
+ theta = np.arccos(c) # in radians
27
+ print(f"theta = {theta:.4f} rad ({np.rad2deg(theta):.4f} deg)")
28
+
29
+ upper_lattice.perform_rotation(theta)
30
+
31
+ lower_lattice.generate_points(mlv1, mlv2, nx, ny)
32
+ upper_lattice.generate_points(mlv1, mlv2, nx, ny)
33
+
34
+ self.a = a
35
+ self.b = b
36
+ self.nx = nx
37
+ self.ny = ny
38
+ self.lower_lattice = lower_lattice
39
+ self.upper_lattice = upper_lattice
40
+ self.theta = theta
41
+ self.mlv1 = mlv1
42
+ self.mlv2 = mlv2
43
+ self.ham = None
44
+ self.interlayer_distance = interlayer_distance
45
+
46
+ print(f"{len(self.lower_lattice.points)} points in lower lattice")
47
+ print(f"{len(self.upper_lattice.points)} points in upper lattice")
48
+
49
+ # self.plot_lattice()
50
+
51
+ def plot_lattice(self):
52
+ mlv1 = self.mlv1
53
+ mlv2 = self.mlv2
54
+ nx = self.nx
55
+ ny = self.ny
56
+
57
+ plt.plot(*zip(*self.lower_lattice.points), 'ro')
58
+ plt.plot(*zip(*self.upper_lattice.points), 'bo')
59
+
60
+ # parallellogram around the whole lattice
61
+ plt.plot([0, nx*mlv1[0]], [0, nx*mlv1[1]], 'k', linewidth=1)
62
+ plt.plot([0, ny*mlv2[0]], [0, ny*mlv2[1]], 'k', linewidth=1)
63
+ plt.plot([nx*mlv1[0], nx*mlv1[0] + ny*mlv2[0]], [nx*mlv1[1], nx*mlv1[1] + ny*mlv2[1]], 'k', linewidth=1)
64
+ plt.plot([ny*mlv2[0], nx*mlv1[0] + ny*mlv2[0]], [ny*mlv2[1], nx*mlv1[1] + ny*mlv2[1]], 'k', linewidth=1)
65
+
66
+ # just plot mlv1 and mlv2 parallellogram
67
+ plt.plot([0, mlv1[0]], [0, mlv1[1]], 'k', linewidth=1)
68
+ plt.plot([0, mlv2[0]], [0, mlv2[1]], 'k', linewidth=1)
69
+ plt.plot([mlv1[0], mlv1[0] + mlv2[0]], [mlv1[1], mlv1[1] + mlv2[1]], 'k', linewidth=1)
70
+ plt.plot([mlv2[0], mlv1[0] + mlv2[0]], [mlv2[1], mlv1[1] + mlv2[1]], 'k', linewidth=1)
71
+
72
+ plt.grid()
73
+ plt.show()
74
+
75
+ def _validate_input1(self, a, name):
76
+ if a is None:
77
+ a = 1
78
+ print(f"WARNING: {name} is not set, setting it to 1")
79
+ if callable(a): return a
80
+ return lambda this_coo, neigh_coo, this_type, neigh_type: a
81
+
82
+ def _validate_input2(self, a, name):
83
+ if a is None:
84
+ a = 0
85
+ print(f"WARNING: {name} is not set, setting it to 1")
86
+ if callable(a): return a
87
+ return lambda this_coo, this_type: a
88
+
89
+ def generate_hamiltonian(
90
+ self,
91
+ tll: Union[float, int, Callable[[float], float]] = None,
92
+ tuu: Union[float, int, Callable[[float], float]] = None,
93
+ tlu: Union[float, int, Callable[[float], float]] = None,
94
+ tul: Union[float, int, Callable[[float], float]] = None,
95
+ tuself: Union[float, int, Callable[[float], float]] = None,
96
+ tlself: Union[float, int, Callable[[float], float]] = None,
97
+ ):
98
+ if tll is None or isinstance(tll, int) or isinstance(tll, float): tll = self._validate_input1(tll, "tll")
99
+ if tuu is None or isinstance(tuu, int) or isinstance(tuu, float): tuu = self._validate_input1(tuu, "tuu")
100
+ if tlu is None or isinstance(tlu, int) or isinstance(tlu, float): tlu = self._validate_input1(tlu, "tlu")
101
+ if tul is None or isinstance(tul, int) or isinstance(tul, float): tul = self._validate_input1(tul, "tul")
102
+ if tuself is None or isinstance(tuself, int) or isinstance(tuself, float): tuself = self._validate_input2(tuself, "tuself")
103
+ if tlself is None or isinstance(tlself, int) or isinstance(tlself, float): tlself = self._validate_input2(tlself, "tlself")
104
+ assert (
105
+ callable(tll)
106
+ and callable(tuu)
107
+ and callable(tlu)
108
+ and callable(tul)
109
+ and callable(tuself)
110
+ and callable(tlself)
111
+ ), "tuu, tll, tlu, tul, tuself and tlself must be floats, ints or callable objects like functions"
112
+ # self.plot_lattice()
113
+
114
+ # 1. interaction inside the lower lattice
115
+ ham_ll = np.zeros((len(self.lower_lattice.points), len(self.lower_lattice.points)))
116
+ _, indices = self.lower_lattice.first_nearest_neighbours(self.lower_lattice.points, self.lower_lattice.point_types)
117
+ for i in range(len(self.lower_lattice.points)): # self interactions
118
+ ham_ll[i, i] = tlself(
119
+ self.lower_lattice.points[i],
120
+ self.lower_lattice.point_types[i]
121
+ )
122
+ for this_i in range(len(self.lower_lattice.points)): # neighbour interactions
123
+ this_coo = self.lower_lattice.points[this_i]
124
+ this_type = self.lower_lattice.point_types[this_i]
125
+ for neigh_i in indices[this_i]:
126
+ neigh_coo = self.lower_lattice.points[neigh_i]
127
+ neigh_type = self.lower_lattice.point_types[neigh_i]
128
+ ham_ll[this_i, neigh_i] = tuu(this_coo, neigh_coo, this_type, neigh_type)
129
+
130
+ # 2. interaction inside the upper lattice
131
+ ham_uu = np.zeros((len(self.upper_lattice.points), len(self.upper_lattice.points)))
132
+ _, indices = self.upper_lattice.first_nearest_neighbours(self.upper_lattice.points, self.upper_lattice.point_types)
133
+ for i in range(len(self.upper_lattice.points)): # self interactions
134
+ ham_uu[i, i] = tuself(
135
+ self.upper_lattice.points[i],
136
+ self.upper_lattice.point_types[i]
137
+ )
138
+ for this_i in range(len(self.upper_lattice.points)): # neighbour interactions
139
+ this_coo = self.upper_lattice.points[this_i]
140
+ this_type = self.upper_lattice.point_types[this_i]
141
+ for neigh_i in indices[this_i]:
142
+ neigh_coo = self.upper_lattice.points[neigh_i]
143
+ neigh_type = self.upper_lattice.point_types[neigh_i]
144
+ ham_uu[this_i, neigh_i] = tll(this_coo, neigh_coo, this_type, neigh_type)
145
+
146
+ # 3. interaction from the lower to the upper lattice
147
+ ham_lu = np.zeros((len(self.lower_lattice.points), len(self.upper_lattice.points)))
148
+ _, indices = self.upper_lattice.query(self.lower_lattice.points, k=1)
149
+ for this_i in range(len(self.lower_lattice.points)):
150
+ neigh_i = indices[this_i]
151
+ ham_lu[this_i, neigh_i] = tlu(
152
+ self.lower_lattice.points[this_i],
153
+ self.upper_lattice.points[neigh_i],
154
+ self.lower_lattice.point_types[this_i],
155
+ self.upper_lattice.point_types[neigh_i],
156
+ )
157
+
158
+ # 4. interaction from the upper to the lower lattice
159
+ ham_ul = np.zeros((len(self.upper_lattice.points), len(self.lower_lattice.points)))
160
+ _, indices = self.lower_lattice.query(self.upper_lattice.points, k=1)
161
+ for this_i in range(len(self.upper_lattice.points)):
162
+ neigh_i = indices[this_i]
163
+ ham_ul[this_i, neigh_i] = tul(
164
+ self.upper_lattice.points[this_i],
165
+ self.lower_lattice.points[neigh_i],
166
+ self.upper_lattice.point_types[this_i],
167
+ self.lower_lattice.point_types[neigh_i],
168
+ )
169
+
170
+ # # in ham_ll and ham_uu, check if sum of all the rows...
171
+ # # for constant t it should represent the number of neighbours for each point
172
+ # print(f"unique sums in ham_ll: {np.unique(np.sum(ham_ll, axis=1))}")
173
+ # print(f"unique sums in ham_uu: {np.unique(np.sum(ham_uu, axis=1))}")
174
+ # print(f"unique sums in ham_lu: {np.unique(np.sum(ham_lu, axis=1))}")
175
+ # print(f"unique sums in ham_ul: {np.unique(np.sum(ham_ul, axis=1))}")
176
+
177
+ # combine the hamiltonians
178
+ self.ham = np.block([
179
+ [ham_ll, ham_lu],
180
+ [ham_ul, ham_uu]
181
+ ])
182
+
183
+
184
+ return self.ham
185
+
186
+
187
+
188
+
189
+
190
+ if __name__ == "__main__":
191
+
192
+ t = time.time()
193
+
194
+ # lattice = MoireLattice(TriangularLayer, 9, 10, 3+0, 2+0, pbc=False)
195
+ # lattice = MoireLattice(TriangularLayer, 5, 6, 2, 2, pbc=True)
196
+ lattice = MoireLattice(SquareLayer, 6, 7, 2, 2, pbc=True)
197
+ # lattice = MoireLattice(TriangularLayer, 5, 6, 2, 2, pbc=True)
198
+ # lattice = MoireLattice(TriangularLayer, 12, 13, 1, 1, pbc=True)
199
+ # lattice = MoireLattice(TriangularLayer, 9, 10, 2, 4, pbc=False)
200
+
201
+ print(f"initialization took: {time.time() - t:.2f} seconds")
202
+ t = time.time()
203
+
204
+ ham = lattice.generate_hamiltonian(1, 1, 1)
205
+
206
+ print(f"hamiltonian generation took: {time.time() - t:.2f} seconds")
207
+
208
+ # check if ham is hermitian
209
+ if np.allclose(ham, ham.T.conj()): print("Hamiltonian is hermitian.")
210
+ else: print("Hamiltonian is not hermitian.")
211
+
212
+ t = time.time()
213
+
214
+ evals, evecs = np.linalg.eigh(ham)
215
+
216
+ print(f"diagonalization took: {time.time() - t:.2f} seconds")
217
+
218
+
219
+
220
+
221
+ plt.imshow(ham, cmap="gray")
222
+ plt.colorbar()
223
+ plt.show()
224
+
225
+ # lattice.plot_lattice()
@@ -0,0 +1,9 @@
1
+ import numpy as np
2
+
3
+ def get_rotation_matrix(theta_rad):
4
+ return np.array(
5
+ [
6
+ [np.cos(theta_rad), -np.sin(theta_rad)],
7
+ [np.sin(theta_rad), np.cos(theta_rad)]
8
+ ]
9
+ )
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.2
2
+ Name: moirepy
3
+ Version: 0.0.1
4
+ Summary: Simulate moire lattice systems in both real and momentum space and calculate various related observables.
5
+ Home-page: https://github.com/jabed-umar/MoirePy
6
+ Author: Aritra Mukhopadhyay, Jabed Umar
7
+ Author-email: amukherjeeniser@gmail.com, jabedumar12@gmail.com
8
+ Keywords: python,moire,lattice,physics,materials,condensed matter
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Education
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Education
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Scientific/Engineering :: Physics
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy
30
+ Requires-Dist: scipy
31
+ Requires-Dist: matplotlib
32
+ Requires-Dist: tqdm
33
+ Requires-Dist: notebook
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: keywords
41
+ Dynamic: requires-dist
42
+ Dynamic: summary
43
+
44
+ # MoirePy: Twist It, Solve It, Own It!
45
+
46
+ MoirePy is a Python package for the analysis of moiré lattice. It is designed to be a user-friendly tool for studying bilayer moiré lattices.
47
+
48
+
49
+ <!-- @jabed write here, the license should go at the bottom (I will write within this week)-->
50
+
51
+
52
+
53
+ ## License
54
+
55
+ This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
56
+
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+
59
+
60
+
61
+ ## Cite This Work
62
+
63
+ If you use this software or a modified version in academic or scientific research, please cite:
64
+
65
+ ```BibTeX
66
+ @misc{MoirePy2025,
67
+ author = {Aritra Mukhopadhyay, Jabed Umar},
68
+ title = {MoirePy: Python package for efficient tight binding simulation of bilayer moiré lattices},
69
+ year = {2025},
70
+ url = {https://jabed-umar.github.io/MoirePy/},
71
+ }
72
+ ```
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ requirements.txt
5
+ setup.py
6
+ moirepy/__init__.py
7
+ moirepy/layers.py
8
+ moirepy/main.py
9
+ moirepy/moire.py
10
+ moirepy/utils.py
11
+ moirepy.egg-info/PKG-INFO
12
+ moirepy.egg-info/SOURCES.txt
13
+ moirepy.egg-info/dependency_links.txt
14
+ moirepy.egg-info/requires.txt
15
+ moirepy.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ numpy
2
+ scipy
3
+ matplotlib
4
+ tqdm
5
+ notebook
@@ -0,0 +1 @@
1
+ moirepy
@@ -0,0 +1,5 @@
1
+ numpy
2
+ scipy
3
+ matplotlib
4
+ tqdm
5
+ notebook
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
moirepy-0.0.1/setup.py ADDED
@@ -0,0 +1,43 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ VERSION = '0.0.1'
4
+ DESCRIPTION = 'Simulate moire lattice systems in both real and momentum space and calculate various related observables.'
5
+ with open("README.md", "r") as f:
6
+ LONG_DESCRIPTION = f.read()
7
+ with open("requirements.txt", "r") as f:
8
+ INSTALL_REQUIRES = f.read().splitlines()
9
+
10
+ # Setting up
11
+ setup(
12
+ name="moirepy",
13
+ version=VERSION,
14
+ author="Aritra Mukhopadhyay, Jabed Umar",
15
+ author_email="amukherjeeniser@gmail.com, jabedumar12@gmail.com",
16
+ description=DESCRIPTION,
17
+ long_description_content_type="text/markdown",
18
+ long_description=LONG_DESCRIPTION,
19
+ packages=find_packages(),
20
+ install_requires=INSTALL_REQUIRES,
21
+ url="https://github.com/jabed-umar/MoirePy",
22
+ keywords=['python', 'moire', 'lattice', 'physics', 'materials', 'condensed matter'],
23
+ classifiers=[
24
+ "Development Status :: 2 - Pre-Alpha",
25
+ "Intended Audience :: Education",
26
+ "Intended Audience :: Science/Research",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.7",
32
+ "Programming Language :: Python :: 3.8",
33
+ "Programming Language :: Python :: 3.9",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Topic :: Education",
38
+ "Topic :: Scientific/Engineering",
39
+ "Topic :: Scientific/Engineering :: Chemistry",
40
+ "Topic :: Scientific/Engineering :: Mathematics",
41
+ "Topic :: Scientific/Engineering :: Physics",
42
+ ]
43
+ )