crystalbuilder 0.5.4__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of crystalbuilder might be problematic. Click here for more details.
- crystalbuilder/__init__.py +27 -0
- crystalbuilder/bilbao.py +297 -0
- crystalbuilder/conversions/lumc.py +36 -0
- crystalbuilder/conversions/t3d.py +179 -0
- crystalbuilder/convert.py +379 -0
- crystalbuilder/geometry.py +759 -0
- crystalbuilder/lattice.py +795 -0
- crystalbuilder/utils.py +22 -0
- crystalbuilder/vectors.py +279 -0
- crystalbuilder/viewer.py +82 -0
- crystalbuilder-0.5.4.dist-info/METADATA +12 -0
- crystalbuilder-0.5.4.dist-info/RECORD +14 -0
- crystalbuilder-0.5.4.dist-info/WHEEL +5 -0
- crystalbuilder-0.5.4.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import ast
|
|
3
|
+
from matplotlib import pyplot as plt
|
|
4
|
+
from crystalbuilder import vectors as vm
|
|
5
|
+
import copy
|
|
6
|
+
from crystalbuilder import geometry as geometry
|
|
7
|
+
|
|
8
|
+
debug='off'
|
|
9
|
+
scattersizes = 1/72
|
|
10
|
+
|
|
11
|
+
def check_numbound(point:list, bound1:list, bound2:list, bound3:list):
|
|
12
|
+
if (bound1[0] <= point[0] <= bound1[1]) == False:
|
|
13
|
+
return False
|
|
14
|
+
elif (bound2[0] <= point[1] <= bound2[1]) == False:
|
|
15
|
+
return False
|
|
16
|
+
elif (bound3[0] <= point[2] <= bound3[1]) == False:
|
|
17
|
+
return False
|
|
18
|
+
else:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
def _plot_modulation(modulation_output, **kwargs):
|
|
22
|
+
"""
|
|
23
|
+
Makes a scatter plot of each point, color coded by its modulation, on a scale of -maxmod to maxmod
|
|
24
|
+
|
|
25
|
+
Returns nothing. Simply plots.
|
|
26
|
+
|
|
27
|
+
point sizes are determined by global variable scattersizes, which can be changed after import.
|
|
28
|
+
"""
|
|
29
|
+
ax = kwargs.get("ax")
|
|
30
|
+
position = modulation_output[0]
|
|
31
|
+
mod = modulation_output[1]
|
|
32
|
+
maxmod = modulation_output[2][0]
|
|
33
|
+
if debug=='on': print("The inputs are position[0]: {}, position[1]: {} \n mod:{}, maxmod: {}".format(position[0], position[1], mod, maxmod))
|
|
34
|
+
if ax == None:
|
|
35
|
+
plt.scatter(position[0], position[1], c=mod, vmin=-maxmod, vmax=maxmod, cmap='Spectral', s=scattersizes, linewidths=0)
|
|
36
|
+
else:
|
|
37
|
+
ax.scatter(position[0], position[1], c=mod, vmin=-maxmod, vmax=maxmod, cmap='Spectral', s=scattersizes, linewidths=0)
|
|
38
|
+
|
|
39
|
+
class Lattice:
|
|
40
|
+
"""
|
|
41
|
+
This class handles defining and building the lattice itself, but also includes the functions for making
|
|
42
|
+
repeats of geometries.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
a1 = np.array([1,0,0]),
|
|
49
|
+
a2 = np.array([0,1,0]),
|
|
50
|
+
a3 = np.array([0,0,1]),
|
|
51
|
+
magnitude = np.array([1,1,1]),
|
|
52
|
+
**kwargs):
|
|
53
|
+
"""
|
|
54
|
+
The Lattice class can be initialized with specified a1, a2, a3 vectors and magnitude
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
a1 (3-array, asarray-compatible object): first lattice vector - defaults to 1,0,0 (Cartesian X)
|
|
58
|
+
a2 (3-array, asarray-compatible object): second lattice vector - defaults to 0,1,0 (Cartesian Y)
|
|
59
|
+
a3 (3-array, asarray-compatible object): third lattice vector - defaults to 0,0,1 (Cartesian Z)
|
|
60
|
+
magnitude (3-array, asarray-compatible object): lattice vector lengths - defaults to 1,1,1 (unit vectors)
|
|
61
|
+
|
|
62
|
+
Properties
|
|
63
|
+
-----------
|
|
64
|
+
basis : array of vectors scaled to magnitudes
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
self.a1 = np.asarray(a1)
|
|
68
|
+
self.a2 = np.asarray(a2)
|
|
69
|
+
self.a3 = np.asarray(a3)
|
|
70
|
+
self.magnitude = np.asarray(magnitude)
|
|
71
|
+
self.array = kwargs.get("a_array", None)
|
|
72
|
+
self.latt_const = self.magnitude[0]
|
|
73
|
+
|
|
74
|
+
if self.array != None:
|
|
75
|
+
self.a1 = self.array[0]
|
|
76
|
+
self.a2 = self.array[1]
|
|
77
|
+
self.a3 = self.array[2]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
self.a1_scaled = ((self.a1/np.sqrt(self.a1.dot(self.a1)))*self.magnitude[0])
|
|
81
|
+
self.a2_scaled = ((self.a2/np.sqrt(self.a2.dot(self.a2)))*self.magnitude[1])
|
|
82
|
+
self.a3_scaled = ((self.a3/np.sqrt(self.a3.dot(self.a3)))*self.magnitude[2])
|
|
83
|
+
|
|
84
|
+
self.basis = np.concatenate([[self.a1_scaled],[self.a2_scaled], [self.a3_scaled]], axis=0)
|
|
85
|
+
|
|
86
|
+
### Basis Manipulation ###
|
|
87
|
+
def rotate_basis(self, theta, axis = 2, unit='degrees'):
|
|
88
|
+
origin = (0,0,0)
|
|
89
|
+
self.basis[0] = vm.rotate([self.a1_scaled], theta, origin, axis=axis, unit=unit, toarray=True)
|
|
90
|
+
self.basis[1] = vm.rotate([self.a2_scaled], theta, origin, axis=axis, unit=unit, toarray=True)
|
|
91
|
+
self.basis[2] = vm.rotate([self.a3_scaled], theta, origin, axis=axis, unit=unit, toarray=True)
|
|
92
|
+
|
|
93
|
+
### Convenience Methods ###
|
|
94
|
+
def lat_to_cart(self, point):
|
|
95
|
+
newpoint= vm.basis_change(self.basis, 'cartesian', point)
|
|
96
|
+
return newpoint
|
|
97
|
+
|
|
98
|
+
def cart_to_lat(self, point):
|
|
99
|
+
newpoint = vm.basis_change('cartesian', self.basis, point)
|
|
100
|
+
return newpoint
|
|
101
|
+
|
|
102
|
+
def cart_to_polar(self, point):
|
|
103
|
+
newpoint = vm.cart_to_pol(point)
|
|
104
|
+
return newpoint
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### Tiling Methods ###
|
|
108
|
+
def tile_mpgeometry(self, VerticesList:list, a1reps:int, a2reps:int, a3reps:int, style='centered'):
|
|
109
|
+
"""
|
|
110
|
+
INCOMPLETE - RETURNS COORDINATES, NOT NEW OBJECTS;
|
|
111
|
+
For MEEP/MPB
|
|
112
|
+
Accepts a structure defined by VerticesList, then adds various combinations of basis vectors to
|
|
113
|
+
repeat the structures a1reps * a2reps times. Style determines if reps are formed from adding and subtracting
|
|
114
|
+
or just adding the basis vectors (e.g. if the first point is the bottom left or center of final tiling)
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
VerticesList: list of lists, Vector3, or array_like
|
|
119
|
+
vertices of geometry object(s)
|
|
120
|
+
a1reps, a2reps, a3reps: int
|
|
121
|
+
number of repetitions along a1, a2, a3 lattice vectors respectively
|
|
122
|
+
style: str
|
|
123
|
+
"centered" or "positive" to tile in all directions or in +x only.
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
newpos: list
|
|
127
|
+
List of lists with [x,y,z] coordinates of the lattice sites
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
#All points are going to be 3-tuples or 3-lists. Varying number of vertices in each geometry.
|
|
133
|
+
geolist = []
|
|
134
|
+
newpos = []
|
|
135
|
+
#print(type(VerticesList))
|
|
136
|
+
#print(isinstance(VerticesList, Triangle))
|
|
137
|
+
if isinstance(VerticesList, geometry.Triangle):
|
|
138
|
+
VerticesList = VerticesList.vertlist
|
|
139
|
+
|
|
140
|
+
for geometry in VerticesList:
|
|
141
|
+
#iterate through the list of geometry objects
|
|
142
|
+
if isinstance(geometry, list):
|
|
143
|
+
#print(geometry)
|
|
144
|
+
xcen = geometry[0]
|
|
145
|
+
ycen = geometry[1]
|
|
146
|
+
zcen = geometry[2]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
geolist.append([xcen, ycen, zcen])
|
|
150
|
+
for n in range(a1reps):
|
|
151
|
+
if style == 'centered':
|
|
152
|
+
n+= -(a1reps//2)
|
|
153
|
+
for m in range(a2reps):
|
|
154
|
+
if style == 'centered':
|
|
155
|
+
m+= -(a2reps//2)
|
|
156
|
+
for k in range(0,a3reps):
|
|
157
|
+
if style == 'centered':
|
|
158
|
+
k+= -(a3reps//2)
|
|
159
|
+
|
|
160
|
+
newx = xcen+(n*self.basis[0][0] + m*self.basis[1][0] + k*self.basis[2][0])
|
|
161
|
+
newy = ycen+(n*self.basis[0][1] + m*self.basis[1][1] + k*self.basis[2][1])
|
|
162
|
+
newz = zcen+(n*self.basis[0][2] + m*self.basis[1][2] + k*self.basis[2][2])
|
|
163
|
+
|
|
164
|
+
newpos.append([newx, newy, newz])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
else:
|
|
169
|
+
print("This is a", type(geometry), '\n')
|
|
170
|
+
|
|
171
|
+
return newpos
|
|
172
|
+
|
|
173
|
+
def tile_tdgeometry(self, Geometry, a1reps:int, a2reps:int, a3reps: int, style='centered', **kwargs):
|
|
174
|
+
import tidy3d as td
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
Tiles Tidy3D Geometry
|
|
178
|
+
Similar to tile_mpgeometry, but meant for tidy3d classes. Can accept either Geometry or GeometryGroup
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
------
|
|
182
|
+
Geometry : Tidy3D Geometry Object
|
|
183
|
+
Geometry or list of geometries to tile
|
|
184
|
+
a1reps : int
|
|
185
|
+
number of repetitions in a1 direction
|
|
186
|
+
a2reps : int
|
|
187
|
+
number of repetitions in a2 direction
|
|
188
|
+
a3reps : int
|
|
189
|
+
number of repetitions in a3 direction
|
|
190
|
+
style : 'centered', 'positive', 'bounded', 'numbounded'
|
|
191
|
+
bounded options require kwargs
|
|
192
|
+
|
|
193
|
+
Keyword Arguments
|
|
194
|
+
-----------------
|
|
195
|
+
a1bounds: list
|
|
196
|
+
start and end bounds in a1 direction.
|
|
197
|
+
If style is 'bounded', a1bounds should be [int, int] to define starting and stopping lattice sites
|
|
198
|
+
if style is 'numbounded', a1bounds should be [float, float] to define starting and stopping coordinates
|
|
199
|
+
|
|
200
|
+
a2bounds: list
|
|
201
|
+
start and end bounds in a2 direction.
|
|
202
|
+
If style is 'bounded', a2bounds should be [int, int] to define starting and stopping lattice sites
|
|
203
|
+
if style is 'numbounded', a2bounds should be [float, float] to define starting and stopping coordinates
|
|
204
|
+
|
|
205
|
+
a3bounds: list
|
|
206
|
+
start and end bounds in a3 direction.
|
|
207
|
+
If style is 'bounded', a3bounds should be [int, int] to define starting and stopping lattice sites
|
|
208
|
+
if style is 'numbounded', a3bounds should be [float, float] to define starting and stopping coordinates
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
newgeom = []
|
|
212
|
+
if isinstance(Geometry, td.GeometryGroup):
|
|
213
|
+
print("It's a Geometry Group")
|
|
214
|
+
for n in Geometry.geometries:
|
|
215
|
+
if isinstance(n, td.PolySlab):
|
|
216
|
+
print("Polyslab has no center attribute")
|
|
217
|
+
|
|
218
|
+
else:
|
|
219
|
+
xcen = n.center[0]
|
|
220
|
+
ycen = n.center[1]
|
|
221
|
+
zcen = n.center[2]
|
|
222
|
+
|
|
223
|
+
tiledpoints = self.tiling([xcen, ycen,zcen], a1reps, a2reps, a3reps, style=style)
|
|
224
|
+
|
|
225
|
+
for m in range(0,len(tiledpoints)):
|
|
226
|
+
newgeom.append(n.updated_copy(center=tiledpoints[m]))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
elif isinstance(Geometry, (td.components.geometry.Box, td.components.geometry.Sphere, td.components.geometry.Cylinder, td.components.geometry.PolySlab, td.components.geometry.TriangleMesh)):
|
|
230
|
+
print("It's a ", type(Geometry))
|
|
231
|
+
|
|
232
|
+
else:
|
|
233
|
+
print("Geometry must be a td.Geometry object, not ", type(Geometry))
|
|
234
|
+
|
|
235
|
+
return td.GeometryGroup(geometries=newgeom)
|
|
236
|
+
|
|
237
|
+
def tile_geogeometry(self, Geometry, a1reps:int, a2reps:int, a3reps: int, style='centered', **kwargs):
|
|
238
|
+
"""
|
|
239
|
+
Tiles CrystalBuilder.geometry object and outputs new lists of vertices.
|
|
240
|
+
|
|
241
|
+
This method is recommended for all new structures. the conversion_methods module can be used to convert the output to MPB or Tidy3D. it's recommended to only use the other methods if there is an existing MPB or Tidy3D structure you are trying to tile.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
------
|
|
245
|
+
Geometry : CrystalBuilder.geometry object
|
|
246
|
+
Geometry or list of geometries to tile
|
|
247
|
+
a1reps : int
|
|
248
|
+
number of repetitions in a1 direction
|
|
249
|
+
a2reps : int
|
|
250
|
+
number of repetitions in a2 direction
|
|
251
|
+
a3reps : int
|
|
252
|
+
number of repetitions in a3 direction
|
|
253
|
+
style : 'centered', 'positive', 'bounded', 'numbounded', 'radial'
|
|
254
|
+
bounded options require kwargs
|
|
255
|
+
|
|
256
|
+
Keyword Arguments
|
|
257
|
+
-----------------
|
|
258
|
+
a1bounds: list
|
|
259
|
+
start and end bounds in a1 direction.
|
|
260
|
+
If style is 'bounded', a1bounds should be [int, int] to define starting and stopping lattice sites
|
|
261
|
+
if style is 'numbounded', a1bounds should be [float, float] to define starting and stopping coordinates
|
|
262
|
+
|
|
263
|
+
a2bounds: list
|
|
264
|
+
start and end bounds in a2 direction.
|
|
265
|
+
If style is 'bounded', a2bounds should be [int, int] to define starting and stopping lattice sites
|
|
266
|
+
if style is 'numbounded', a2bounds should be [float, float] to define starting and stopping coordinates
|
|
267
|
+
|
|
268
|
+
a3bounds: list
|
|
269
|
+
start and end bounds in a3 direction.
|
|
270
|
+
If style is 'bounded', a3bounds should be [int, int] to define starting and stopping lattice sites
|
|
271
|
+
if style is 'numbounded', a3bounds should be [float, float] to define starting and stopping coordinates
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
newgeom = []
|
|
276
|
+
if isinstance(Geometry, geometry.Triangle):
|
|
277
|
+
if vm.debug == 'on': print("Lat: Geometry is a Triangle")
|
|
278
|
+
xcen = Geometry.center[0]
|
|
279
|
+
ycen = Geometry.center[1]
|
|
280
|
+
zcen = Geometry.center[2]
|
|
281
|
+
if vm.debug == 'on': print("Lat: ", [xcen, ycen, zcen])
|
|
282
|
+
|
|
283
|
+
tiledpoints = self.tiling([xcen, ycen,zcen], a1reps, a2reps, a3reps, style=style)
|
|
284
|
+
if vm.debug == 'on': print("Lat: ", tiledpoints)
|
|
285
|
+
|
|
286
|
+
for m in range(0,len(tiledpoints)):
|
|
287
|
+
if vm.debug == 'on': print("Lat: the center is: \n", tiledpoints[m], "\n")
|
|
288
|
+
newstruct = Geometry.copy(center=tiledpoints[m])
|
|
289
|
+
newgeom.append(newstruct)
|
|
290
|
+
|
|
291
|
+
if vm.debug == 'on':
|
|
292
|
+
print("Lat: ", "newgeom = ", newgeom[0].center, " ", newgeom[1].center)
|
|
293
|
+
|
|
294
|
+
elif isinstance(Geometry, geometry.Sphere):
|
|
295
|
+
if vm.debug == 'on': print("Lat: Geometry is a Sphere")
|
|
296
|
+
xcen = Geometry.center[0]
|
|
297
|
+
ycen = Geometry.center[1]
|
|
298
|
+
zcen = Geometry.center[2]
|
|
299
|
+
if vm.debug == 'on': print("Lat: ", [xcen, ycen, zcen])
|
|
300
|
+
|
|
301
|
+
tiledpoints = self.tiling([xcen, ycen,zcen], a1reps, a2reps, a3reps, style=style)
|
|
302
|
+
if vm.debug == 'on': print("Lat: ", tiledpoints)
|
|
303
|
+
|
|
304
|
+
for m in range(0,len(tiledpoints)):
|
|
305
|
+
if vm.debug == 'on': print("Lat: the center is: \n", tiledpoints[m], "\n")
|
|
306
|
+
newstruct = Geometry.copy(center=tiledpoints[m])
|
|
307
|
+
newgeom.append(newstruct)
|
|
308
|
+
|
|
309
|
+
if vm.debug == 'on':
|
|
310
|
+
print("Lat: ", "newgeom = ", newgeom[0].center, " ", newgeom[1].center)
|
|
311
|
+
|
|
312
|
+
elif isinstance(Geometry, list):
|
|
313
|
+
if vm.debug == 'on': print("Lat: Geometry is a list")
|
|
314
|
+
for n in Geometry:
|
|
315
|
+
if isinstance(n, geometry.Structure):
|
|
316
|
+
xcen = n.center[0]
|
|
317
|
+
ycen = n.center[1]
|
|
318
|
+
zcen = n.center[2]
|
|
319
|
+
|
|
320
|
+
tiledpoints = self.tiling([xcen, ycen,zcen], a1reps, a2reps, a3reps, style=style)
|
|
321
|
+
|
|
322
|
+
for m in range(0,len(tiledpoints)):
|
|
323
|
+
newstruct = n.copy(center=tiledpoints[m])
|
|
324
|
+
newgeom.append(newstruct)
|
|
325
|
+
else:
|
|
326
|
+
sublist = self.tile_geogeometry(n, a1reps, a2reps, a3reps)
|
|
327
|
+
newgeom.append(sublist)
|
|
328
|
+
|
|
329
|
+
elif isinstance(Geometry, geometry.SuperCell):
|
|
330
|
+
if debug == 'on': print("Lat: Geometry is a SuperCell")
|
|
331
|
+
xcen = Geometry.center[0]
|
|
332
|
+
ycen = Geometry.center[1]
|
|
333
|
+
zcen = Geometry.center[2]
|
|
334
|
+
if debug == 'on': print("Lat: Cell Center is ", str([xcen, ycen, zcen]))
|
|
335
|
+
|
|
336
|
+
tiledpoints = self.tiling([xcen, ycen,zcen], a1reps, a2reps, a3reps, style=style)
|
|
337
|
+
if debug == 'on': print("Lat: The Tiled Points are at ", tiledpoints)
|
|
338
|
+
|
|
339
|
+
for m in range(0,len(tiledpoints)):
|
|
340
|
+
if debug == 'on': print("Lat: the center is: \n", tiledpoints[m], "\n")
|
|
341
|
+
newstruct = Geometry.copy(center=tiledpoints[m], relative_center=[xcen, ycen, zcen])
|
|
342
|
+
newgeom.append(newstruct)
|
|
343
|
+
|
|
344
|
+
if debug == 'on':
|
|
345
|
+
print("Lat: newgeom = ", newgeom[0].center, " ", newgeom[1].center)
|
|
346
|
+
|
|
347
|
+
else:
|
|
348
|
+
print("Geometry must be a CrystalBuilder.geometry object, not ", type(Geometry))
|
|
349
|
+
|
|
350
|
+
return newgeom
|
|
351
|
+
|
|
352
|
+
def _radial_tiling(self, centers, radius:int, startrad=0):
|
|
353
|
+
"""
|
|
354
|
+
Tiles in 2D radially from [startrad] to [radius] unit cells
|
|
355
|
+
|
|
356
|
+
Should be called by passing "radial" as the style for tile_geogeometry()
|
|
357
|
+
"""
|
|
358
|
+
xcen = centers[0]
|
|
359
|
+
ycen = centers[1]
|
|
360
|
+
zcen = centers[2]
|
|
361
|
+
|
|
362
|
+
krange = range(int(np.ceil(-5)), int(np.ceil(5)))
|
|
363
|
+
mrange = range(int(np.ceil(-5)), int(np.ceil(5)))
|
|
364
|
+
prange = range(int(np.ceil(-2)), int(np.ceil(2)))
|
|
365
|
+
|
|
366
|
+
newpoints = []
|
|
367
|
+
for k in krange: #a1 loop
|
|
368
|
+
for m in mrange: # a2 loop
|
|
369
|
+
for p in prange: # a3 loop
|
|
370
|
+
if (abs(k) <= radius) and (abs(m) <= radius) and (abs(k) >= startrad) and (abs(m) >= startrad) :
|
|
371
|
+
newx = xcen+(k*self.basis[0][0] + m*self.basis[1][0] + p*self.basis[2][0])
|
|
372
|
+
newy = ycen+(k*self.basis[0][1] + m*self.basis[1][1] + p*self.basis[2][1])
|
|
373
|
+
newz = zcen+(k*self.basis[0][2] + m*self.basis[1][2] + p*self.basis[2][2])
|
|
374
|
+
newvert = [newx, newy, newz]
|
|
375
|
+
# if check_radbound(newvert, a1lims, a2lims, a3lims) == True:
|
|
376
|
+
newpoints.append(newvert)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
return newpoints
|
|
380
|
+
|
|
381
|
+
def tiling(self,centers:list, a1reps:int, a2reps:int, a3reps:int, style='centered', **kwargs):
|
|
382
|
+
"""
|
|
383
|
+
Tiling function called by Tidy3D and Meep methods. Accepts [xcen, ycen, zcen] for structures with centers.
|
|
384
|
+
Else, idk yet. Not sure how I want to do triangles.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
centers : list
|
|
389
|
+
X,Y,Z center in order
|
|
390
|
+
a1reps : int
|
|
391
|
+
number of repetitions in basis direction a1
|
|
392
|
+
a2reps : int
|
|
393
|
+
number of repetitions in basis direction a2
|
|
394
|
+
a3reps : int
|
|
395
|
+
number of repetitions in basis direction a3
|
|
396
|
+
style : str
|
|
397
|
+
"centered" to shift tiling center to 0,0,0.
|
|
398
|
+
|
|
399
|
+
'positive' to tile from 0,0,0 in the positive directions
|
|
400
|
+
|
|
401
|
+
'bounded' tile from a1bounds[0] to a1bounds[1] (and a2bounds, a3bounds), given in lattice units
|
|
402
|
+
|
|
403
|
+
'numbounded' tile from a1bounds[0] to a1bounds[1] given in raw coordinates
|
|
404
|
+
|
|
405
|
+
'radial' to tile a circular-ish region
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
if (style=='centered'):
|
|
411
|
+
krange = range(int(np.ceil(-a1reps/2)), int(np.ceil(a1reps/2)))
|
|
412
|
+
mrange = range(int(np.ceil(-a2reps/2)), int(np.ceil(a2reps/2)))
|
|
413
|
+
prange = range(int(np.ceil(-a3reps/2)), int(np.ceil(a3reps/2)))
|
|
414
|
+
|
|
415
|
+
elif style=='positive':
|
|
416
|
+
krange = range(a1reps)
|
|
417
|
+
mrange = range(a2reps)
|
|
418
|
+
prange = range(a3reps)
|
|
419
|
+
|
|
420
|
+
elif style=='bounded':
|
|
421
|
+
a1lims = kwargs.get("a1bounds", [0,a1reps])
|
|
422
|
+
a2lims = kwargs.get("a2bounds", [0,a2reps])
|
|
423
|
+
a3lims = kwargs.get("a3bounds", [0,a3reps])
|
|
424
|
+
|
|
425
|
+
krange = range(a1lims[0], a1lims[1])
|
|
426
|
+
mrange = range(a2lims[0], a2lims[1])
|
|
427
|
+
prange = range(a3lims[0], a3lims[1])
|
|
428
|
+
|
|
429
|
+
elif style=='numbounded':
|
|
430
|
+
#defcut defines number of unit cells numerically.
|
|
431
|
+
a1lims = kwargs.get("a1bounds", [0, a1reps * self.magnitude[0]])
|
|
432
|
+
a2lims = kwargs.get("a2bounds", [0, a2reps * self.magnitude[1]])
|
|
433
|
+
a3lims = kwargs.get("a3bounds", [0, a3reps * self.magnitude[2]])
|
|
434
|
+
|
|
435
|
+
#Condition bounds
|
|
436
|
+
"""
|
|
437
|
+
Round up the number of lattice vectors to reach the max value, round down the number required for the minimum.
|
|
438
|
+
Then add 2 and subtract 2 respectively to add 2 unit cell of padding for the bounds. There will
|
|
439
|
+
be a second step that checks the coordinates themselves, so this is just to tell the for
|
|
440
|
+
loop how long to go.
|
|
441
|
+
"""
|
|
442
|
+
maxa1 = int(np.ceil(a1lims[1]/self.magnitude[0]))
|
|
443
|
+
maxa2 = int(np.ceil(a2lims[1]/self.magnitude[1]))
|
|
444
|
+
maxa3 = int(np.ceil(a3lims[1]/self.magnitude[2]))
|
|
445
|
+
|
|
446
|
+
mina1 = int(np.floor(a1lims[0]/self.magnitude[0]))
|
|
447
|
+
mina2 = int(np.floor(a2lims[0]/self.magnitude[1]))
|
|
448
|
+
mina3 = int(np.floor(a3lims[0]/self.magnitude[2]))
|
|
449
|
+
|
|
450
|
+
krange = range(mina1, maxa1)
|
|
451
|
+
mrange = range(mina2, maxa2)
|
|
452
|
+
prange = range(mina3, maxa3)
|
|
453
|
+
|
|
454
|
+
elif style=='radial':
|
|
455
|
+
krange = range(int(np.ceil(-a1reps)), int(np.ceil(a1reps)))
|
|
456
|
+
mrange = range(int(np.ceil(-a2reps)), int(np.ceil(a2reps)))
|
|
457
|
+
prange = range(int(np.ceil(-a3reps)), int(np.ceil(a3reps)))
|
|
458
|
+
startrad = kwargs.get("start_radius", 0)
|
|
459
|
+
radius = kwargs.get("radius", max(a1reps, a2reps))
|
|
460
|
+
else:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
xcen = centers[0]
|
|
465
|
+
ycen = centers[1]
|
|
466
|
+
zcen = centers[2]
|
|
467
|
+
|
|
468
|
+
newpoints = []
|
|
469
|
+
if style !='radial':
|
|
470
|
+
for k in krange: #a1 loop
|
|
471
|
+
for m in mrange: # a2 loop
|
|
472
|
+
for p in prange: # a3 loop
|
|
473
|
+
newx = xcen+(k*self.basis[0][0] + m*self.basis[1][0] + p*self.basis[2][0])
|
|
474
|
+
newy = ycen+(k*self.basis[0][1] + m*self.basis[1][1] + p*self.basis[2][1])
|
|
475
|
+
newz = zcen+(k*self.basis[0][2] + m*self.basis[1][2] + p*self.basis[2][2])
|
|
476
|
+
|
|
477
|
+
newvert = [newx, newy, newz]
|
|
478
|
+
|
|
479
|
+
if style != 'numbounded':
|
|
480
|
+
newpoints.append(newvert)
|
|
481
|
+
|
|
482
|
+
elif style == "numbounded":
|
|
483
|
+
if check_numbound(newvert, a1lims, a2lims, a3lims) == True:
|
|
484
|
+
newpoints.append(newvert)
|
|
485
|
+
else:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
else:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
return newpoints
|
|
492
|
+
|
|
493
|
+
def cubic_tiling(self, centers:list, a1reps:int, a2reps:int, a3reps:int, **kwargs):
|
|
494
|
+
"""
|
|
495
|
+
Tile unit cells in a way that gives a cubic or semi-cubic structure by limiting bounds.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
### Tiling Modifications ###
|
|
504
|
+
""" These are functions that apply the below modulations to the tiled lattice"""
|
|
505
|
+
|
|
506
|
+
def modulate_cells(self, structure, vortex_radius, winding_number, max_modulation, modulation_type='radius', whole_cell=True, plot_modulation = False, **kwargs ):
|
|
507
|
+
if debug == "on": print("Modulating Cells for %s" %(structure))
|
|
508
|
+
poslist = []
|
|
509
|
+
modlist = []
|
|
510
|
+
ax = kwargs.get("ax")
|
|
511
|
+
if (modulation_type=="radius") or (modulation_type=="balanced"):
|
|
512
|
+
if whole_cell == True:
|
|
513
|
+
"""This does discrete modifications of each supercell, not a continuous overlaid modulation"""
|
|
514
|
+
modulation = self.kekule_modulation(geo_object=structure, vortex_radius=vortex_radius, winding_number=winding_number, max_modulation=max_modulation, modulation_type=modulation_type,output_modulation= plot_modulation)
|
|
515
|
+
if modulation != None:
|
|
516
|
+
if plot_modulation == True:
|
|
517
|
+
if ax != None:
|
|
518
|
+
_plot_modulation(modulation, ax=ax)
|
|
519
|
+
else:
|
|
520
|
+
_plot_modulation(modulation)
|
|
521
|
+
poslist.append(modulation[0])
|
|
522
|
+
modlist.append(modulation[1])
|
|
523
|
+
print(modulation)
|
|
524
|
+
modlist.append(modulation)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
else:
|
|
528
|
+
"""This applies a gradient modification"""
|
|
529
|
+
cellcenter = structure.center
|
|
530
|
+
for n in structure.structures:
|
|
531
|
+
modulation = self.kekule_modulation(geo_object=n, vortex_radius=vortex_radius, winding_number=winding_number, max_modulation=max_modulation, modulation_type=modulation_type, cell_center = cellcenter, output_modulation= plot_modulation)
|
|
532
|
+
if modulation != None:
|
|
533
|
+
if plot_modulation == True:
|
|
534
|
+
if ax != None:
|
|
535
|
+
_plot_modulation(modulation, ax=ax)
|
|
536
|
+
else:
|
|
537
|
+
_plot_modulation(modulation)
|
|
538
|
+
poslist.append(modulation[0])
|
|
539
|
+
modlist.append(modulation[1])
|
|
540
|
+
|
|
541
|
+
elif modulation_type=="dual":
|
|
542
|
+
cellcenter = structure.center
|
|
543
|
+
for n in structure.structures:
|
|
544
|
+
modulation = self.dual_modulation(geo_object=n, vortex_radius=vortex_radius, winding_number=winding_number, max_modulation=max_modulation, cell_center = cellcenter, output_modulation= plot_modulation)
|
|
545
|
+
if modulation != None:
|
|
546
|
+
if plot_modulation == True:
|
|
547
|
+
if ax != None:
|
|
548
|
+
_plot_modulation(modulation, ax=ax)
|
|
549
|
+
else:
|
|
550
|
+
_plot_modulation(modulation)
|
|
551
|
+
poslist.append(modulation[0])
|
|
552
|
+
modlist.append(modulation[1])
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
### Modulation Methods ###
|
|
556
|
+
"""These should all act on a single object in a single position! This keeps them modular and easily integrated to tiling methods"""
|
|
557
|
+
|
|
558
|
+
def kekule_modulation(self, geo_object, vortex_radius, winding_number, max_modulation, modulation_type = 'radius', normalize = True, **kwargs):
|
|
559
|
+
position = geo_object.center
|
|
560
|
+
winding = winding_number
|
|
561
|
+
|
|
562
|
+
#this only matters for the case in which all modulations are radial only (e.g. r is constant)
|
|
563
|
+
cellcenter = kwargs.get("cell_center", None)
|
|
564
|
+
|
|
565
|
+
output_rmod = kwargs.get("output_modulation", False)
|
|
566
|
+
|
|
567
|
+
if normalize == True:
|
|
568
|
+
maxmod = max_modulation * self.magnitude
|
|
569
|
+
vortrad = vortex_radius * self.magnitude
|
|
570
|
+
|
|
571
|
+
else:
|
|
572
|
+
maxmod = max_modulation
|
|
573
|
+
vortrad = vortex_radius
|
|
574
|
+
|
|
575
|
+
if modulation_type == 'radius':
|
|
576
|
+
rmod = self._kekule_radius_mod(position, vortrad, winding, maxmod)
|
|
577
|
+
oldrad = geo_object.radius
|
|
578
|
+
newrad = oldrad + rmod
|
|
579
|
+
if newrad > 0:
|
|
580
|
+
geo_object.radius = newrad
|
|
581
|
+
elif newrad <= 0:
|
|
582
|
+
geo_object.radius = 0
|
|
583
|
+
print("negative radius calculated. Check Units!")
|
|
584
|
+
if debug == "on": print(geo_object, " is now radius ", newrad)
|
|
585
|
+
|
|
586
|
+
if modulation_type == 'balanced':
|
|
587
|
+
absolute_position = geo_object.center.tolist()
|
|
588
|
+
in_cell_position = np.asarray(geo_object.center) - np.asarray(cellcenter)
|
|
589
|
+
if debug == 'on': print("The relative position of the structure is", position)
|
|
590
|
+
|
|
591
|
+
rmod = self._kekule_radius_mod(position, vortrad, winding, maxmod, in_cell_pos = in_cell_position)
|
|
592
|
+
oldrad = geo_object.radius
|
|
593
|
+
newrad = oldrad + rmod
|
|
594
|
+
if newrad > 0:
|
|
595
|
+
geo_object.radius = newrad
|
|
596
|
+
elif newrad <= 0:
|
|
597
|
+
geo_object.radius = 0
|
|
598
|
+
print("negative radius calculated. Check Units!")
|
|
599
|
+
if debug == "on": print(geo_object, " is now radius ", newrad)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
elif modulation_type == 'position':
|
|
603
|
+
print("Error: position modulation is not implemented yet.")
|
|
604
|
+
|
|
605
|
+
if output_rmod == True:
|
|
606
|
+
return (absolute_position, rmod, maxmod)
|
|
607
|
+
|
|
608
|
+
def dual_modulation(self, geo_object, vortex_radius,
|
|
609
|
+
winding_number, max_modulation, normalize=True, **kwargs):
|
|
610
|
+
position = geo_object.center
|
|
611
|
+
winding = winding_number
|
|
612
|
+
if normalize == True:
|
|
613
|
+
maxmod = max_modulation * self.magnitude
|
|
614
|
+
vortrad = vortex_radius * self.magnitude
|
|
615
|
+
|
|
616
|
+
else:
|
|
617
|
+
maxmod = max_modulation
|
|
618
|
+
vortrad = vortex_radius
|
|
619
|
+
#this only matters for the case in which all modulations are radial only (e.g. r is constant)
|
|
620
|
+
cellcenter = kwargs.get("cell_center", None)
|
|
621
|
+
output_rmod = kwargs.get("output_modulation", False)
|
|
622
|
+
|
|
623
|
+
absolute_position = geo_object.center.tolist()
|
|
624
|
+
in_cell_position = np.asarray(geo_object.center) - np.asarray(cellcenter)
|
|
625
|
+
|
|
626
|
+
center_distance = np.linalg.norm(in_cell_position)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
if debug == 'on': print("The relative position of the structure is", position)
|
|
630
|
+
|
|
631
|
+
posmod, rmod = self._dual_symmetry_mod(position, vortrad, winding, maxmod, in_cell_pos = in_cell_position)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
#Will need to do this for position, but the delta affects d, which isn't carried by the geometry object. So I might have to recalculate it. The effect of the modulation is "d= a0/3 - delta_t" That's the same as subtracting that delta t term from my existing d term. Since these objects won't inherit the d term, I need to reformulate this w.r.t. their coordinates. A little distributive property shows that a positive or negative delta_t*(x,y)/(a0/3) could be added or subtracted.
|
|
635
|
+
|
|
636
|
+
oldrad = geo_object.radius
|
|
637
|
+
newrad = oldrad + rmod
|
|
638
|
+
oldpos = np.asarray(geo_object.center)
|
|
639
|
+
newpos = in_cell_position + (posmod/center_distance)*in_cell_position + np.asarray(cellcenter)
|
|
640
|
+
if debug == 'on':
|
|
641
|
+
print("The new position would be: ", newpos)
|
|
642
|
+
print("The old position was: ", oldpos)
|
|
643
|
+
print("\n")
|
|
644
|
+
|
|
645
|
+
if newrad > 0:
|
|
646
|
+
geo_object.radius = newrad
|
|
647
|
+
geo_object.center = newpos
|
|
648
|
+
|
|
649
|
+
elif newrad <= 0:
|
|
650
|
+
geo_object.radius = 0
|
|
651
|
+
geo_object.center = newpos
|
|
652
|
+
print("negative radius calculated. Check Units!")
|
|
653
|
+
|
|
654
|
+
if debug == "on": print(geo_object, " is now radius ", newrad)
|
|
655
|
+
|
|
656
|
+
if output_rmod == True:
|
|
657
|
+
return (absolute_position, rmod, maxmod)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
### Modulation Core Methods ###
|
|
661
|
+
"""
|
|
662
|
+
These are the actual equations for performing the modulation. They should return some parameter that gets passed to the above Modulation Methods, which will then apply that parameter to the structure.
|
|
663
|
+
|
|
664
|
+
e.g. _kekule_radius_mod returns a delta radius term, that kekule_modulation adds to the radius of a cylinder.
|
|
665
|
+
"""
|
|
666
|
+
def _kekule_radius_mod(self, position, vortrad, winding, maxmod, **kwargs):
|
|
667
|
+
cellpos = kwargs.get("in_cell_pos", None) #position of structure relative to cell center
|
|
668
|
+
|
|
669
|
+
#Next, check if the structure's cartesian position will be specified relative to the center of the supercell or be set to the same as the absolute positition (relative to center of vortex at 0,0)
|
|
670
|
+
try:
|
|
671
|
+
if cellpos.any != None:
|
|
672
|
+
cartpos = cellpos[:2]
|
|
673
|
+
print("Using in-cell-position: ", cartpos)
|
|
674
|
+
else:
|
|
675
|
+
cartpos = position[:2]
|
|
676
|
+
except AttributeError:
|
|
677
|
+
if cellpos != None:
|
|
678
|
+
cartpos = cellpos[:2]
|
|
679
|
+
print("Using in-cell-position: ", cartpos)
|
|
680
|
+
else:
|
|
681
|
+
cartpos = position[:2]
|
|
682
|
+
|
|
683
|
+
if debug == "on": print("cartpos = ", cartpos)
|
|
684
|
+
#Generate the polar coordinates of the absolute position
|
|
685
|
+
polpos = vm.cart_to_pol(position)
|
|
686
|
+
rho, theta = polpos[0], polpos[1]
|
|
687
|
+
|
|
688
|
+
#Create delta R modulation term from rho (polar distance)
|
|
689
|
+
delR_term = maxmod*np.tanh(rho/vortrad)
|
|
690
|
+
if debug == "on": print("delR_term = ", delR_term)
|
|
691
|
+
#The K+ and K - points get summed to Ktot, whose dot product with the cartesian coordinate (relative or absolute) selects for the two sublattices or the center pillar (+, -, 0).
|
|
692
|
+
KPlus = np.array([4*np.pi/(3*self.latt_const), 0])
|
|
693
|
+
KMinus = np.array([-4*np.pi/(3*self.latt_const), 0])
|
|
694
|
+
Ktot = KPlus - KMinus
|
|
695
|
+
if debug == "on": print("lat: Ktot = ", Ktot)
|
|
696
|
+
costerm = np.cos(np.dot(Ktot, cartpos) + (winding*theta))
|
|
697
|
+
if debug == "only_dot": print(f"lat: dot product = {np.dot(Ktot, cartpos)}" )
|
|
698
|
+
if debug == "on": print("costerm = ", costerm)
|
|
699
|
+
RMod = delR_term[0] * costerm
|
|
700
|
+
if debug == "on": print("RMod = ", RMod)
|
|
701
|
+
return RMod
|
|
702
|
+
|
|
703
|
+
def _dual_symmetry_mod(self, position, vortrad, winding, maxmod, alpha=4, **kwargs):
|
|
704
|
+
"""
|
|
705
|
+
This is a low-level modulation function using the scheme of Jingwen Ma et al. The modulation is basically a simultaneous spin-hall and valley-hall perturbation, where the lattice is contracted/expanded while the C6 symmetry is broken to two C3 sublattices.
|
|
706
|
+
|
|
707
|
+
The paper defines two modulation terms: delta_t and delta_i (dt, di) with some magnitude d0. These terms come from d0(alpha*sin(theta), cos(theta)). The alpha gets set to .65 for positive dt and .33 for negative dt.
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
cellpos = kwargs.get("in_cell_pos", None)
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
if cellpos.any != None:
|
|
716
|
+
cartpos = cellpos[:2]
|
|
717
|
+
else:
|
|
718
|
+
cartpos = position[:2]
|
|
719
|
+
except AttributeError:
|
|
720
|
+
if cellpos != None:
|
|
721
|
+
cartpos = cellpos[:2]
|
|
722
|
+
else:
|
|
723
|
+
cartpos = position[:2]
|
|
724
|
+
|
|
725
|
+
pos_alpha = .65
|
|
726
|
+
neg_alpha = .33
|
|
727
|
+
polpos = vm.cart_to_pol(position)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
rho, theta = polpos[0], polpos[1]
|
|
731
|
+
|
|
732
|
+
delta_0 = maxmod*(np.tanh(rho/vortrad)**alpha)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
"""
|
|
736
|
+
There needs to be a way to select the two sublattices.
|
|
737
|
+
Sublattice 1:
|
|
738
|
+
[0, -d]
|
|
739
|
+
[-d*sqrt(3)/2, d/2]
|
|
740
|
+
[d*sqrt(3)/2, d/2]
|
|
741
|
+
|
|
742
|
+
Sublattice 2:
|
|
743
|
+
[0, d]
|
|
744
|
+
[d*sqrt(3)/2, -d/2]
|
|
745
|
+
[-d*sqrt(3)/2, -d/2]
|
|
746
|
+
|
|
747
|
+
if x = 0 and y = - ...
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
"""
|
|
751
|
+
|
|
752
|
+
if (cartpos[0] > 0) and (cartpos[1] >= 0):
|
|
753
|
+
lattice_selector = 1
|
|
754
|
+
elif (cartpos[0] < 0) and (cartpos[1] >= 0):
|
|
755
|
+
lattice_selector = 1
|
|
756
|
+
elif (np.isclose(cartpos[0], 0)) and (cartpos[1] < 0):
|
|
757
|
+
lattice_selector = 1
|
|
758
|
+
elif (np.isclose(cartpos[0], 0)) and (cartpos[1] > 0):
|
|
759
|
+
lattice_selector = -1
|
|
760
|
+
elif (cartpos[0] > 0) and (cartpos[1] < 0):
|
|
761
|
+
lattice_selector = -1
|
|
762
|
+
elif (cartpos[0] < 0) and (cartpos[1] < 0):
|
|
763
|
+
lattice_selector = -1
|
|
764
|
+
else:
|
|
765
|
+
print(cartpos)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
delta_t_pre = delta_0[0]*np.sin((winding*theta))
|
|
770
|
+
|
|
771
|
+
if delta_t_pre <= 0:
|
|
772
|
+
delta_t = delta_t_pre*neg_alpha
|
|
773
|
+
elif delta_t_pre >=0:
|
|
774
|
+
delta_t = delta_t_pre*pos_alpha
|
|
775
|
+
|
|
776
|
+
delta_i = delta_0[0] * np.cos((winding*theta))
|
|
777
|
+
|
|
778
|
+
if lattice_selector >= 0:
|
|
779
|
+
sizemod = delta_i
|
|
780
|
+
|
|
781
|
+
elif lattice_selector < 0:
|
|
782
|
+
sizemod = -1*delta_i
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
return [delta_t, sizemod]
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
|