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 +21 -0
- moirepy-0.0.1/MANIFEST.in +3 -0
- moirepy-0.0.1/PKG-INFO +72 -0
- moirepy-0.0.1/README.md +29 -0
- moirepy-0.0.1/moirepy/__init__.py +7 -0
- moirepy-0.0.1/moirepy/layers.py +577 -0
- moirepy-0.0.1/moirepy/main.py +34 -0
- moirepy-0.0.1/moirepy/moire.py +225 -0
- moirepy-0.0.1/moirepy/utils.py +9 -0
- moirepy-0.0.1/moirepy.egg-info/PKG-INFO +72 -0
- moirepy-0.0.1/moirepy.egg-info/SOURCES.txt +15 -0
- moirepy-0.0.1/moirepy.egg-info/dependency_links.txt +1 -0
- moirepy-0.0.1/moirepy.egg-info/requires.txt +5 -0
- moirepy-0.0.1/moirepy.egg-info/top_level.txt +1 -0
- moirepy-0.0.1/requirements.txt +5 -0
- moirepy-0.0.1/setup.cfg +4 -0
- moirepy-0.0.1/setup.py +43 -0
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.
|
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
|
+
[](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
|
+
```
|
moirepy-0.0.1/README.md
ADDED
|
@@ -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
|
+
[](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,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,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
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moirepy
|
moirepy-0.0.1/setup.cfg
ADDED
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
|
+
)
|