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.

@@ -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
+