pyTEMlib 0.2025.4.2__py3-none-any.whl → 0.2025.9.1__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 pyTEMlib might be problematic. Click here for more details.

Files changed (94) hide show
  1. build/lib/pyTEMlib/__init__.py +33 -0
  2. build/lib/pyTEMlib/animation.py +640 -0
  3. build/lib/pyTEMlib/atom_tools.py +238 -0
  4. build/lib/pyTEMlib/config_dir.py +31 -0
  5. build/lib/pyTEMlib/crystal_tools.py +1219 -0
  6. build/lib/pyTEMlib/diffraction_plot.py +756 -0
  7. build/lib/pyTEMlib/dynamic_scattering.py +293 -0
  8. build/lib/pyTEMlib/eds_tools.py +826 -0
  9. build/lib/pyTEMlib/eds_xsections.py +432 -0
  10. build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
  11. build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  12. build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
  13. build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  14. build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  15. build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  16. build/lib/pyTEMlib/file_reader.py +274 -0
  17. build/lib/pyTEMlib/file_tools.py +811 -0
  18. build/lib/pyTEMlib/get_bote_salvat.py +69 -0
  19. build/lib/pyTEMlib/graph_tools.py +1153 -0
  20. build/lib/pyTEMlib/graph_viz.py +599 -0
  21. build/lib/pyTEMlib/image/__init__.py +37 -0
  22. build/lib/pyTEMlib/image/image_atoms.py +270 -0
  23. build/lib/pyTEMlib/image/image_clean.py +197 -0
  24. build/lib/pyTEMlib/image/image_distortion.py +299 -0
  25. build/lib/pyTEMlib/image/image_fft.py +277 -0
  26. build/lib/pyTEMlib/image/image_graph.py +926 -0
  27. build/lib/pyTEMlib/image/image_registration.py +316 -0
  28. build/lib/pyTEMlib/image/image_utilities.py +309 -0
  29. build/lib/pyTEMlib/image/image_window.py +421 -0
  30. build/lib/pyTEMlib/image_tools.py +699 -0
  31. build/lib/pyTEMlib/interactive_image.py +1 -0
  32. build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
  33. build/lib/pyTEMlib/microscope.py +61 -0
  34. build/lib/pyTEMlib/probe_tools.py +906 -0
  35. build/lib/pyTEMlib/sidpy_tools.py +153 -0
  36. build/lib/pyTEMlib/simulation_tools.py +104 -0
  37. build/lib/pyTEMlib/test.py +437 -0
  38. build/lib/pyTEMlib/utilities.py +314 -0
  39. build/lib/pyTEMlib/version.py +5 -0
  40. build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
  41. pyTEMlib/__init__.py +25 -3
  42. pyTEMlib/animation.py +31 -22
  43. pyTEMlib/atom_tools.py +29 -34
  44. pyTEMlib/config_dir.py +2 -28
  45. pyTEMlib/crystal_tools.py +129 -165
  46. pyTEMlib/eds_tools.py +559 -342
  47. pyTEMlib/eds_xsections.py +432 -0
  48. pyTEMlib/eels_tools/__init__.py +44 -0
  49. pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  50. pyTEMlib/eels_tools/eels_database.py +134 -0
  51. pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  52. pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  53. pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  54. pyTEMlib/file_reader.py +274 -0
  55. pyTEMlib/file_tools.py +260 -1130
  56. pyTEMlib/get_bote_salvat.py +69 -0
  57. pyTEMlib/graph_tools.py +101 -174
  58. pyTEMlib/graph_viz.py +150 -0
  59. pyTEMlib/image/__init__.py +37 -0
  60. pyTEMlib/image/image_atoms.py +270 -0
  61. pyTEMlib/image/image_clean.py +197 -0
  62. pyTEMlib/image/image_distortion.py +299 -0
  63. pyTEMlib/image/image_fft.py +277 -0
  64. pyTEMlib/image/image_graph.py +926 -0
  65. pyTEMlib/image/image_registration.py +316 -0
  66. pyTEMlib/image/image_utilities.py +309 -0
  67. pyTEMlib/image/image_window.py +421 -0
  68. pyTEMlib/image_tools.py +154 -928
  69. pyTEMlib/kinematic_scattering.py +1 -1
  70. pyTEMlib/probe_tools.py +1 -1
  71. pyTEMlib/test.py +437 -0
  72. pyTEMlib/utilities.py +314 -0
  73. pyTEMlib/version.py +2 -3
  74. pyTEMlib/xrpa_x_sections.py +14 -10
  75. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
  76. pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
  77. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
  78. pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
  79. pyTEMlib/core_loss_widget.py +0 -721
  80. pyTEMlib/eels_dialog.py +0 -754
  81. pyTEMlib/eels_dialog_utilities.py +0 -1199
  82. pyTEMlib/eels_tools.py +0 -2359
  83. pyTEMlib/file_tools_qt.py +0 -193
  84. pyTEMlib/image_dialog.py +0 -158
  85. pyTEMlib/image_dlg.py +0 -146
  86. pyTEMlib/info_widget.py +0 -1086
  87. pyTEMlib/info_widget3.py +0 -1120
  88. pyTEMlib/low_loss_widget.py +0 -479
  89. pyTEMlib/peak_dialog.py +0 -1129
  90. pyTEMlib/peak_dlg.py +0 -286
  91. pytemlib-0.2025.4.2.dist-info/RECORD +0 -38
  92. pytemlib-0.2025.4.2.dist-info/top_level.txt +0 -1
  93. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
  94. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,926 @@
1
+ """
2
+ image_graph part of pycroscopy
3
+
4
+ Author: Gerd Duscher
5
+
6
+ The atomic positions are viewed as a graph; more specifically a ring graph.
7
+ The projections of the unit_cell and defect structural units are the rings.
8
+ The center of the ring is the interstitial location.
9
+ The vertices are the atom positions and the edges are the bonds.
10
+
11
+ This is a modification of the method of Banadaki and Patala
12
+ http://dx.doi.org/10.1038/s41524-017-0016-0
13
+ for 2-D (works in 3D as well)
14
+
15
+ """
16
+ import typing
17
+ import numpy as np
18
+ import matplotlib
19
+ import scipy
20
+ import scipy.sparse
21
+ import ase
22
+
23
+ from tqdm.auto import tqdm, trange
24
+
25
+ ###########################################################################
26
+ # utility functions
27
+ ###########################################################################
28
+
29
+ def interstitial_sphere_center(vertex_pos: np.ndarray, atom_radii: np.ndarray,
30
+ optimize: bool = True) -> typing.Tuple[np.ndarray, float]:
31
+ """
32
+ Function finds center and radius of the largest interstitial sphere of a simplex.
33
+ Which is the center of the cirumsphere if all atoms have the same radius,
34
+ but differs for differently sized atoms.
35
+ In the last case, the circumsphere center is used as starting point for refinement.
36
+
37
+ Parameters
38
+ ----------
39
+ vertex_pos : numpy array
40
+ The position of vertices of a tetrahedron
41
+ atom_radii : float
42
+ bond radii of atoms
43
+ optimize: boolean
44
+ whether atom bond lengths are optimized or not
45
+ Returns
46
+ -------
47
+ new_center : numpy array
48
+ The center of the largest interstitial sphere
49
+ radius : float
50
+ The radius of the largest interstitial sphere
51
+ """
52
+ center, radius = circum_center(vertex_pos, tol=1e-4)
53
+
54
+ def distance_deviation(sphere_center):
55
+ return np.std(np.linalg.norm(vertex_pos - sphere_center, axis=1) - atom_radii)
56
+
57
+ if np.std(atom_radii) == 0 or not optimize:
58
+ return center, radius-atom_radii[0]
59
+ center_new = scipy.optimize.minimize(distance_deviation, center)
60
+ return center_new.x, np.linalg.norm(vertex_pos[0]-center_new.x)-atom_radii[0]
61
+
62
+
63
+ def circum_center(vertex_pos: np.ndarray, tol: float = 1e-4) -> tuple[np.ndarray, float]:
64
+ """
65
+ Function finds the center and the radius of the circumsphere of every simplex.
66
+ Reference:
67
+ Fiedler, Miroslav. Matrices and graphs in geometry. No. 139. Cambridge University Press, 2011.
68
+ (p.29 bottom: example 2.1.11)
69
+ Code started from https://github.com/spatala/gbpy
70
+ with help of
71
+ https://codereview.stackexchange.com/questions/77593/calculating-the-volume-of-a-tetrahedron
72
+
73
+ Parameters
74
+ -----------------
75
+ vertex_pos : numpy array
76
+ The position of vertices of a tetrahedron
77
+ tol : float
78
+ Tolerance defined to identify co-planar tetrahedrons
79
+ Returns
80
+ ----------
81
+ circum_center : numpy array
82
+ The center of the circumsphere
83
+ circum_radius : float
84
+ The radius of the circumsphere
85
+ """
86
+
87
+ # Make Cayley-Menger Matrix
88
+ number_vertices = len(vertex_pos)
89
+ matrix_c = np.identity(number_vertices+1)*-1+1
90
+ distances = scipy.spatial.distance.pdist(np.asarray(vertex_pos, dtype=float),
91
+ metric='sqeuclidean')
92
+ matrix_c[1:, 1:] = scipy.spatial.distance.squareform(distances)
93
+ det_matrix_c = np.linalg.det(matrix_c)
94
+ if abs(det_matrix_c) < tol:
95
+ return np.array(vertex_pos[0]*0), 0
96
+ matrix = -2 * np.linalg.inv(matrix_c)
97
+
98
+ center = vertex_pos[0, :]*0
99
+ for i in range(number_vertices):
100
+ center += matrix[0, i+1] * vertex_pos[i, :]
101
+ center /= np.sum(matrix[0, 1:])
102
+
103
+ circum_radius = np.sqrt(matrix[0, 0]) / 2
104
+
105
+ return np.array(center), circum_radius
106
+
107
+
108
+ def voronoi_volumes(atoms: ase.Atoms) -> np.ndarray:
109
+ """
110
+ Volumes of voronoi cells from
111
+ https://stackoverflow.com/questions/19634993/volume-of-voronoi-cell-python
112
+
113
+ Parameters
114
+ ----------
115
+ atoms: ase.Atoms
116
+ The atomic structure
117
+
118
+ Returns
119
+ -------
120
+ vol: np.ndarray
121
+ The volumes of the voronoi cells
122
+ """
123
+ points = atoms.positions
124
+ v = scipy.spatial.Voronoi(points)
125
+ vol = np.zeros(v.npoints)
126
+ for i, reg_num in enumerate(v.point_region):
127
+ indices = v.regions[reg_num]
128
+ if -1 in indices: # some regions can be opened
129
+ vol[i] = 0
130
+ else:
131
+ try:
132
+ hull = scipy.spatial.ConvexHull(v.simplices[indices])
133
+ vol[i] = hull.volume
134
+ except:
135
+ vol[i] = 0.
136
+
137
+ if atoms.info is None:
138
+ atoms.info = {}
139
+ # atoms.info.update({'volumes': vol})
140
+ return vol
141
+
142
+
143
+ def get_voronoi(tetrahedra: scipy.spatial.Delaunay, atoms: ase.Atoms,
144
+ bond_radii: typing.Optional[typing.Union[float, typing.Sequence[float]]] = None,
145
+ optimize: bool = True):
146
+ """
147
+ Find Voronoi vertices and keep track of associated tetrahedrons and interstitial radii
148
+
149
+ Used in find_polyhedra function
150
+
151
+ Parameters
152
+ ----------
153
+ tetrahedra: scipy.spatial.Delaunay object
154
+ Delaunay tesselation
155
+ atoms: ase.Atoms object
156
+ the structural information
157
+ optimize: boolean
158
+ whether to use different atom radii or not
159
+
160
+ Returns
161
+ -------
162
+ voronoi_vertices: list
163
+ list of positions of voronoi vertices
164
+ voronoi_tetrahedra:
165
+ list of indices of associated vertices of tetrahedra
166
+ r_vv: list of float
167
+ list of all interstitial sizes
168
+ """
169
+
170
+ extent = atoms.cell.lengths()
171
+ print('extent', extent)
172
+
173
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
174
+ positions = atoms.positions[:, :2]
175
+ extent = extent[:2]
176
+ else:
177
+ positions = atoms.positions
178
+
179
+ if atoms.info is None:
180
+ atoms.info = {}
181
+
182
+ if bond_radii is not None:
183
+ bond_radii = [bond_radii]*len(atoms)
184
+ elif 'bond_radii' in atoms.info:
185
+ bond_radii = atoms.info['bond_radii']
186
+ else:
187
+ raise TypeError('We need bond radii to find interstitials')
188
+
189
+ voronoi_vertices = []
190
+ voronoi_tetrahedrons = []
191
+ r_vv = []
192
+ r_aa = []
193
+ print('Find interstitials (finding centers for different elements takes a bit)')
194
+ for vertices in tqdm(tetrahedra.simplices):
195
+ r_a = []
196
+ for vert in vertices:
197
+ r_a.append(bond_radii[vert])
198
+ voronoi, radius = interstitial_sphere_center(positions[vertices], r_a, optimize=optimize)
199
+
200
+ r_a = np.average(r_a) # np.min(r_a)
201
+ r_aa.append(r_a)
202
+
203
+ if (voronoi >= 0).all() and (extent - voronoi > 0).all() and radius > 0.01:
204
+ voronoi_vertices.append(voronoi)
205
+ voronoi_tetrahedrons.append(vertices)
206
+ r_vv.append(radius)
207
+ return voronoi_vertices, voronoi_tetrahedrons, r_vv, np.max(r_aa)
208
+
209
+
210
+ def find_overlapping_spheres(voronoi_vertices: typing.Sequence[np.ndarray],
211
+ r_vv: typing.Sequence[float], r_a: float,
212
+ cheat: float = 1.) -> np.ndarray:
213
+ """Find overlapping spheres
214
+ Parameters
215
+ ----------
216
+ voronoi_vertices: typing.Sequence[np.ndarray]
217
+ List of Voronoi vertices
218
+ r_vv: typing.Sequence[float]
219
+ List of radii for Voronoi vertices
220
+ r_a: float
221
+ Radius of the atom
222
+
223
+ Returns
224
+ -------
225
+ np.ndarray
226
+ Array of overlapping sphere pairs
227
+ """
228
+
229
+ vertex_tree = scipy.spatial.KDTree(voronoi_vertices)
230
+
231
+ pairs = vertex_tree.query_pairs(r=r_a * 2)
232
+
233
+ overlapping_pairs = []
234
+ for (i, j) in pairs:
235
+ if np.linalg.norm(voronoi_vertices[i] - voronoi_vertices[j]) < (r_vv[i] + r_vv[j]) * cheat:
236
+ overlapping_pairs.append([i, j])
237
+
238
+ return np.array(sorted(overlapping_pairs))
239
+
240
+
241
+ def find_interstitial_clusters(overlapping_pairs: np.ndarray) -> tuple[list[list[int]], list[int]]:
242
+ """Make clusters
243
+ Breadth first search to go through the list of overlapping spheres or
244
+ circles to determine clusters
245
+
246
+ Parameters
247
+ ----------
248
+ overlapping_pairs : np.ndarray
249
+ Array of overlapping pairs
250
+
251
+ Returns
252
+ -------
253
+ tuple
254
+ A tuple containing a list of clusters and a list of all visited indices
255
+ """
256
+ visited_all = []
257
+ clusters = []
258
+ for initial in overlapping_pairs[:, 0]:
259
+ if initial not in visited_all:
260
+ # breadth first search
261
+ visited = [] # the atoms we visited
262
+ queue = [initial]
263
+ while queue:
264
+ node = queue.pop(0)
265
+ if node not in visited_all:
266
+ visited.append(node)
267
+ visited_all.append(node)
268
+ # neighbors = overlapping_pairs[overlapping_pairs[:,0]==node,1]
269
+ neighbors = np.append(overlapping_pairs[overlapping_pairs[:, 1] == node, 0],
270
+ overlapping_pairs[overlapping_pairs[:, 0] == node, 1])
271
+
272
+ for neighbour in neighbors:
273
+ if neighbour not in visited:
274
+ queue.append(neighbour)
275
+ clusters.append(visited)
276
+ return clusters, visited_all
277
+
278
+
279
+ def make_polygons(atoms: ase.Atoms, voronoi_vertices: typing.List[typing.Any],
280
+ voronoi_tetrahedrons: typing.List[typing.Any],
281
+ clusters: typing.List[typing.List[int]],
282
+ visited_all: typing.List[int]) -> dict[str, typing.Any]:
283
+ """ make polygons from convex hulls of vertices around interstitial positions
284
+
285
+ Parameters
286
+ ----------
287
+ atoms : ase.Atoms
288
+ Atomic structure information
289
+ voronoi_vertices : list
290
+ List of Voronoi vertices
291
+ voronoi_tetrahedrons : list
292
+ List of Voronoi tetrahedrons
293
+ clusters : list
294
+ List of clusters
295
+ visited_all : list
296
+ List of all visited indices
297
+
298
+ Returns
299
+ -------
300
+ dict
301
+ Dictionary containing polygon information
302
+ """
303
+ polyhedra = {}
304
+ for index, cluster in tqdm(enumerate(clusters)):
305
+ cc = []
306
+ for c in cluster:
307
+ cc = cc + list(voronoi_tetrahedrons[c])
308
+
309
+ hull = scipy.spatial.ConvexHull(atoms.positions[list(set(cc)), :2])
310
+ faces = []
311
+ triangles = []
312
+ for s in hull.simplices:
313
+ faces.append(atoms.positions[list(set(cc))][s])
314
+ triangles.append(list(s))
315
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
316
+ 'faces': faces, 'triangles': triangles,
317
+ 'length': len(list(set(cc))),
318
+ 'combined_vertices': cluster,
319
+ 'interstitial_index': index,
320
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
321
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))]}
322
+
323
+ print('Define conventional interstitial polyhedra')
324
+ running_number = index + 0
325
+ for index in trange(len(voronoi_vertices)):
326
+ if index not in visited_all:
327
+ vertices = voronoi_tetrahedrons[index]
328
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices, :2])
329
+ faces = []
330
+ triangles = []
331
+ for s in hull.simplices:
332
+ faces.append(atoms.positions[vertices][s])
333
+ triangles.append(list(s))
334
+
335
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
336
+ 'faces': faces, 'triangles': triangles,
337
+ 'length': len(vertices),
338
+ 'combined_vertices': index,
339
+ 'interstitial_index': running_number,
340
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
341
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices]}
342
+ # 'volume': hull.volume}
343
+
344
+ running_number += 1
345
+
346
+ return polyhedra
347
+
348
+
349
+ def make_polyhedrons(atoms: ase.Atoms, voronoi_vertices: typing.List[typing.Any],
350
+ voronoi_tetrahedrons: typing.List[typing.Any],
351
+ clusters: typing.List[typing.List[int]],
352
+ visited_all: typing.List[int]) -> dict[str, typing.Any]:
353
+ """collect output data and make dictionary
354
+ Parameters
355
+ ----------
356
+ atoms : ase.Atoms
357
+ Atomic structure information
358
+ voronoi_vertices : list
359
+ List of Voronoi vertices
360
+ voronoi_tetrahedrons : list
361
+ List of Voronoi tetrahedrons
362
+ clusters : list
363
+ List of clusters
364
+ visited_all : list
365
+ List of all visited indices
366
+
367
+ Returns
368
+ -------
369
+ dict
370
+ Dictionary containing polyhedron information
371
+ """
372
+
373
+ polyhedra = {}
374
+ connectivity_matrix = scipy.sparse.dok_matrix((len(atoms), len(atoms)), dtype=bool)
375
+
376
+ print('Define clustered interstitial polyhedra')
377
+ index = 0
378
+ for index, cluster in tqdm(enumerate(clusters)):
379
+ cc = []
380
+ for c in cluster:
381
+ cc = cc + list(voronoi_tetrahedrons[c])
382
+ cc = list(set(cc))
383
+
384
+ hull = scipy.spatial.ConvexHull(atoms.positions[cc])
385
+ faces = []
386
+ triangles = []
387
+ for s in hull.simplices:
388
+ faces.append(atoms.positions[cc][s])
389
+ triangles.append(list(s))
390
+ for k in range(len(s)):
391
+ l = (k + 1) % len(s)
392
+ if cc[s[k]] > cc[s[l]]:
393
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
394
+ else:
395
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
396
+
397
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
398
+ 'faces': faces, 'triangles': triangles,
399
+ 'length': len(list(set(cc))),
400
+ 'combined_vertices': cluster,
401
+ 'interstitial_index': index,
402
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
403
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))],
404
+ 'volume': hull.volume}
405
+ # 'coplanar': hull.coplanar}
406
+
407
+ print('Define conventional interstitial polyhedra')
408
+ running_number = index + 0
409
+ for index in range(len(voronoi_vertices)):
410
+ if index not in visited_all:
411
+ vertices = voronoi_tetrahedrons[index]
412
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices])
413
+ faces = []
414
+ triangles = []
415
+ for s in hull.simplices:
416
+ faces.append(atoms.positions[vertices][s])
417
+ triangles.append(list(s))
418
+ for k in range(len(s)):
419
+ l = (k + 1) % len(s)
420
+ if cc[s[k]] > cc[s[l]]:
421
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
422
+ else:
423
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
424
+
425
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
426
+ 'faces': faces, 'triangles': triangles,
427
+ 'length': len(vertices),
428
+ 'combined_vertices': index,
429
+ 'interstitial_index': running_number,
430
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
431
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices],
432
+ 'volume': hull.volume}
433
+
434
+ running_number += 1
435
+ if atoms.info is None:
436
+ atoms.info = {}
437
+ atoms.info.update({'graph': {'connectivity_matrix': connectivity_matrix}})
438
+ return polyhedra
439
+
440
+
441
+ ##################################################################
442
+ # polyhedra functions
443
+ ##################################################################
444
+
445
+ def get_non_periodic_supercell(super_cell: ase.Atoms) -> ase.Atoms:
446
+ """Get a non-periodic supercell of the given supercell.
447
+ Parameters:
448
+ -----------
449
+ super_cell: ase.Atoms
450
+ The supercell to modify.
451
+
452
+ Returns:
453
+ --------
454
+ ase.Atoms
455
+ The non-periodic supercell.
456
+ """
457
+ super_cell.wrap()
458
+ atoms = super_cell*3
459
+ atoms.positions -= super_cell.cell.lengths()
460
+ atoms.positions[:,0] += super_cell.cell[0,0]*.0
461
+ del(atoms[atoms.positions[: , 0]<-5])
462
+ del(atoms[atoms.positions[: , 0]>super_cell.cell[0,0]+5])
463
+ del(atoms[atoms.positions[: , 1]<-5])
464
+ del(atoms[atoms.positions[: , 1]>super_cell.cell[1,1]+5])
465
+ del(atoms[atoms.positions[: , 2]<-5])
466
+ del(atoms[atoms.positions[: , 2]>super_cell.cell[2,2]+5])
467
+ return atoms
468
+
469
+ def get_connectivity_matrix(crystal: ase.Atoms, atoms: ase.Atoms,
470
+ polyhedra: dict) -> np.ndarray:
471
+ """
472
+ Get the connectivity matrix for the given atoms and polyhedra.
473
+ Parameters:
474
+ -----------
475
+ crystal: ase.Atoms
476
+ The crystal structure.
477
+ atoms: ase.Atoms
478
+ The atoms to consider.
479
+ polyhedra: dict
480
+ The polyhedra information.
481
+
482
+ Returns:
483
+ --------
484
+ np.ndarray
485
+ The connectivity matrix.
486
+ """
487
+ crystal_tree = scipy.spatial.KDTree(crystal.positions)
488
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=int)
489
+
490
+ for polyhedron in polyhedra.values():
491
+ vertices = polyhedron['vertices'] - crystal.cell.lengths()
492
+ atom_ind = np.array(polyhedron['indices'])
493
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
494
+ to_bond = np.where(dd<0.001)[0]
495
+
496
+ for triangle in polyhedron['triangles']:
497
+ triangle = np.array(triangle)
498
+ for permut in [[0,1], [1,2], [0,2]]:
499
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
500
+ if vertex[0] in to_bond or vertex[1] in to_bond:
501
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = 1
502
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = 1
503
+ return connectivity_matrix
504
+
505
+
506
+ def get_bonds(crystal, shift= 0., verbose = False, cheat=1.0):
507
+ """
508
+ Get polyhedra, and bonds from and edges and lengths of edges for each polyhedron
509
+ and store it in info dictionary of new ase.Atoms object
510
+
511
+ Parameter:
512
+ ----------
513
+ crystal: ase.atoms_object
514
+ information on all polyhedra
515
+ shift: float
516
+ The amount to shift the crystal positions.
517
+ verbose: bool
518
+ If True, print additional information.
519
+ cheat: float
520
+ The amount to cheat the bond lengths.
521
+
522
+ Returns
523
+ -------
524
+ ase.Atoms
525
+ The modified atoms object.
526
+ """
527
+ crystal.positions += shift * crystal.cell[0, 0]
528
+ crystal.wrap()
529
+
530
+ atoms = get_non_periodic_supercell(crystal)
531
+ atoms = atoms[atoms.numbers.argsort()]
532
+
533
+ atoms.positions += crystal.cell.lengths()
534
+ polyhedra = find_polyhedra(atoms, cheat=cheat)
535
+
536
+ connectivity_matrix = get_connectivity_matrix(crystal, atoms, polyhedra)
537
+ coord = connectivity_matrix.sum(axis=1)
538
+
539
+ del atoms[np.where(coord==0)]
540
+ new_polyhedra = {}
541
+ index = 0
542
+ octahedra =[]
543
+ tetrahedra = []
544
+ other = []
545
+ super_cell_atoms =[]
546
+
547
+ atoms_tree = scipy.spatial.KDTree(atoms.positions-crystal.cell.lengths())
548
+ crystal_tree = scipy.spatial.KDTree(crystal.positions)
549
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=float)
550
+
551
+ for polyhedron in polyhedra.values():
552
+ polyhedron['vertices'] -= crystal.cell.lengths()
553
+ vertices = polyhedron['vertices']
554
+ center = np.average(polyhedron['vertices'], axis=0)
555
+
556
+ dd, polyhedron['indices'] = atoms_tree.query(vertices , k=1)
557
+ atom_ind = np.array(polyhedron['indices'])
558
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
559
+
560
+ to_bond = np.where(dd<0.001)[0]
561
+ super_cell_atoms.extend(list(atom_ind[to_bond]))
562
+
563
+ edges = []
564
+ lengths = []
565
+ for triangle in polyhedron['triangles']:
566
+ triangle = np.array(triangle)
567
+ for permut in [[0,1], [1,2], [0,2]]:
568
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
569
+ length = np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]])
570
+ if vertex[0] in to_bond or vertex[1] in to_bond:
571
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = length
572
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = length
573
+ if vertex[0] not in to_bond:
574
+ atoms[atom_ind[vertex[0]]].symbol = 'Be'
575
+ if vertex[1] not in to_bond:
576
+ atoms[atom_ind[vertex[1]]].symbol = 'Be'
577
+ if vertex not in edges:
578
+ edges.append(vertex)
579
+ lengths.append(np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]] ))
580
+ polyhedron['edges'] = edges
581
+ polyhedron['edge_lengths'] = lengths
582
+ if all(center > -0.000001) and all(center < crystal.cell.lengths()-0.01):
583
+ new_polyhedra[str(index)]=polyhedron
584
+ if polyhedron['length'] == 4:
585
+ tetrahedra.append(str(index))
586
+ elif polyhedron['length'] == 6:
587
+ octahedra.append(str(index))
588
+ else:
589
+ other.append(str(index))
590
+ if verbose:
591
+ print(polyhedron['length'])
592
+ index += 1
593
+ atoms.positions -= crystal.cell.lengths()
594
+ coord = connectivity_matrix.copy()
595
+ coord[np.where(coord>.1)] = 1
596
+ coord = coord.sum(axis=1)
597
+
598
+ super_cell_atoms = np.sort(np.unique(super_cell_atoms))
599
+ atoms.info.update({'polyhedra': {'polyhedra': new_polyhedra,
600
+ 'tetrahedra': tetrahedra,
601
+ 'octahedra': octahedra,
602
+ 'other' : other}})
603
+ atoms.info.update({'bonds': {'connectivity_matrix': connectivity_matrix,
604
+ 'super_cell_atoms': super_cell_atoms,
605
+ 'super_cell_dimensions': crystal.cell.array,
606
+ 'coordination': coord}})
607
+ atoms.info.update({'supercell': crystal})
608
+ return atoms
609
+
610
+ def find_polyhedra(atoms, optimize=True, cheat=1.0, bond_radii=None):
611
+ """ get polyhedra information from an ase.Atoms object
612
+
613
+ This is following the method of Banadaki and Patala
614
+ http://dx.doi.org/10.1038/s41524-017-0016-0
615
+
616
+ We are using the bond radius according to Kirkland, which is tabulated in
617
+ - pyTEMlib.crystal_tools.electronFF[atoms.symbols[vert]]['bond_length'][1]
618
+
619
+ Parameter
620
+ ---------
621
+ atoms: ase.Atoms object
622
+ the structural information
623
+ cheat: float
624
+ does not exist
625
+
626
+ Returns
627
+ -------
628
+ polyhedra: dict
629
+ dictionary with all information of polyhedra
630
+ """
631
+ if not isinstance(atoms, ase.Atoms):
632
+ raise TypeError('This function needs an ase.Atoms object')
633
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
634
+ positions = atoms.positions[:, :2]
635
+ print('2D')
636
+ else:
637
+ positions = atoms.positions
638
+ tetrahedra = scipy.spatial.Delaunay(positions)
639
+ voronoi_vertices, voronoi_tetrahedrons, r_vv, r_a = get_voronoi(tetrahedra, atoms,
640
+ optimize=optimize,
641
+ bond_radii=bond_radii)
642
+ if positions.shape[1] < 3:
643
+ r_vv = np.array(r_vv)*1.
644
+ overlapping_pairs = find_overlapping_spheres(voronoi_vertices, r_vv, r_a, cheat=cheat)
645
+
646
+ clusters, visited_all = find_interstitial_clusters(overlapping_pairs)
647
+ if positions.shape[1] < 3:
648
+ rings = get_polygons(atoms, clusters, voronoi_tetrahedrons)
649
+ return rings
650
+ polyhedra = make_polyhedrons(atoms, voronoi_vertices, voronoi_tetrahedrons,
651
+ clusters, visited_all)
652
+ return polyhedra
653
+
654
+
655
+ def polygon_sort(corners: np.ndarray) -> np.ndarray:
656
+ """Sort corners of a polygon based on their angles relative to the center.
657
+ Parameters
658
+ ----------
659
+ corners: np.ndarray
660
+ The corners of the polygon.
661
+
662
+ Returns
663
+ -------
664
+ np.ndarray
665
+ The sorted corners of the polygon.
666
+ """
667
+ center = np.average(corners[:, :2], axis=0)
668
+ angles = (np.arctan2(corners[:,0]-center[0], corners[:,1]-center[1])+2.0*np.pi)% (2.0*np.pi)
669
+ return corners[np.argsort(angles)]
670
+
671
+ def get_polygons(atoms: ase.Atoms, clusters: typing.List[typing.List[int]],
672
+ voronoi_tetrahedrons: typing.List[typing.Iterable[int]]) -> dict[str, typing.Any]:
673
+ """Get polygon information from clusters and Voronoi tetrahedrons.
674
+ Parameters
675
+ ----------
676
+ atoms: ase.Atoms
677
+ The atomic structure.
678
+ clusters: list of list of int
679
+ The clusters of atoms.
680
+ voronoi_tetrahedrons: list of iterable of int
681
+ The Voronoi tetrahedrons.
682
+
683
+ Returns
684
+ -------
685
+ dict
686
+ A dictionary containing polygon information.
687
+ """
688
+
689
+ polygons = []
690
+ cyclicity = []
691
+ centers = []
692
+ corners =[]
693
+ for cluster in clusters:
694
+ cc = []
695
+ for c in cluster:
696
+ cc = cc + list(voronoi_tetrahedrons[c])
697
+
698
+ sorted_corners = polygon_sort(atoms.positions[list(set(cc)), :2])
699
+ cyclicity.append(len(sorted_corners))
700
+ corners.append(sorted_corners)
701
+ centers.append(np.mean(sorted_corners[:,:2], axis=0))
702
+ polygons.append(matplotlib.patches.Polygon(np.array(sorted_corners)[:,:2],
703
+ closed=True, fill=True,
704
+ edgecolor='red'))
705
+
706
+ rings={'atoms': atoms.positions[:, :2],
707
+ 'cyclicity': np.array(cyclicity),
708
+ 'centers': np.array(centers),
709
+ 'corners': corners,
710
+ 'polygons': polygons}
711
+ return rings
712
+
713
+
714
+ def sort_polyhedra_by_vertices(polyhedra: dict, visible: typing.Iterable[int] = range(4, 100),
715
+ z_lim: typing.List[float] = [0, 100],
716
+ verbose: bool = False) -> typing.List[str]:
717
+ """Sort polyhedra by their number of vertices.
718
+
719
+ Parameters
720
+ ----------
721
+ polyhedra: dict
722
+ Dictionary of polyhedra information.
723
+ visible: range
724
+ Range of visible vertex counts.
725
+ z_lim: list
726
+ Z-axis limits for visibility.
727
+ verbose: bool
728
+ If True, print detailed information.
729
+
730
+ Returns
731
+ -------
732
+ list
733
+ List of polyhedron indices sorted by vertex count.
734
+ """
735
+ indices = []
736
+
737
+ for key, polyhedron in polyhedra.items():
738
+ if 'length' not in polyhedron:
739
+ polyhedron['length'] = len(polyhedron['vertices'])
740
+
741
+ if polyhedron['length'] in visible:
742
+ center = polyhedron['vertices'].mean(axis=0)
743
+ if z_lim[0] < center[2] < z_lim[1]:
744
+ indices.append(key)
745
+ if verbose:
746
+ print(key, polyhedron['length'], center)
747
+ return indices
748
+
749
+
750
+ ##########################
751
+ # New Graph Stuff
752
+ ##########################
753
+ def breadth_first_search(graph: np.ndarray, initial_node_index: int,
754
+ projected_crystal: ase.Atoms) -> typing.Tuple[np.ndarray, np.ndarray]:
755
+ """ breadth first search of atoms viewed as a graph
756
+
757
+ the projection dictionary has to contain the following items
758
+ 'number_of_nearest_neighbours', 'rotated_cell', 'near_base', 'allowed_variation'
759
+
760
+ Parameters
761
+ ----------
762
+ graph: numpy array (Nx2)
763
+ the atom positions
764
+ initial: int
765
+ index of starting atom
766
+ projection_tags: dict
767
+ dictionary with information on projected unit cell (with 'rotated_cell' item)
768
+
769
+ Returns
770
+ -------
771
+ graph[visited]: numpy array (M,2) with M<N
772
+ positions of atoms hopped in unit cell lattice
773
+ ideal: numpy array (M,2)
774
+ ideal atom positions
775
+ """
776
+
777
+ projection_tags = projected_crystal.info['projection']
778
+ if 'lattice_vector' in projection_tags:
779
+ a_lattice_vector = projection_tags['lattice_vector']['a']
780
+ b_lattice_vector = projection_tags['lattice_vector']['b']
781
+ main = np.array([a_lattice_vector, -a_lattice_vector,
782
+ b_lattice_vector, -b_lattice_vector]) # vectors of unit cell
783
+ near = main
784
+ else:
785
+ # get lattice vectors to hopp along through graph
786
+ projected_unit_cell = projected_crystal.cell[:2, :2]
787
+ a_lattice_vector = projected_unit_cell[0]
788
+ b_lattice_vector = projected_unit_cell[1]
789
+ main = np.array([a_lattice_vector, -a_lattice_vector,
790
+ b_lattice_vector, -b_lattice_vector]) # vectors of unit cell
791
+ near = projection_tags['near_base'] # all nearest atoms
792
+ near = np.append(main, near, axis=0)
793
+
794
+ neighbour_tree = scipy.spatial.KDTree(graph)
795
+ distances, indices = neighbour_tree.query(graph, k=50) # let's get all neighbours
796
+
797
+ visited = [] # the atoms we visited
798
+ ideal = [] # atoms at ideal lattice
799
+ sub_lattice = [] # atoms in base and disregarded
800
+ queue = [initial_node_index]
801
+ ideal_queue = [graph[initial_node_index]]
802
+
803
+ while queue:
804
+ node = queue.pop(0)
805
+ ideal_node = ideal_queue.pop(0)
806
+
807
+ if node not in visited:
808
+ visited.append(node)
809
+ ideal.append(ideal_node)
810
+ # print(node,ideal_node)
811
+ neighbors = indices[node]
812
+ for i, neighbour in enumerate(neighbors):
813
+ if neighbour not in visited:
814
+ distance_to_ideal = np.linalg.norm(near + graph[node]-graph[neighbour], axis=1)
815
+ if np.min(distance_to_ideal) < projection_tags['allowed_variation']:
816
+ direction = np.argmin(distance_to_ideal)
817
+ if direction > 3: # counting starts at 0
818
+ sub_lattice.append(neighbour)
819
+ elif distances[node, i] < projection_tags['distance_unit_cell'] * 1.05:
820
+ queue.append(neighbour)
821
+ ideal_queue.append(ideal_node + near[direction])
822
+
823
+ return graph[visited], ideal
824
+
825
+
826
+ def get_base_atoms(graph: np.ndarray, origins: np.ndarray, base: np.ndarray,
827
+ tolerance: float = 3) -> list[np.ndarray]:
828
+ """ get sublattices of atoms in a graph
829
+ This function returns the indices of atoms in a graph that are close to the base atoms.
830
+ Parameters
831
+ ----------
832
+ graph: numpy array (Nx2)
833
+ the atom positions
834
+ origins: numpy array (Nx2)
835
+ the origin positions
836
+ base: numpy array (Mx2)
837
+ the base atom positions
838
+ tolerance: float
839
+ the distance tolerance for finding base atoms
840
+ Returns
841
+ -------
842
+ sublattices: list of numpy arrays
843
+ list of indices of atoms in the graph that are close to each base atom
844
+ """
845
+ sublattices = []
846
+ neighbour_tree = scipy.spatial.KDTree(graph)
847
+ for base_atom in base:
848
+ distances, indices = neighbour_tree.query(origins+base_atom[:2], k=50)
849
+ sublattices.append(indices[distances < tolerance])
850
+ return sublattices
851
+
852
+
853
+ def breadth_first_search_flexible(graph: np.ndarray, initial_node_index: int,
854
+ lattice_parameter: typing.Union[float, np.ndarray, ase.Atoms],
855
+ tolerance: float = 1) -> typing.Tuple[np.ndarray, list[int]]:
856
+ """ breadth first search of atoms viewed as a graph
857
+ This is a rotational invariant search of atoms in a lattice,
858
+ and returns the angles of unit cells.
859
+ We only use the ideal lattice parameter to determine the lattice.
860
+
861
+ Parameters
862
+ ----------
863
+ graph: numpy array (Nx2)
864
+ atomic positions
865
+ lattice_parameter: float or numpy array
866
+ the lattice parameter
867
+ tolerance: float
868
+ the tolerance for finding similar atoms
869
+
870
+ Returns
871
+ -------
872
+ numpy array (Nx3)
873
+ the output atomic positions and angles
874
+ """
875
+ if isinstance(lattice_parameter, ase.Atoms):
876
+ lattice_parameter = lattice_parameter.cell.lengths()[:2]
877
+ elif isinstance(lattice_parameter, float):
878
+ lattice_parameter = [lattice_parameter]
879
+ lattice_parameter = np.array(lattice_parameter)
880
+
881
+ neighbour_tree = scipy.spatial.KDTree(graph)
882
+ _, indices = neighbour_tree.query(graph, k=50) # let's get all neighbours
883
+ visited = [] # the atoms we visited
884
+ angles = [] # atoms at ideal lattice
885
+ queue = [initial_node_index]
886
+ queue_angles=[0]
887
+ while queue:
888
+ node = queue.pop(0)
889
+ angle = queue_angles.pop(0)
890
+ if node not in visited:
891
+ visited.append(node)
892
+ angles.append(angle)
893
+ neighbors = indices[node]
894
+ for neighbour in neighbors:
895
+ if neighbour not in visited:
896
+ hopp = graph[node] - graph[neighbour]
897
+ distance_to_ideal = np.linalg.norm(hopp)
898
+ if np.min(np.abs(distance_to_ideal - lattice_parameter)) < tolerance:
899
+ queue.append(neighbour)
900
+ queue_angles.append(np.arctan2(hopp[1], hopp[0]))
901
+ angles[0] = angles[1]
902
+ out_atoms = np.stack([graph[visited][:, 0], graph[visited][:, 1], angles])
903
+ return out_atoms.T, visited
904
+
905
+ def delete_rim_atoms(atoms: np.ndarray, extent: np.ndarray, rim_distance: float) -> np.ndarray:
906
+ """Delete rim atoms from the atomic structure.
907
+
908
+ Parameters
909
+ ----------
910
+ atoms: numpy array (Nx2)
911
+ Atomic positions.
912
+ extent: numpy array (2,)
913
+ The extent of the region of interest.
914
+ rim_distance: float
915
+ The distance from the extent within which atoms will be removed.
916
+
917
+ Returns
918
+ -------
919
+ numpy array (Mx2)
920
+ The atomic positions after rim atoms have been removed.
921
+ """
922
+ rim = np.where(atoms[:, :2] - extent > -rim_distance)[0]
923
+ middle_atoms = np.delete(atoms, rim, axis=0)
924
+ rim = np.where(middle_atoms[:, :2].min(axis=1)<rim_distance)[0]
925
+ middle_atoms = np.delete(middle_atoms, rim, axis=0)
926
+ return middle_atoms