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,27 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ try:
4
+ __version__ = version("crystalbuilder")
5
+ except PackageNotFoundError:
6
+ __version__ = "unknown"
7
+
8
+ __all__ = ["convert", "geometry", "lattice", "vectors","bilbao", "viewer", "conversions", "conversions"]
9
+
10
+
11
+
12
+
13
+ """
14
+ CrystalBuilder allows building triangle and cylinder based photonic crystal lattices for MEEP, MPB, and Tidy3D.
15
+
16
+ convert.py: methods for converting CrystalBuilder objects to MEEP/MPB/Tidy3D geometries
17
+
18
+ geometry.py: geometry classes. These are not actually "meshed" structures, but vertices/centers as required by the simulation program. e.g. a "triangle" object is defined by vertices in MEEP and Tidy3D so it is also defined that way in CrystalBuilder.geometry. There are additional methods in the geometry program that allow for defining with a center+size, but under the hood it still just calculates vertices.
19
+
20
+ lattice.py: the lattice class contains the basis vector information, and the methods for tiling the geometry objects. kekule modulation is also here.
21
+
22
+ vectors.py: methods for rotating/shifting/scaling vectors defined as numpy arrays or simple lists. This probably won't need to be called by users, but is integral for all the other packages.
23
+
24
+ bilbao.py: methods for retrieving space group information from Bilbao servers
25
+
26
+ viewer.py: methods for visualizing structures
27
+ """
@@ -0,0 +1,297 @@
1
+
2
+ import re
3
+ import os
4
+
5
+ location = os.path.dirname(os.path.realpath(__file__))
6
+ resources = os.path.join(location, 'resources')
7
+
8
+ import requests
9
+ import numpy as np
10
+ from bs4 import BeautifulSoup
11
+ import math
12
+ import json
13
+
14
+
15
+ def check_resource(filename):
16
+ if os.path.exists(filename):
17
+ with open(filename) as f:
18
+ kvec_dict = json.load(f)
19
+ return kvec_dict
20
+ else:
21
+ return False
22
+
23
+ def create_resource(filename, dictionary):
24
+ for key, value in dictionary.items():
25
+ if isinstance(value, np.ndarray):
26
+ dictionary[key] = value.tolist()
27
+ with open(filename, 'w') as f:
28
+ json.dump(dictionary, f, indent=3)
29
+
30
+ def get_kvectors(groupnum, dict_out=False):
31
+
32
+ kvec_array = [] #will convert coordinates to array
33
+ kvec_dictionary = {} ## for converting both symbols and coordinates to formatted dictionary
34
+
35
+ file = os.path.join(resources, f'{groupnum}-kvec.json')
36
+ localkdict = check_resource(file)
37
+ if localkdict:
38
+ for key, k in localkdict.items():
39
+ kvec_array.append(k)
40
+ save_file = False
41
+ kvec_dictionary = localkdict
42
+ else:
43
+ save_file = True
44
+ URL = "https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-kv-list"
45
+ page = requests.post(URL, data={'gnum': str(groupnum),'standard':'Optimized listing of k-vector types using ITA description'})
46
+ soup = BeautifulSoup(page.content, "html.parser")
47
+ kvec_table = soup.find_all('table')[1]
48
+ rows = kvec_table('tr')[2:]
49
+ raw_kvec_dict = {}
50
+ for row in rows:
51
+ sympoint = row.find_all('td')[0].get_text() #first cell has symbol/letter
52
+ coordstring = row.find_all('td')[1].get_text() #next cell has the coordinates
53
+ coord = coordstring.split(',') #split the kvec into components
54
+ raw_kvec_dict[sympoint] = coord # create dictionary from symbol and coordinate
55
+
56
+ for key, n in raw_kvec_dict.items():
57
+ if len(n) == 3: #Make sure we have (kx,ky,kz)
58
+ point = [] ## container for the 3 coordinates
59
+ for index, k in enumerate(n): ## iterate through all the points
60
+ k= re.split(r'\b\D\b', k) ### remove blanks, letters, and slashes from division signs.
61
+ try:
62
+ coordinate = float(k[0])/float(k[1])
63
+ except IndexError:
64
+ coordinate = float(k[0])
65
+ except ValueError:
66
+ try:
67
+ coordinate = float(k[0])
68
+ except ValueError:
69
+ coordinate = 1
70
+ pass
71
+ pass
72
+ point.append(coordinate)
73
+ kxkykz = np.reshape(np.array(point),(3))
74
+ kvec_array.append(kxkykz)
75
+
76
+ kvec_dictionary[key] = kxkykz
77
+
78
+
79
+ kvec_array = np.reshape(np.asarray(kvec_array), (-1,3))
80
+
81
+ if save_file == True:
82
+ create_resource(file, kvec_dictionary)
83
+
84
+ if dict_out == True:
85
+ return kvec_dictionary
86
+ else:
87
+ return kvec_array
88
+
89
+ def get_genmat(groupnum):
90
+ """ Retrieve generator matrices
91
+
92
+ Parameters
93
+ -----------
94
+ groupnum : int
95
+ One of 230 numbered space groups in the IUCr
96
+
97
+ Returns
98
+ --------
99
+ matrix_list : list
100
+ list of matrices representing general positions
101
+
102
+ """
103
+
104
+ matrix_list = [] #will convert coordinates to array
105
+ gen_dictionary = {} ## for converting both symbols and coordinates to formatted dictionary
106
+
107
+ file = os.path.join(resources, f'{groupnum}-generators.json')
108
+ localgendict = check_resource(file)
109
+ if localgendict:
110
+ for key, k in localgendict.items():
111
+ matrix = k
112
+ matrix_list.append(k)
113
+ save_file = False
114
+ else:
115
+ save_file = True
116
+ URL = "https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-getgen"
117
+ page = requests.post(URL, data={'gnum': str(
118
+ groupnum), 'what': 'gp', 'list': 'Standard/Default+Setting'})
119
+ gen_pos = BeautifulSoup(page.content, "html.parser")
120
+ holder = gen_pos.find_all("pre")
121
+
122
+ matrix_text = []
123
+ for k in holder:
124
+ matrix_text.append(k.get_text()) #get text from table
125
+
126
+ for f in range(0, len(matrix_text)):
127
+ genpos_line = matrix_text[f].split('\n') #separate the matrix text based on newline, giving 3x4 matrix of strings
128
+
129
+ genpos_matrix = np.array([]).reshape(0,4) #create an empty numpy array to "append" each row of the matrix to
130
+
131
+ for row_string in genpos_line:
132
+ row_list = list(filter(None, row_string.split(' '))) # generate a list of strings for each row.
133
+ row_elements = [] ## create list to store the elements as floats
134
+
135
+ for element in row_list: #go through and convert string elements to floating point ones
136
+ try:
137
+ matrix_value = float(element) #try simply converting
138
+ except ValueError:
139
+ elem_split = element.split('/') #if the simple conversion doesn't work, it's likely because it's written as a fraction
140
+ matrix_value = float(elem_split[0])/float(elem_split[1]) #calculate the float from the fraction
141
+
142
+
143
+ row_elements.append(matrix_value) #Put the element in a list with the others in the same row
144
+
145
+ row_elements = np.asarray(row_elements) #convert to numpy array
146
+ genpos_matrix = np.vstack([genpos_matrix, row_elements]) #add the below our existing row
147
+
148
+ matrix_list.append(genpos_matrix) #After iterating through the rows of the matrix, put the matrix in the list
149
+
150
+ if save_file == True:
151
+ matdict = {}
152
+ for key, value in enumerate(matrix_list):
153
+ matdict[key] = value
154
+
155
+ create_resource(file, matdict)
156
+
157
+ return matrix_list
158
+
159
+ def get_coordinates(groupnum, origin, output_array=True):
160
+ """ Generates positions from specified origin and generator matrices
161
+
162
+ Parameters
163
+ -----------
164
+ groupnum : int
165
+ One of 230 numbered space groups in the IUCr
166
+
167
+ origin : list
168
+ Any point that should be used as (x,y,z) for symmetry operations from the generator matrices
169
+
170
+ output_array : bool
171
+ Oututs numpy array by default (True), since the result should be an m x 3 matrix with m being the number of generator matrices. If False, the output is a list of lists.
172
+
173
+ Returns
174
+ --------
175
+ coordinates : list, array
176
+ Returns an array if output_array is True (default), returns a list object otherwise.
177
+
178
+
179
+
180
+ """
181
+
182
+ position_vector = np.array([origin[0], origin[1], origin[2]]).reshape(3,1)
183
+ matrix_list = get_genmat(groupnum)
184
+ coordinate_list = []
185
+ coordinate_array = np.array([]).reshape(0,3)
186
+ for n in matrix_list:
187
+ n = np.asarray(n)
188
+ linear_part, translation_part = np.split(n, [3,], axis=1) #Split matrix into linear part and translation part, *after* third element in row
189
+ #linear_part is 3x3, translation_part is 3x1
190
+ linear_product = np.matmul(linear_part, position_vector) #matrix part
191
+ transformation = linear_product + translation_part #affine transformation
192
+ new_point = transformation.reshape(1,3) #make row matrix
193
+ if ((new_point.all() <= 1) and (new_point.all() >= 0)):
194
+ if output_array==True:
195
+ coordinate_array = np.concatenate([coordinate_array, new_point], axis=0)
196
+ else:
197
+ coordinate_list.append(new_point.tolist()) #add point to list
198
+ else:
199
+ continue
200
+
201
+
202
+ if output_array == True:
203
+ return coordinate_array
204
+ else:
205
+ return coordinate_list
206
+
207
+ class SpaceGroup():
208
+ """
209
+ One of 230 space groups
210
+
211
+ This class will have the properties necessary for each space group, as pulled from the Bilbao database.
212
+
213
+ Includes:
214
+ k-vectors with labels
215
+ generator matrices
216
+ generation of equivalent points
217
+ """
218
+
219
+ def __init__(
220
+ self,
221
+ group_number,
222
+ **kwargs
223
+ ) -> None:
224
+
225
+ """
226
+ Create the instance of a Space Group
227
+
228
+ Parameters
229
+ -----------
230
+ group_number : int
231
+ 1-230, corresponding to IUCr and Bilbao server notation.
232
+
233
+ kwargs
234
+ -------
235
+ points : list, ndarray
236
+ Initial coordinates that will be operated on by the symmetry operations to create the entire unit cell.
237
+
238
+ """
239
+
240
+ self.point_list = kwargs.get("points", None)
241
+ self.group_num = group_number
242
+
243
+ self.kvec_dict = get_kvectors(self.group_num, dict_out=True)
244
+ self.kvec_arr = get_kvectors(self.group_num, dict_out=False)
245
+
246
+ self.generator_matrices = get_genmat(self.group_num)
247
+
248
+ self.generated_points = self.calculate_points(self.point_list)
249
+
250
+ def calculate_points(self, point_list):
251
+ """
252
+ Return a list of coordinates resulting from symmetry operations to each point in `point_list`. This is called once if the `SpaceGroup` is initialized with the `points` kwarg.
253
+ It can be called any number of times to directly return points from new `point_list` inputs.
254
+
255
+ Parameter
256
+ ----------
257
+ point_list : tuple, list, ndarray
258
+ point(s) on which to perform symmetry operations
259
+
260
+ Return
261
+ -------
262
+ generated_points : ndarray
263
+ Unique points resulting from the symmetry operations on points in point_list. This includes negative values and values greater than 1 (outside the primitive cell).
264
+ """
265
+ generated_points = np.array([]).reshape(-1,3)
266
+ if point_list is not None:
267
+ if isinstance(point_list, (list, np.ndarray)):
268
+ for n in point_list:
269
+ newpoint = get_coordinates(self.group_num, origin=n)
270
+ generated_points = np.vstack((generated_points, newpoint))
271
+ generated_points.reshape(-1,3)
272
+ generated_points = np.unique(generated_points, axis=0)
273
+
274
+ else:
275
+ generated_points = get_coordinates(self.group_num, origin=point_list)
276
+ generated_points.reshape(-1,3)
277
+ generated_points = np.unique(generated_points, axis=0)
278
+
279
+ return generated_points
280
+ else:
281
+ pass
282
+
283
+
284
+ if __name__ == "__main__":
285
+ from matplotlib import pyplot as plt
286
+
287
+
288
+ # crystest = SpaceGroup(227)
289
+ # pointlist = crystest.calculate_points([(0,0,0)])
290
+ # print(pointlist)
291
+ # print(pointlist.shape)
292
+
293
+ # fig = plt.figure()
294
+ # ax = fig.add_subplot(projection='3d')
295
+
296
+ # ax.scatter(pointlist[:, 0], pointlist[:, 1], pointlist[:,2])
297
+ # plt.show()
@@ -0,0 +1,36 @@
1
+ import numpy as np
2
+ from crystalbuilder import lattice as lat
3
+ from crystalbuilder import geometry as geo
4
+
5
+ try:
6
+ import lumpy.simobjects as so
7
+ except ModuleNotFoundError:
8
+ print("Error: Lumpy and/or the Lumerical API were not found.")
9
+
10
+
11
+ debug = 'trace' #trace = 3, debug = 2, info = 1, none = 0
12
+
13
+ def debug_msg(string, level):
14
+ debug_levels = {'trace':3, 'debug':2, 'info': 1, 'none':0}
15
+ req_deb = debug_levels[debug]
16
+ if level <= req_deb:
17
+ print(string)
18
+
19
+ def flatten(list):
20
+ """ Some of these methods can accidentally create nested lists, so this function can be used in try statements to correct those """
21
+ try:
22
+ flat_list = [item for sublist in list for item in sublist]
23
+ except:
24
+ flat_list = list
25
+ return flat_list
26
+
27
+ def convert_cylinder(Cylinder, material='dielectric', index=1.5):
28
+ axis = Cylinder.axis
29
+ lumCyl = so.Cylinder(radius=Cylinder.radius, height=Cylinder.height, center=tuple(flatten(Cylinder.center)), material=material, index=index, orientation=axis)
30
+ debug_msg(lumCyl.out(), 3)
31
+ return lumCyl
32
+
33
+ def convert_prism(Prism, material='dielectric', index=1.5):
34
+ verts = Prism.vertices[:, 0:2]
35
+ lumPrism = so.Prism(vertices=verts, z_span=Prism.height, center=tuple(flatten(Prism.center)), material=material, index=index)
36
+ return lumPrism
@@ -0,0 +1,179 @@
1
+
2
+ import numpy as np
3
+ from tidy3d import Transformed, Structure, GeometryGroup, Cylinder, Medium, Simulation, PointDipole, C_0, GridSpec, GaussianPulse
4
+ try:
5
+ from tidy3d import Transformed, Structure, GeometryGroup, Cylinder, Medium, Simulation, PointDipole, C_0, GridSpec, GaussianPulse
6
+ except ModuleNotFoundError:
7
+ pass
8
+
9
+ from crystalbuilder import geometry as geo
10
+ debug = "off"
11
+
12
+ def unpack_supercell(supercell):
13
+ """Turns supercell into a list of geometry objects
14
+
15
+ Parameters
16
+ ----------
17
+ supercell : gm.SuperCell()
18
+ A SuperCell object from geometry.py
19
+
20
+ Returns
21
+ -------
22
+ [structures]: list
23
+ list of the geometry objects in SuperCell
24
+
25
+ """
26
+
27
+ structures = supercell.structures
28
+ return structures
29
+
30
+ def flatten(list):
31
+ """ Some of these methods can accidentally create nested lists, so this function can be used in try statements to correct those """
32
+ try:
33
+ if isinstance(list, list):
34
+ flat_list = [item for sublist in list for item in sublist]
35
+ except:
36
+ flat_list = list
37
+ return flat_list
38
+
39
+ def rotate_to(orientation, v1 = [0,0,1]):
40
+ """
41
+ Create a quaternion to rotate the original vector to the specified `orientation`. By default, use a Z unit vector
42
+ """
43
+
44
+ orientation = np.asarray(orientation)/np.linalg.norm(orientation)
45
+ v1 = np.asarray(v1)/np.linalg.norm(v1)
46
+ eyemat = np.identity(3)
47
+
48
+
49
+ rot_axis_unnorm = np.cross(v1, orientation)
50
+ s_angle = np.linalg.norm(rot_axis_unnorm)
51
+ rot_axis = rot_axis_unnorm/s_angle
52
+ c_angle = np.dot(v1, orientation)
53
+ rotmat2 = Transformed.rotation(s_angle, (rot_axis[0], rot_axis[1], rot_axis[2]))
54
+
55
+ # print(s_angle)
56
+ # print(c_angle)
57
+
58
+ skew_mat = np.array(
59
+ ((0, -rot_axis[2], rot_axis[1]),
60
+ (rot_axis[2], 0, -rot_axis[0]),
61
+ (-rot_axis[1], rot_axis[0], 0))
62
+ )
63
+
64
+ rot_mat = eyemat + (s_angle * skew_mat) + ((1-c_angle)*(skew_mat@skew_mat))
65
+ #Tidy3D takes an affine transformation matrix. So we'll pad it to 4x4 with zeros and a 1 in the bottom corner
66
+
67
+ rot_mat = np.pad(rot_mat, (0,1))
68
+ rot_mat[3,3] = 1
69
+ # rot_mat = rotmat2
70
+ return rot_mat
71
+
72
+ def _convert_cyl(geometry_object, material):
73
+ """ Put the structure at the origin, do the rotation, then shift it to the correct spot."""
74
+ m = geometry_object
75
+ rot_mat = rotate_to(m.axis)
76
+ shift_center = flatten(m.center)
77
+ rot_mat[:3, -1] = shift_center
78
+ tdgeom = Transformed(geometry = Cylinder(radius=m.radius, axis= 2, length=m.height, center=[0,0,0]), transform=rot_mat)
79
+ return tdgeom
80
+
81
+ def _geo_to_tidy3d(geometry_object, material, **kwargs):
82
+ """Converts geometry object (or supercell) to the Tidy3D equivalent. Note that Tidy3D values always include units (microns by default).
83
+
84
+ Tidy3D geometries are combined with a specified medium to create a Tidy3D structure object, which can be given a unique name. For now, the naming will be systematic. This might be changed in the future via kwargs.
85
+
86
+ The material assignment will occur after all of the geometries have been made. This means a td.GeometryGroup object will be created and made into a structure.
87
+ """
88
+
89
+ geom_list = []
90
+ try:
91
+ for m in geometry_object:
92
+ if isinstance(m, geo.SuperCell):
93
+ if debug=="on": print("This is running the iterable Supercell")
94
+ innerlist = _geo_to_tidy3d(m, material)
95
+ geom_list.append(innerlist)
96
+
97
+ elif isinstance(m, geo.Cylinder):
98
+ if debug=="on": print("This is running the iterable cylinder")
99
+ tdgeom = _convert_cyl(m, material)
100
+ geom_list.append(tdgeom)
101
+
102
+ except TypeError:
103
+ if isinstance(geometry_object, geo.SuperCell):
104
+ if debug=="on": print("This is running the single Supercell")
105
+ structs = unpack_supercell(geometry_object)
106
+ m = structs
107
+ newlist = _geo_to_tidy3d(m, material)
108
+ geom_list.append(newlist)
109
+
110
+ elif isinstance(geometry_object, geo.Cylinder):
111
+ m = geometry_object
112
+ if debug=="on": print("This is creating a single cylinder named")
113
+ tdgeom = _convert_cyl(m, material)
114
+ geom_list.append(tdgeom)
115
+
116
+ return geom_list
117
+
118
+ def geo_to_tidy3d(geometry_object, material, name="Structure Group", **kwargs):
119
+ """Converts CrystalBuilder geometry object(s) to the corresponding Tidy3D object(s) with defined medium.
120
+
121
+
122
+ `material` can be either a td.Medium() object or a float corresponding to the refractive index of the desired Medium.
123
+
124
+ This is a higher level wrapper of the _geo_to_tidy3d function, which I have yet to document
125
+
126
+ Parameters
127
+ ------------
128
+ geometry_object : Geometry or list of Geometry
129
+ an object or list of objects
130
+ material : td.Medium() or float
131
+ Tidy3D Medium or the refractive index that will be assigned to the material.
132
+
133
+
134
+
135
+ Returns
136
+ ------------
137
+ td.Structure()
138
+ a Tidy3D structure group with defined Medium
139
+
140
+ """
141
+ material_name = kwargs.get("material_name", "Dielectric Material")
142
+ geometry_list_raw = _geo_to_tidy3d(geometry_object, material)
143
+ geometry_list =geometry_list_raw
144
+ geometry_group = GeometryGroup(geometries = tuple(geometry_list))
145
+ if isinstance(material, Medium):
146
+ medium = material
147
+ else:
148
+ medium = Medium(permittivity = material**2, name=material_name)
149
+
150
+ return Structure(geometry=geometry_group, medium=medium, name=name)
151
+
152
+ if __name__ == '__main__':
153
+ """testing code"""
154
+
155
+ cylinder = geo.Cylinder.from_vertices([[0,0,0], [3,0,0]], radius=.1)
156
+ newgeo = geo_to_tidy3d([cylinder, cylinder], material=3)
157
+
158
+ def view_structures(geometry):
159
+ # create source
160
+ lda0 = 0.75 # wavelength of interest (length scales are micrometers in Tidy3D)
161
+ freq0 = C_0 / lda0 # frequency of interest
162
+ source = PointDipole(
163
+ center=(-1.5, 0, 0), # position of the dipole
164
+ source_time=GaussianPulse(freq0=freq0, fwidth=freq0 / 10.0), # time profile of the source
165
+ polarization="Ey", # polarization of the dipole
166
+ )
167
+
168
+ sim = Simulation(
169
+ size=(5, 5, 5), # simulation domain size
170
+ grid_spec=GridSpec.auto(
171
+ min_steps_per_wvl=25
172
+ ), # automatic nonuniform FDTD grid with 25 grids per wavelength in the material
173
+ structures=[geometry],
174
+ sources=[source],
175
+ run_time=3e-13, # physical simulation time in second
176
+ )
177
+ sim.plot_3d()
178
+
179
+ view_structures(newgeo)