pyTEMlib 0.2020.11.0__py3-none-any.whl → 0.2024.8.4__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 (59) hide show
  1. pyTEMlib/__init__.py +11 -11
  2. pyTEMlib/animation.py +631 -0
  3. pyTEMlib/atom_tools.py +240 -222
  4. pyTEMlib/config_dir.py +57 -29
  5. pyTEMlib/core_loss_widget.py +658 -0
  6. pyTEMlib/crystal_tools.py +1255 -0
  7. pyTEMlib/diffraction_plot.py +756 -0
  8. pyTEMlib/dynamic_scattering.py +293 -0
  9. pyTEMlib/eds_tools.py +609 -0
  10. pyTEMlib/eels_dialog.py +749 -486
  11. pyTEMlib/{interactive_eels.py → eels_dialog_utilities.py} +1199 -1524
  12. pyTEMlib/eels_tools.py +2031 -1731
  13. pyTEMlib/file_tools.py +1276 -491
  14. pyTEMlib/file_tools_qt.py +193 -0
  15. pyTEMlib/graph_tools.py +1166 -450
  16. pyTEMlib/graph_viz.py +449 -0
  17. pyTEMlib/image_dialog.py +158 -0
  18. pyTEMlib/image_dlg.py +146 -0
  19. pyTEMlib/image_tools.py +1399 -956
  20. pyTEMlib/info_widget.py +933 -0
  21. pyTEMlib/interactive_image.py +1 -0
  22. pyTEMlib/kinematic_scattering.py +1196 -0
  23. pyTEMlib/low_loss_widget.py +176 -0
  24. pyTEMlib/microscope.py +61 -78
  25. pyTEMlib/peak_dialog.py +1047 -350
  26. pyTEMlib/peak_dlg.py +286 -248
  27. pyTEMlib/probe_tools.py +653 -202
  28. pyTEMlib/sidpy_tools.py +153 -129
  29. pyTEMlib/simulation_tools.py +104 -87
  30. pyTEMlib/version.py +6 -3
  31. pyTEMlib/xrpa_x_sections.py +20972 -0
  32. {pyTEMlib-0.2020.11.0.dist-info → pyTEMlib-0.2024.8.4.dist-info}/LICENSE +21 -21
  33. pyTEMlib-0.2024.8.4.dist-info/METADATA +93 -0
  34. pyTEMlib-0.2024.8.4.dist-info/RECORD +37 -0
  35. {pyTEMlib-0.2020.11.0.dist-info → pyTEMlib-0.2024.8.4.dist-info}/WHEEL +6 -5
  36. {pyTEMlib-0.2020.11.0.dist-info → pyTEMlib-0.2024.8.4.dist-info}/entry_points.txt +0 -1
  37. pyTEMlib/KinsCat.py +0 -2685
  38. pyTEMlib/__version__.py +0 -2
  39. pyTEMlib/data/TEMlibrc +0 -68
  40. pyTEMlib/data/edges_db.csv +0 -189
  41. pyTEMlib/data/edges_db.pkl +0 -0
  42. pyTEMlib/data/fparam.txt +0 -103
  43. pyTEMlib/data/microscopes.csv +0 -7
  44. pyTEMlib/data/microscopes.xml +0 -167
  45. pyTEMlib/data/path.txt +0 -1
  46. pyTEMlib/defaults_parser.py +0 -86
  47. pyTEMlib/dm3_reader.py +0 -609
  48. pyTEMlib/edges_db.py +0 -76
  49. pyTEMlib/eels_dlg.py +0 -240
  50. pyTEMlib/hdf_utils.py +0 -481
  51. pyTEMlib/image_tools1.py +0 -2194
  52. pyTEMlib/info_dialog.py +0 -227
  53. pyTEMlib/info_dlg.py +0 -205
  54. pyTEMlib/nion_reader.py +0 -293
  55. pyTEMlib/nsi_reader.py +0 -165
  56. pyTEMlib/structure_tools.py +0 -316
  57. pyTEMlib-0.2020.11.0.dist-info/METADATA +0 -20
  58. pyTEMlib-0.2020.11.0.dist-info/RECORD +0 -42
  59. {pyTEMlib-0.2020.11.0.dist-info → pyTEMlib-0.2024.8.4.dist-info}/top_level.txt +0 -0
pyTEMlib/graph_tools.py CHANGED
@@ -1,450 +1,1166 @@
1
- import numpy as np
2
- import scipy.spatial as spatial
3
-
4
- from skimage.measure import points_in_poly
5
-
6
-
7
- def turning_function(corners, points):
8
- # sort corners in counter-clockwise direction
9
- # calculate centroid of the polygon
10
- corners1 = np.array(points[corners])
11
- corners2 = np.roll(corners1, 1)
12
- corners0 = np.roll(corners1, -1)
13
-
14
- v = corners1 - corners0
15
- an = (np.arctan2(v[:, 0], v[:, 1]) + 2.0 * np.pi) % (2.0 * np.pi) / np.pi * 180
16
- print(corners1)
17
- angles = []
18
- for i in range(len(corners1)):
19
- a = corners1[i] - corners0[i]
20
- b = corners1[i] - corners2[i]
21
- num = np.dot(a, b)
22
- denom = np.linalg.norm(a) * np.linalg.norm(b)
23
- angles.append(np.arccos(num / denom) * 180 / np.pi)
24
-
25
- return angles
26
-
27
-
28
- def polygon_sort2(corners, points):
29
- """
30
- # sort corners in counter-clockwise direction
31
- input:
32
- corners are indices in points array
33
- points is list or array of points
34
- output:
35
- corners_with_angles
36
- """
37
- # calculate centroid of the polygon
38
- n = len(corners) # of corners
39
- cx = float(sum(x for x, y in points[corners])) / n
40
- cy = float(sum(y for x, y in points[corners])) / n
41
-
42
- # create a new list of corners which includes angles
43
- # angles from the positive x axis
44
- corners_with_angles = []
45
- for i in corners:
46
- x, y = points[i]
47
- an = (np.atan2(y - cy, x - cx) + 2.0 * np.pi) % (2. * np.pi)
48
- corners_with_angles.append([i, np.degrees(an)])
49
-
50
- # sort it using the angles
51
- corners_with_angles.sort(key=lambda tup: tup[1])
52
-
53
- return corners_with_angles
54
-
55
-
56
- def polygons_inner(indices, points):
57
- pp = np.array(points)[indices, :]
58
- # Determine inner angle of polygon
59
- # Generate second array which is shifted by one
60
- pp2 = np.roll(pp, 1, axis=0)
61
- # and subtract it from former: this is now a list of vectors
62
- p_vectors = pp - pp2
63
-
64
- # angles of vectors with respect to positive x-axis
65
- ang = np.arctan2(p_vectors[:, 1], p_vectors[:, 0]) / np.pi * 180 + 360 % 360
66
- # shift angles array by one
67
- ang2 = np.roll(ang, -1, axis=0)
68
-
69
- # difference of angles is outer angle but we want the inner (inner + outer = 180)
70
- inner_angles = (180 - (ang2 - ang) + 360) % 360
71
-
72
- return inner_angles
73
-
74
-
75
- # sort corners in counter-clockwise direction
76
- def polygon_sort(corners):
77
- # calculate centroid of the polygon
78
- n = len(corners) # of corners
79
- cx = float(sum(x for x, y in corners)) / n
80
- cy = float(sum(y for x, y in corners)) / n
81
-
82
- # create a new list of corners which includes angles
83
- corners_with_angles = []
84
- for x, y in corners:
85
- an = (np.atan2(y - cy, x - cx) + 2.0 * np.pi) % (2.0 * np.pi)
86
- corners_with_angles.append((x, y, np.degrees(an)))
87
-
88
- # sort it using the angles
89
- corners_with_angles.sort(key=lambda tup: tup[2])
90
-
91
- return corners_with_angles
92
-
93
-
94
- def polygon_area(corners):
95
- """
96
- # Area of Polygon using Shoelace formula
97
- # http://en.wikipedia.org/wiki/Shoelace_formula
98
- # FB - 20120218
99
- # corners must be ordered in clockwise or counter-clockwise direction
100
- """
101
- n = len(corners) # of corners
102
- area = 0.0
103
- c_x = 0
104
- c_y = 0
105
- for i in range(n):
106
- j = (i + 1) % n
107
- nn = corners[i][0] * corners[j][1] - corners[j][0] * corners[i][1]
108
- area += nn
109
- c_x += (corners[i][0] + corners[j][0]) * nn
110
- c_y += (corners[i][1] + corners[j][1]) * nn
111
-
112
- area = abs(area) / 2.0
113
-
114
- # centeroid or arithmetic mean
115
- c_x = c_x / (6 * area)
116
- c_y = c_y / (6 * area)
117
-
118
- return area, c_x, c_y
119
-
120
-
121
- def polygon_angles(corners):
122
- angles = []
123
- # calculate centroid of the polygon
124
- n = len(corners) # of corners
125
- cx = float(sum(x for x, y in corners)) / n
126
- cy = float(sum(y for x, y in corners)) / n
127
- # create a new list of angles
128
- # print (cx, cy)
129
- for x, y in corners:
130
- an = (np.atan2(y - cy, x - cx) + 2.0 * np.pi) % (2.0 * np.pi)
131
- angles.append((np.degrees(an)))
132
-
133
- return angles
134
-
135
-
136
- def voronoi_tags(vor):
137
- sym = {'voronoi': vor, 'vertices': vor.vertices, 'ridge_points': vor.ridge_points,
138
- 'ridge_vertices': vor.ridge_vertices, 'regions': vor.regions, 'point_region': vor.point_region}
139
- # Indices of the points between which each Voronoi ridge lies.
140
- # Indices of the Voronoi vertices forming each Voronoi ridge.
141
- # Indices of the Voronoi vertices forming each Voronoi region. -1 indicates vertex outside the Voronoi diagram.
142
- # Index of the Voronoi region for each input point. If qhull option “Qc” was not specified,
143
- # the list will contain -1 for points that are not associated with a Voronoi region.
144
-
145
- points = vor.points
146
- nn_tree = (points)
147
-
148
- rim = []
149
- regions = []
150
-
151
- # ##
152
- # We get all the vertice length
153
-
154
- lengths = []
155
- for vertice in vor.ridge_vertices:
156
- if not (-1 in vertice):
157
- p1 = vor.vertices[vertice[0]]
158
- p2 = vor.vertices[vertice[1]]
159
- lengths.append(np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2))
160
-
161
- sym['lengths'] = lengths
162
- sym['median lengths'] = np.median(lengths)
163
- sym['Min Voronoi Edge'] = np.median(lengths) / 1.5
164
- # print ('median lengths', np.median(lengths))
165
- # print ('Min Voronoi Edge',np.median(lengths)/1.5)
166
- corners_hist = []
167
- nn_hist = []
168
- nn_dist_hist = []
169
- angle_hist = []
170
- area_hist = []
171
- deviation_hist = []
172
-
173
- for i, region in enumerate(vor.point_region):
174
- x, y = points[i]
175
- sym[str(i)] = {}
176
- vertices = vor.regions[region]
177
-
178
- # #
179
- # We get all the rim atoms
180
- # ##
181
-
182
- # if all(v >= 0 and all(vor.vertices[v] >0) and all(vor.vertices[v]<tags['data'].shape[0]) for v in vertices):
183
- if all(v >= 0 and all(vor.vertices[v] > 0) for v in vertices):
184
- # finite regions only now
185
- # negative and too large vertices (corners) are excluded
186
-
187
- regions.append(vertices)
188
- poly = []
189
- for v in vertices:
190
- poly.append(vor.vertices[v])
191
-
192
- area, cx, cy = polygon_area(poly)
193
- cx = abs(cx)
194
- cy = abs(cy)
195
-
196
- angles = polygon_angles(poly)
197
- angle_hist.append(angles)
198
- area_hist.append(area)
199
- deviation_hist.append(np.sqrt((cx - x) ** 2 + (cy - y) ** 2))
200
-
201
- sym[str(i)]['xy'] = [x, y]
202
- sym[str(i)]['geometric'] = [cx, cy]
203
- sym[str(i)]['area'] = area
204
-
205
- sym[str(i)]['angles'] = angles
206
- sym[str(i)]['off center'] = [cx - x, cy - y]
207
-
208
- sym[str(i)]['position'] = 'inside'
209
- sym[str(i)]['corner'] = vertices
210
- sym[str(i)]['vertices'] = poly
211
- sym[str(i)]['corners'] = len(vertices)
212
- corners_hist.append(len(vertices))
213
- nn = 0
214
- nn_vor = []
215
- length = []
216
- for j in range(len(vertices)):
217
- k = (j + 1) % len(vertices)
218
- p1 = vor.vertices[vertices[j]]
219
- p2 = vor.vertices[vertices[k]]
220
- leng = np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
221
- length.append(leng)
222
- sym[str(i)]['length'] = length
223
- if leng > sym['Min Voronoi Edge']:
224
- nn += 1
225
- nn_vor.append(vertices[j])
226
- sym[str(i)]['length'] = length
227
- nn_p = nn_tree.query(points[i], k=nn + 1)
228
- sym[str(i)]['neighbors'] = []
229
- sym[str(i)]['nn Distance'] = []
230
- sym[str(i)]['nn'] = nn
231
- if nn > 0:
232
- nn_hist.append(nn)
233
- for j in range(1, len(nn_p[0])):
234
- sym[str(i)]['nn Distance'].append(nn_p[0][j])
235
- sym[str(i)]['neighbors'].append(nn_p[1][j])
236
- nn_dist_hist.append(nn_p[0][j])
237
- else:
238
- rim.append(i)
239
- sym[str(i)]['position'] = 'rim'
240
- sym[str(i)]['corners'] = 0
241
- print('weird nn determination', i)
242
-
243
- else:
244
- rim.append(i)
245
- sym[str(i)]['position'] = 'rim'
246
- sym[str(i)]['corners'] = 0
247
- sym[str(i)]['xy'] = [x, y]
248
-
249
- sym['average corners'] = np.median(corners_hist)
250
- sym['average area'] = np.median(area_hist)
251
- sym['num atoms at rim'] = len(rim)
252
- sym['num voronoi'] = len(points) - len(rim)
253
- sym['Median Coordination'] = np.median(nn_hist)
254
- sym['Median NN Distance'] = np.median(nn_dist_hist)
255
-
256
- sym['_hist corners'] = corners_hist
257
- sym['_hist area'] = area_hist
258
- sym['atoms at rim'] = rim
259
- sym['_hist Coordination'] = nn_hist
260
- sym['_hist NN Distance'] = nn_dist_hist
261
- sym['_hist deviation'] = deviation_hist
262
-
263
- return sym
264
- # print ('average corners', np.median(corners_hist))
265
-
266
-
267
- def define_symmetry(tags):
268
- # make dictionary to store
269
- if 'symmetry' in tags:
270
- tags['symmetry'].clear()
271
-
272
- tags['symmetry'] = {}
273
- sym = tags['symmetry']
274
- if 'latticeType' in tags:
275
- lattice_types = ['None', 'Find Lattice', 'hexagonal', 'honeycomb', 'square', 'square centered',
276
- 'diamond', 'fcc']
277
- sym['lattice'] = lattice_types[tags['latticeType']]
278
-
279
- sym['number of atoms'] = len(tags['atoms'])
280
-
281
- points = []
282
- for i in range(sym['number of atoms']):
283
- sym[str(i)] = {}
284
- sym[str(i)]['index'] = i
285
- sym[str(i)]['x'] = tags['atoms'][i][0]
286
- sym[str(i)]['y'] = tags['atoms'][i][1]
287
- sym[str(i)]['intensity'] = tags['atoms'][i][3]
288
- sym[str(i)]['maximum'] = tags['atoms'][i][4]
289
- sym[str(i)]['position'] = 'inside'
290
- sym[str(i)]['Z'] = 0
291
- sym[str(i)]['Name'] = 'undefined'
292
- sym[str(i)]['Column'] = -1
293
-
294
- points.append([int(sym[str(i)]['x'] + 0.5), int(sym[str(i)]['y'] + 0.5)])
295
-
296
- # self.points = points.copy()
297
-
298
-
299
- def voronoi(atoms, tags):
300
- im = tags['image']
301
- vor = spatial.Voronoi(np.array(atoms)[:, 0:2]) # Plot it:
302
- rim_vertices = []
303
- for i in range(len(vor.vertices)):
304
-
305
- if (vor.vertices[i, 0:2] < 0).any() or (vor.vertices[i, 0:2] > im.shape[0] - 5).any():
306
- rim_vertices.append(i)
307
- rim_vertices = set(rim_vertices)
308
- mid_vertices = list(set(np.arange(len(vor.vertices))).difference(rim_vertices))
309
-
310
- mid_regions = []
311
- for region in vor.regions: # Check all Voronoi polygons
312
- # we get a lot of rim (-1) and empty and regions
313
- if all(x in mid_vertices for x in region) and len(region) > 1:
314
- mid_regions.append(region)
315
- tags['atoms']['voronoi'] = vor
316
- tags['atoms']['voronoi_vertices'] = vor.vertices
317
- tags['atoms']['voronoi_regions'] = vor.regions
318
- tags['atoms']['voronoi_midVerticesIndices'] = mid_vertices
319
- tags['atoms']['voronoi_midVertices'] = vor.vertices[mid_vertices]
320
- tags['atoms']['voronoi_midRegions'] = mid_regions
321
-
322
-
323
- def voronoi2(tags, atoms):
324
- sym = tags['symmetry']
325
- points = []
326
-
327
- for i in range(sym['number of atoms']):
328
- points.append([int(sym[str(i)]['x'] + 0.5), int(sym[str(i)]['y'] + 0.5)])
329
-
330
- # points = np.array(atoms[:][0:2])
331
- vor = spatial.Voronoi(points)
332
-
333
- sym['voronoi'] = vor
334
-
335
- nn_tree = spatial.cKDTree(points)
336
-
337
- rim = []
338
- regions = []
339
-
340
- # ##
341
- # We get all the vertice length
342
-
343
- lengths = []
344
- for vertice in vor.ridge_vertices:
345
- if all(v >= 0 for v in vertice):
346
- p1 = vor.vertices[vertice[0]]
347
- p2 = vor.vertices[vertice[1]]
348
- lengths.append(np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2))
349
-
350
- sym['lengths'] = lengths
351
- sym['median lengths'] = np.median(lengths)
352
- sym['Min Voronoi Edge'] = np.median(lengths) / 1.5
353
- # print ('median lengths', np.median(lengths))
354
- # print ('Min Voronoi Edge',np.median(lengths)/1.5)
355
- corners_hist = []
356
- nn_hist = []
357
- nn_dist_hist = []
358
- angle_hist = []
359
- area_hist = []
360
- deviation_hist = []
361
-
362
- for i, region in enumerate(vor.point_region):
363
- x, y = points[i]
364
-
365
- vertices = vor.regions[region]
366
-
367
- # ##
368
- # We get all the rim atoms
369
- # ##
370
-
371
- if all(v >= 0 and all(vor.vertices[v] > 0) and all(vor.vertices[v] < tags['data'].shape[0]) for v in vertices):
372
- # finite regions only now
373
- # negative and too large vertices (corners) are excluded
374
-
375
- regions.append(vertices)
376
- poly = []
377
- for v in vertices:
378
- poly.append(vor.vertices[v])
379
-
380
- area, cx, cy = polygon_area(poly)
381
- cx = abs(cx)
382
- cy = abs(cy)
383
-
384
- angles = polygon_angles(poly)
385
- angle_hist.append(angles)
386
- area_hist.append(area)
387
- deviation_hist.append(np.sqrt((cx - x) ** 2 + (cy - y) ** 2))
388
-
389
- sym[str(i)]['xy'] = [x, y]
390
- sym[str(i)]['geometric'] = [cx, cy]
391
- sym[str(i)]['area'] = area
392
-
393
- sym[str(i)]['angles'] = angles
394
- sym[str(i)]['off center'] = [cx - x, cy - y]
395
-
396
- sym[str(i)]['position'] = 'inside'
397
- sym[str(i)]['corner'] = vertices
398
- sym[str(i)]['vertices'] = poly
399
- sym[str(i)]['corners'] = len(vertices)
400
- corners_hist.append(len(vertices))
401
- nn = 0
402
- nn_vor = []
403
- length = []
404
- for j in range(len(vertices)):
405
- k = (j + 1) % len(vertices)
406
- p1 = vor.vertices[vertices[j]]
407
- p2 = vor.vertices[vertices[k]]
408
- leng = np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
409
- length.append(leng)
410
- sym[str(i)]['length'] = length
411
- if leng > sym['Min Voronoi Edge']:
412
- nn += 1
413
- nn_vor.append(vertices[j])
414
- sym[str(i)]['length'] = length
415
- nn_p = nn_tree.query(points[i], k=nn + 1)
416
- sym[str(i)]['neighbors'] = []
417
- sym[str(i)]['nn Distance'] = []
418
- sym[str(i)]['nn'] = nn
419
- if nn > 0:
420
- nn_hist.append(nn)
421
- for j in range(1, len(nn_p[0])):
422
- sym[str(i)]['nn Distance'].append(nn_p[0][j])
423
- sym[str(i)]['neighbors'].append(nn_p[1][j])
424
- nn_dist_hist.append(nn_p[0][j])
425
- else:
426
- rim.append(i)
427
- sym[str(i)]['position'] = 'rim'
428
- sym[str(i)]['corners'] = 0
429
- print('weird nn determination', i)
430
- else:
431
- rim.append(i)
432
- sym[str(i)]['position'] = 'rim'
433
- sym[str(i)]['corners'] = 0
434
- sym[str(i)]['xy'] = [x, y]
435
-
436
- sym['average corners'] = np.median(corners_hist)
437
- sym['average area'] = np.median(area_hist)
438
- sym['num atoms at rim'] = len(rim)
439
- sym['num voronoi'] = len(points) - len(rim)
440
- sym['Median Coordination'] = np.median(nn_hist)
441
- sym['Median NN Distance'] = np.median(nn_dist_hist)
442
-
443
- sym['_hist corners'] = corners_hist
444
- sym['_hist area'] = area_hist
445
- sym['atoms at rim'] = rim
446
- sym['_hist Coordination'] = nn_hist
447
- sym['_hist NN Distance'] = nn_dist_hist
448
- sym['_hist deviation'] = deviation_hist
449
-
450
- # print ('average corners', np.median(corners_hist))
1
+ """
2
+
3
+ """
4
+ import numpy as np
5
+ # import ase
6
+ import sys
7
+
8
+ # from scipy.spatial import cKDTree, Voronoi, ConvexHull
9
+ import scipy.spatial
10
+ import scipy.optimize
11
+ import scipy.interpolate
12
+
13
+ from skimage.measure import grid_points_in_poly, points_in_poly
14
+
15
+ # import plotly.graph_objects as go
16
+ # import plotly.express as px
17
+ import matplotlib.patches as patches
18
+
19
+ import pyTEMlib.crystal_tools
20
+ from tqdm.auto import tqdm, trange
21
+
22
+ from .graph_viz import *
23
+
24
+
25
+ ###########################################################################
26
+ # utility functions
27
+ ###########################################################################
28
+
29
+ def interstitial_sphere_center(vertex_pos, atom_radii, optimize=True):
30
+ """
31
+ Function finds center and radius of the largest interstitial sphere of a simplex.
32
+ Which is the center of the cirumsphere if all atoms have the same radius,
33
+ but differs for differently sized atoms.
34
+ In the last case, the circumsphere center is used as starting point for refinement.
35
+
36
+ Parameters
37
+ -----------------
38
+ vertex_pos : numpy array
39
+ The position of vertices of a tetrahedron
40
+ atom_radii : float
41
+ bond radii of atoms
42
+ optimize: boolean
43
+ whether atom bond lengths are optimized or not
44
+ Returns
45
+ ----------
46
+ new_center : numpy array
47
+ The center of the largest interstitial sphere
48
+ radius : float
49
+ The radius of the largest interstitial sphere
50
+ """
51
+ center, radius = circum_center(vertex_pos, tol=1e-4)
52
+
53
+ def distance_deviation(sphere_center):
54
+ return np.std(np.linalg.norm(vertex_pos - sphere_center, axis=1) - atom_radii)
55
+
56
+ if np.std(atom_radii) == 0 or not optimize:
57
+ return center, radius-atom_radii[0]
58
+ else:
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, tol=1e-4):
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 https://codereview.stackexchange.com/questions/77593/calculating-the-volume-of-a-tetrahedron
71
+
72
+ Parameters
73
+ -----------------
74
+ vertex_pos : numpy array
75
+ The position of vertices of a tetrahedron
76
+ tol : float
77
+ Tolerance defined to identify co-planar tetrahedrons
78
+ Returns
79
+ ----------
80
+ circum_center : numpy array
81
+ The center of the circumsphere
82
+ circum_radius : float
83
+ The radius of the circumsphere
84
+ """
85
+
86
+ # Make Cayley-Menger Matrix
87
+ number_vertices = len(vertex_pos)
88
+ matrix_c = np.identity(number_vertices+1)*-1+1
89
+ distances = scipy.spatial.distance.pdist(np.asarray(vertex_pos, dtype=float), metric='sqeuclidean')
90
+ matrix_c[1:, 1:] = scipy.spatial.distance.squareform(distances)
91
+ det_matrix_c = (np.linalg.det(matrix_c))
92
+ if abs(det_matrix_c) < tol:
93
+ return np.array(vertex_pos[0]*0), 0
94
+ matrix = -2 * np.linalg.inv(matrix_c)
95
+
96
+ center = vertex_pos[0, :]*0
97
+ for i in range(number_vertices):
98
+ center += matrix[0, i+1] * vertex_pos[i, :]
99
+ center /= np.sum(matrix[0, 1:])
100
+
101
+ circum_radius = np.sqrt(matrix[0, 0]) / 2
102
+
103
+ return np.array(center), circum_radius
104
+
105
+
106
+ def voronoi_volumes(atoms):
107
+ """
108
+ Volumes of voronoi cells from
109
+ https://stackoverflow.com/questions/19634993/volume-of-voronoi-cell-python
110
+ """
111
+ points = atoms.positions
112
+ v = scipy.spatial.Voronoi(points)
113
+ vol = np.zeros(v.npoints)
114
+ for i, reg_num in enumerate(v.point_region):
115
+ indices = v.regions[reg_num]
116
+ if -1 in indices: # some regions can be opened
117
+ vol[i] = 0
118
+ else:
119
+ try:
120
+ hull = scipy.spatial.ConvexHull(v.simplices[indices])
121
+ vol[i] = hull.volume
122
+ except:
123
+ vol[i] = 0.
124
+
125
+ if atoms.info is None:
126
+ atoms.info = {}
127
+ # atoms.info.update({'volumes': vol})
128
+ return vol
129
+
130
+
131
+ def get_bond_radii(atoms, bond_type='bond'):
132
+ """ get all bond radii from Kirkland
133
+ Parameter:
134
+ ----------
135
+ atoms ase.Atoms object
136
+ structure information in ase format
137
+ type: str
138
+ type of bond 'covalent' or 'metallic'
139
+ """
140
+
141
+ r_a = []
142
+ for atom in atoms:
143
+ if atom.symbol == 'X':
144
+ r_a.append(1.2)
145
+ else:
146
+ if bond_type == 'covalent':
147
+ r_a.append(pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][0])
148
+ else:
149
+ r_a.append(pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][1])
150
+ if atoms.info is None:
151
+ atoms.info = {}
152
+ atoms.info['bond_radii'] = r_a
153
+ return r_a
154
+
155
+
156
+ def set_bond_radii(atoms, bond_type='bond'):
157
+ """ set certain or all bond-radii taken from Kirkland
158
+
159
+ Bond_radii are also stored in atoms.info
160
+
161
+ Parameter:
162
+ ----------
163
+ atoms ase.Atoms object
164
+ structure information in ase format
165
+ type: str
166
+ type of bond 'covalent' or 'metallic'
167
+ Return:
168
+ -------
169
+ r_a: list
170
+ list of atomic bond-radii
171
+
172
+ """
173
+ if atoms.info is None:
174
+ atoms.info = {}
175
+ if 'bond_radii' in atoms.info:
176
+ r_a = atoms.info['bond_radii']
177
+ else:
178
+ r_a = np.ones(len(atoms))
179
+
180
+ for atom in atoms:
181
+ if bond_type == 'covalent':
182
+ r_a[atom.index] = (pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][0])
183
+ else:
184
+ r_a[atom.index] = (pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][1])
185
+ atoms.info['bond_radii'] = r_a
186
+ return r_a
187
+
188
+
189
+ def get_voronoi(tetrahedra, atoms, bond_radii=None, optimize=True):
190
+ """
191
+ Find Voronoi vertices and keep track of associated tetrahedrons and interstitial radii
192
+
193
+ Used in find_polyhedra function
194
+
195
+ Parameters
196
+ ----------
197
+ tetrahedra: scipy.spatial.Delaunay object
198
+ Delaunay tesselation
199
+ atoms: ase.Atoms object
200
+ the structural information
201
+ optimize: boolean
202
+ whether to use different atom radii or not
203
+
204
+ Returns
205
+ -------
206
+ voronoi_vertices: list
207
+ list of positions of voronoi vertices
208
+ voronoi_tetrahedra:
209
+ list of indices of associated vertices of tetrahedra
210
+ r_vv: list of float
211
+ list of all interstitial sizes
212
+ """
213
+
214
+ extent = atoms.cell.lengths()
215
+ if atoms.info is None:
216
+ atoms.info = {}
217
+
218
+ if bond_radii is not None:
219
+ bond_radii = [bond_radii]*len(atoms)
220
+ elif 'bond_radii' in atoms.info:
221
+ bond_radii = atoms.info['bond_radii']
222
+
223
+ else:
224
+ bond_radii = get_bond_radii(atoms)
225
+
226
+ voronoi_vertices = []
227
+ voronoi_tetrahedrons = []
228
+ r_vv = []
229
+ r_aa = []
230
+ print('Find interstitials (finding centers for different elements takes a bit)')
231
+ for vertices in tqdm(tetrahedra.simplices):
232
+ r_a = []
233
+ for vert in vertices:
234
+ r_a.append(bond_radii[vert])
235
+ voronoi, radius = interstitial_sphere_center(atoms.positions[vertices], r_a, optimize=optimize)
236
+
237
+ r_a = np.average(r_a) # np.min(r_a)
238
+ r_aa.append(r_a)
239
+
240
+ if (voronoi >= 0).all() and (extent - voronoi > 0).all() and radius > 0.01:
241
+ voronoi_vertices.append(voronoi)
242
+ voronoi_tetrahedrons.append(vertices)
243
+ r_vv.append(radius)
244
+ return voronoi_vertices, voronoi_tetrahedrons, r_vv, np.max(r_aa)
245
+
246
+
247
+ def find_overlapping_spheres(voronoi_vertices, r_vv, r_a, cheat=1.):
248
+ """Find overlapping spheres"""
249
+
250
+ vertex_tree = scipy.spatial.cKDTree(voronoi_vertices)
251
+
252
+ pairs = vertex_tree.query_pairs(r=r_a * 2)
253
+
254
+ overlapping_pairs = []
255
+ for (i, j) in pairs:
256
+ if np.linalg.norm(voronoi_vertices[i] - voronoi_vertices[j]) < (r_vv[i] + r_vv[j]) * cheat:
257
+ overlapping_pairs.append([i, j])
258
+
259
+ return np.array(sorted(overlapping_pairs))
260
+
261
+
262
+ def find_interstitial_clusters(overlapping_pairs):
263
+ """Make clusters
264
+ Breadth first search to go through the list of overlapping spheres or circles to determine clusters
265
+ """
266
+ visited_all = []
267
+ clusters = []
268
+ for initial in overlapping_pairs[:, 0]:
269
+ if initial not in visited_all:
270
+ # breadth first search
271
+ visited = [] # the atoms we visited
272
+ queue = [initial]
273
+ while queue:
274
+ node = queue.pop(0)
275
+ if node not in visited_all:
276
+ visited.append(node)
277
+ visited_all.append(node)
278
+ # neighbors = overlapping_pairs[overlapping_pairs[:,0]==node,1]
279
+ neighbors = np.append(overlapping_pairs[overlapping_pairs[:, 1] == node, 0],
280
+ overlapping_pairs[overlapping_pairs[:, 0] == node, 1])
281
+
282
+ for i, neighbour in enumerate(neighbors):
283
+ if neighbour not in visited:
284
+ queue.append(neighbour)
285
+ clusters.append(visited)
286
+ return clusters, visited_all
287
+
288
+
289
+ def make_polygons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all):
290
+ """ make polygons from convex hulls of vertices around interstitial positions"""
291
+ polyhedra = {}
292
+ for index, cluster in tqdm(enumerate(clusters)):
293
+ cc = []
294
+ for c in cluster:
295
+ cc = cc + list(voronoi_tetrahedrons[c])
296
+
297
+ hull = scipy.spatial.ConvexHull(atoms.positions[list(set(cc)), :2])
298
+ faces = []
299
+ triangles = []
300
+ for s in hull.simplices:
301
+ faces.append(atoms.positions[list(set(cc))][s])
302
+ triangles.append(list(s))
303
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
304
+ 'faces': faces, 'triangles': triangles,
305
+ 'length': len(list(set(cc))),
306
+ 'combined_vertices': cluster,
307
+ 'interstitial_index': index,
308
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
309
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))]} # , 'volume': hull.volume}
310
+ # 'coplanar': hull.coplanar}
311
+
312
+ print('Define conventional interstitial polyhedra')
313
+ running_number = index + 0
314
+ for index in trange(len(voronoi_vertices)):
315
+ if index not in visited_all:
316
+ vertices = voronoi_tetrahedrons[index]
317
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices, :2])
318
+ faces = []
319
+ triangles = []
320
+ for s in hull.simplices:
321
+ faces.append(atoms.positions[vertices][s])
322
+ triangles.append(list(s))
323
+
324
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
325
+ 'faces': faces, 'triangles': triangles,
326
+ 'length': len(vertices),
327
+ 'combined_vertices': index,
328
+ 'interstitial_index': running_number,
329
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
330
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices]}
331
+ # 'volume': hull.volume}
332
+
333
+ running_number += 1
334
+
335
+ return polyhedra
336
+
337
+
338
+ def make_polyhedrons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all):
339
+ """collect output data and make dictionary"""
340
+
341
+ polyhedra = {}
342
+ import scipy.sparse
343
+ connectivity_matrix = scipy.sparse.dok_matrix((len(atoms), len(atoms)), dtype=bool)
344
+
345
+ print('Define clustered interstitial polyhedra')
346
+ for index, cluster in tqdm(enumerate(clusters)):
347
+ cc = []
348
+ for c in cluster:
349
+ cc = cc + list(voronoi_tetrahedrons[c])
350
+ cc = list(set(cc))
351
+
352
+ hull = scipy.spatial.ConvexHull(atoms.positions[cc])
353
+ faces = []
354
+ triangles = []
355
+ for s in hull.simplices:
356
+ faces.append(atoms.positions[cc][s])
357
+ triangles.append(list(s))
358
+ for k in range(len(s)):
359
+ l = (k + 1) % len(s)
360
+ if cc[s[k]] > cc[s[l]]:
361
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
362
+ else:
363
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
364
+
365
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
366
+ 'faces': faces, 'triangles': triangles,
367
+ 'length': len(list(set(cc))),
368
+ 'combined_vertices': cluster,
369
+ 'interstitial_index': index,
370
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
371
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))],
372
+ 'volume': hull.volume}
373
+ # 'coplanar': hull.coplanar}
374
+
375
+ print('Define conventional interstitial polyhedra')
376
+ running_number = index + 0
377
+ for index in range(len(voronoi_vertices)):
378
+ if index not in visited_all:
379
+ vertices = voronoi_tetrahedrons[index]
380
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices])
381
+ faces = []
382
+ triangles = []
383
+ for s in hull.simplices:
384
+ faces.append(atoms.positions[vertices][s])
385
+ triangles.append(list(s))
386
+ for k in range(len(s)):
387
+ l = (k + 1) % len(s)
388
+ if cc[s[k]] > cc[s[l]]:
389
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
390
+ else:
391
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
392
+
393
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
394
+ 'faces': faces, 'triangles': triangles,
395
+ 'length': len(vertices),
396
+ 'combined_vertices': index,
397
+ 'interstitial_index': running_number,
398
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
399
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices],
400
+ 'volume': hull.volume}
401
+
402
+ running_number += 1
403
+ if atoms.info is None:
404
+ atoms.info = {}
405
+ atoms.info.update({'graph': {'connectivity_matrix': connectivity_matrix}})
406
+ return polyhedra
407
+
408
+
409
+ ##################################################################
410
+ # polyhedra functions
411
+ ##################################################################
412
+
413
+ def get_non_periodic_supercell(super_cell):
414
+ super_cell.wrap()
415
+ atoms = super_cell*3
416
+ atoms.positions -= super_cell.cell.lengths()
417
+ atoms.positions[:,0] += super_cell.cell[0,0]*.0
418
+ del(atoms[atoms.positions[: , 0]<-5])
419
+ del(atoms[atoms.positions[: , 0]>super_cell.cell[0,0]+5])
420
+ del(atoms[atoms.positions[: , 1]<-5])
421
+ del(atoms[atoms.positions[: , 1]>super_cell.cell[1,1]+5])
422
+ del(atoms[atoms.positions[: , 2]<-5])
423
+ del(atoms[atoms.positions[: , 2]>super_cell.cell[2,2]+5])
424
+ return atoms
425
+
426
+ def get_connectivity_matrix(crystal, atoms, polyhedra):
427
+ crystal_tree = scipy.spatial.cKDTree(crystal.positions)
428
+
429
+
430
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=int)
431
+
432
+ for polyhedron in polyhedra.values():
433
+ vertices = polyhedron['vertices'] - crystal.cell.lengths()
434
+ atom_ind = np.array(polyhedron['indices'])
435
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
436
+ to_bond = np.where(dd<0.001)[0]
437
+
438
+ for triangle in polyhedron['triangles']:
439
+ triangle = np.array(triangle)
440
+ for permut in [[0,1], [1,2], [0,2]]:
441
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
442
+ if vertex[0] in to_bond or vertex[1] in to_bond:
443
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = 1
444
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = 1
445
+ return connectivity_matrix
446
+
447
+
448
+
449
+ def get_bonds(crystal, shift= 0., verbose = False, cheat=1.0):
450
+ """
451
+ Get polyhedra, and bonds from and edges and lengths of edges for each polyhedron and store it in info dictionary of new ase.Atoms object
452
+
453
+ Parameter:
454
+ ----------
455
+ crystal: ase.atoms_object
456
+ information on all polyhedra
457
+ """
458
+ crystal.positions += shift * crystal.cell[0, 0]
459
+ crystal.wrap()
460
+
461
+ atoms = get_non_periodic_supercell(crystal)
462
+ atoms = atoms[atoms.numbers.argsort()]
463
+
464
+
465
+ atoms.positions += crystal.cell.lengths()
466
+ polyhedra = find_polyhedra(atoms, cheat=cheat)
467
+
468
+ connectivity_matrix = get_connectivity_matrix(crystal, atoms, polyhedra)
469
+ coord = connectivity_matrix.sum(axis=1)
470
+
471
+ del(atoms[np.where(coord==0)])
472
+ new_polyhedra = {}
473
+ index = 0
474
+ octahedra =[]
475
+ tetrahedra = []
476
+ other = []
477
+ super_cell_atoms =[]
478
+
479
+ atoms_tree = scipy.spatial.cKDTree(atoms.positions-crystal.cell.lengths())
480
+ crystal_tree = scipy.spatial.cKDTree(crystal.positions)
481
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=float)
482
+
483
+ for polyhedron in polyhedra.values():
484
+ polyhedron['vertices'] -= crystal.cell.lengths()
485
+ vertices = polyhedron['vertices']
486
+ center = np.average(polyhedron['vertices'], axis=0)
487
+
488
+ dd, polyhedron['indices'] = atoms_tree.query(vertices , k=1)
489
+ atom_ind = (np.array(polyhedron['indices']))
490
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
491
+
492
+ to_bond = np.where(dd<0.001)[0]
493
+ super_cell_atoms.extend(list(atom_ind[to_bond]))
494
+
495
+ edges = []
496
+ lengths = []
497
+ for triangle in polyhedron['triangles']:
498
+ triangle = np.array(triangle)
499
+ for permut in [[0,1], [1,2], [0,2]]:
500
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
501
+ length = np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]])
502
+ if vertex[0] in to_bond or vertex[1] in to_bond:
503
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = length
504
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = length
505
+ if vertex[0] not in to_bond:
506
+ atoms[atom_ind[vertex[0]]].symbol = 'Be'
507
+ if vertex[1] not in to_bond:
508
+ atoms[atom_ind[vertex[1]]].symbol = 'Be'
509
+ if vertex not in edges:
510
+ edges.append(vertex)
511
+ lengths.append(np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]] ))
512
+ polyhedron['edges'] = edges
513
+ polyhedron['edge_lengths'] = lengths
514
+ if all(center > -0.000001) and all(center < crystal.cell.lengths()-0.01):
515
+ new_polyhedra[str(index)]=polyhedron
516
+ if polyhedron['length'] == 4:
517
+ tetrahedra.append(str(index))
518
+ elif polyhedron['length'] == 6:
519
+ octahedra.append(str(index))
520
+ else:
521
+ other.append(str(index))
522
+ if verbose:
523
+ print(polyhedron['length'])
524
+ index += 1
525
+ atoms.positions -= crystal.cell.lengths()
526
+ coord = connectivity_matrix.copy()
527
+ coord[np.where(coord>.1)] = 1
528
+ coord = coord.sum(axis=1)
529
+
530
+ super_cell_atoms = np.sort(np.unique(super_cell_atoms))
531
+ atoms.info.update({'polyhedra': {'polyhedra': new_polyhedra,
532
+ 'tetrahedra': tetrahedra,
533
+ 'octahedra': octahedra,
534
+ 'other' : other}})
535
+ atoms.info.update({'bonds': {'connectivity_matrix': connectivity_matrix,
536
+ 'super_cell_atoms': super_cell_atoms,
537
+ 'super_cell_dimensions': crystal.cell.array,
538
+ 'coordination': coord}})
539
+ atoms.info.update({'supercell': crystal})
540
+ return atoms
541
+
542
+ def plot_atoms(atoms: ase.Atoms, polyhedra_indices=None, plot_bonds=False, color='', template=None, atom_size=None, max_size=35) -> go.Figure:
543
+ """
544
+ Plot structure in a ase.Atoms object with plotly
545
+
546
+ If the info dictionary of the atoms object contains bond or polyedra information, these can be set tobe plotted
547
+
548
+ Partameter:
549
+ -----------
550
+ atoms: ase.Atoms object
551
+ structure of supercell
552
+ polyhedra_indices: list of integers
553
+ indices of polyhedra to be plotted
554
+ plot_bonds: boolean
555
+ whether to plot bonds or not
556
+
557
+ Returns:
558
+ --------
559
+ fig: plotly figure object
560
+ handle to figure needed to modify appearance
561
+ """
562
+ energies = np.zeros(len(atoms))
563
+ if 'bonds' in atoms.info:
564
+ if 'atom_energy' in atoms.info['bonds']:
565
+ energies = np.round(np.array(atoms.info['bonds']['atom_energy'] - 12 * atoms.info['bonds']['ideal_bond_energy']) *1000,0)
566
+
567
+ for atom in atoms:
568
+ if atom.index not in atoms.info['bonds']['super_cell_atoms']:
569
+ energies[atom.index] = 0.
570
+ if color == 'coordination':
571
+ colors = atoms.info['bonds']['coordination']
572
+ elif color == 'layer':
573
+ colors = atoms.positions[:, 2]
574
+ elif color == 'energy':
575
+ colors = energies
576
+ colors[colors>50] = 50
577
+ colors = np.log(1+ energies)
578
+
579
+ else:
580
+ colors = atoms.get_atomic_numbers()
581
+
582
+ if atom_size is None:
583
+ atom_size = atoms.get_atomic_numbers()*4
584
+ elif isinstance(atom_size, float):
585
+ atom_size = atoms.get_atomic_numbers()*4*atom_size
586
+ atom_size[atom_size>max_size] = max_size
587
+ elif isinstance(atom_size, int):
588
+ atom_size = [atom_size]*len(atoms)
589
+ if len(atom_size) != len(atoms):
590
+ atom_size = [10]*len(atoms)
591
+ print('wrong length of atom_size parameter')
592
+ plot_polyhedra = False
593
+ data = []
594
+ if polyhedra_indices is not None:
595
+ if 'polyhedra' in atoms.info:
596
+ if polyhedra_indices == -1:
597
+ data = plot_polyhedron(atoms.info['polyhedra']['polyhedra'], range(len(atoms.info['polyhedra']['polyhedra'])))
598
+ plot_polyhedra = True
599
+ elif isinstance(polyhedra_indices, list):
600
+ data = plot_polyhedron(atoms.info['polyhedra']['polyhedra'], polyhedra_indices)
601
+ plot_polyhedra = True
602
+ text = []
603
+ if 'bonds' in atoms.info:
604
+ coord = atoms.info['bonds']['coordination']
605
+ for atom in atoms:
606
+ if atom.index in atoms.info['bonds']['super_cell_atoms']:
607
+
608
+ text.append(f'Atom {atom.index}: coordination={coord[atom.index]}' +
609
+ f'x:{atom.position[0]:.2f} \n y:{atom.position[1]:.2f} \n z:{atom.position[2]:.2f}')
610
+ if 'atom_energy' in atoms.info['bonds']:
611
+ text[-1] += f"\n energy: {energies[atom.index]:.0f} meV"
612
+ else:
613
+ text.append('')
614
+ else:
615
+ text = [''] * len(atoms)
616
+
617
+ if plot_bonds:
618
+ data += get_plot_bonds(atoms)
619
+ if plot_polyhedra or plot_bonds:
620
+ fig = go.Figure(data=data)
621
+ else:
622
+ fig = go.Figure()
623
+ if color=='energy':
624
+ fig.add_trace(go.Scatter3d(
625
+ mode='markers',
626
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
627
+ hovertemplate='<b>%{text}</b><extra></extra>',
628
+ text = text,
629
+ marker=dict(
630
+ color=colors,
631
+ size=atom_size,
632
+ sizemode='diameter',
633
+ colorscale='Rainbow', #px.colors.qualitative.Light24,
634
+ colorbar=dict(thickness=10, orientation='h'))))
635
+ #hover_name = colors))) # ["blue", "green", "red"])))
636
+
637
+ elif 'bonds' in atoms.info:
638
+ fig.add_trace(go.Scatter3d(
639
+ mode='markers',
640
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
641
+ hovertemplate='<b>%{text}</b><extra></extra>',
642
+ text = text,
643
+ marker=dict(
644
+ color=colors,
645
+ size=atom_size,
646
+ sizemode='diameter',
647
+ colorscale= px.colors.qualitative.Light24)))
648
+ #hover_name = colors))) # ["blue", "green", "red"])))
649
+
650
+ else:
651
+ fig.add_trace(go.Scatter3d(
652
+ mode='markers',
653
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
654
+ marker=dict(
655
+ color=colors,
656
+ size=atom_size,
657
+ sizemode='diameter',
658
+ colorbar=dict(thickness=10),
659
+ colorscale= px.colors.qualitative.Light24)))
660
+ #hover_name = colors))) # ["blue", "green", "red"])))
661
+ fig.update_layout(width=1000, height=700, showlegend=False, template=template)
662
+ fig.update_layout(scene_aspectmode='data',
663
+ scene_aspectratio=dict(x=1, y=1, z=1))
664
+
665
+ camera = {'up': {'x': 0, 'y': 1, 'z': 0},
666
+ 'center': {'x': 0, 'y': 0, 'z': 0},
667
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
668
+ fig.update_coloraxes(showscale=True)
669
+ fig.update_layout(scene_camera=camera, title=r"Al-GB $")
670
+ fig.update_scenes(camera_projection_type="orthographic" )
671
+ fig.show()
672
+ return fig
673
+
674
+
675
+
676
+
677
+ def find_polyhedra(atoms, optimize=True, cheat=1.0, bond_radii=None):
678
+ """ get polyhedra information from an ase.Atoms object
679
+
680
+ This is following the method of Banadaki and Patala
681
+ http://dx.doi.org/10.1038/s41524-017-0016-0
682
+
683
+ We are using the bond radius according to Kirkland, which is tabulated in
684
+ - pyTEMlib.crystal_tools.electronFF[atoms.symbols[vert]]['bond_length'][1]
685
+
686
+ Parameter
687
+ ---------
688
+ atoms: ase.Atoms object
689
+ the structural information
690
+ cheat: float
691
+ does not exist
692
+
693
+ Returns
694
+ -------
695
+ polyhedra: dict
696
+ dictionary with all information of polyhedra
697
+ """
698
+ if not isinstance(atoms, ase.Atoms):
699
+ raise TypeError('This function needs an ase.Atoms object')
700
+
701
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
702
+ tetrahedra = scipy.spatial.Delaunay(atoms.positions[:, :2])
703
+ else:
704
+ tetrahedra = scipy.spatial.Delaunay(atoms.positions)
705
+
706
+ voronoi_vertices, voronoi_tetrahedrons, r_vv, r_a = get_voronoi(tetrahedra, atoms, optimize=optimize, bond_radii=bond_radii)
707
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
708
+ r_vv = np.array(r_vv)*3.
709
+ overlapping_pairs = find_overlapping_spheres(voronoi_vertices, r_vv, r_a, cheat=cheat)
710
+
711
+ clusters, visited_all = find_interstitial_clusters(overlapping_pairs)
712
+
713
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
714
+ rings = get_polygons(atoms, clusters, voronoi_tetrahedrons)
715
+ return rings
716
+ else:
717
+ polyhedra = make_polyhedrons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all)
718
+ return polyhedra
719
+
720
+
721
+ def polygon_sort(corners):
722
+ center = np.average(corners[:, :2], axis=0)
723
+ angles = (np.arctan2(corners[:,0]-center[0], corners[:,1]-center[1]) + 2.0 * np.pi)% (2.0 * np.pi)
724
+ return corners[np.argsort(angles)]
725
+
726
+ def get_polygons(atoms, clusters, voronoi_tetrahedrons):
727
+ polygons = []
728
+ cyclicity = []
729
+ centers = []
730
+ corners =[]
731
+ for index, cluster in (enumerate(clusters)):
732
+ cc = []
733
+ for c in cluster:
734
+ cc = cc + list(voronoi_tetrahedrons[c])
735
+
736
+ sorted_corners = polygon_sort(atoms.positions[list(set(cc)), :2])
737
+ cyclicity.append(len(sorted_corners))
738
+ corners.append(sorted_corners)
739
+ centers.append(np.mean(sorted_corners[:,:2], axis=0))
740
+ polygons.append(patches.Polygon(np.array(sorted_corners)[:,:2], closed=True, fill=True, edgecolor='red'))
741
+
742
+ rings={'atoms': atoms.positions[:, :2],
743
+ 'cyclicity': np.array(cyclicity),
744
+ 'centers': np.array(centers),
745
+ 'corners': corners,
746
+ 'polygons': polygons}
747
+ return rings
748
+
749
+
750
+ def sort_polyhedra_by_vertices(polyhedra, visible=range(4, 100), z_lim=[0, 100], verbose=False):
751
+ indices = []
752
+
753
+ for key, polyhedron in polyhedra.items():
754
+ if 'length' not in polyhedron:
755
+ polyhedron['length'] = len(polyhedron['vertices'])
756
+
757
+ if polyhedron['length'] in visible:
758
+ center = polyhedron['vertices'].mean(axis=0)
759
+ if z_lim[0] < center[2] < z_lim[1]:
760
+ indices.append(key)
761
+ if verbose:
762
+ print(key, polyhedron['length'], center)
763
+ return indices
764
+
765
+ # color_scheme = ['lightyellow', 'silver', 'rosybrown', 'lightsteelblue', 'orange', 'cyan', 'blue', 'magenta',
766
+ # 'firebrick', 'forestgreen']
767
+
768
+
769
+
770
+ ##########################
771
+ # New Graph Stuff
772
+ ##########################
773
+ def breadth_first_search(graph, initial, projected_crystal):
774
+ """ breadth first search of atoms viewed as a graph
775
+
776
+ the projection dictionary has to contain the following items
777
+ 'number_of_nearest_neighbours', 'rotated_cell', 'near_base', 'allowed_variation'
778
+
779
+ Parameters
780
+ ----------
781
+ graph: numpy array (Nx2)
782
+ the atom positions
783
+ initial: int
784
+ index of starting atom
785
+ projection_tags: dict
786
+ dictionary with information on projected unit cell (with 'rotated_cell' item)
787
+
788
+ Returns
789
+ -------
790
+ graph[visited]: numpy array (M,2) with M<N
791
+ positions of atoms hopped in unit cell lattice
792
+ ideal: numpy array (M,2)
793
+ ideal atom positions
794
+ """
795
+
796
+ projection_tags = projected_crystal.info['projection']
797
+
798
+ # get lattice vectors to hopp along through graph
799
+ projected_unit_cell = projected_crystal.cell[:2, :2]
800
+ a_lattice_vector = projected_unit_cell[0]
801
+ b_lattice_vector = projected_unit_cell[1]
802
+ main = np.array([a_lattice_vector, -a_lattice_vector, b_lattice_vector, -b_lattice_vector]) # vectors of unit cell
803
+ near = np.append(main, projection_tags['near_base'], axis=0) # all nearest atoms
804
+ # get k next nearest neighbours for each node
805
+ neighbour_tree = scipy.spatial.cKDTree(graph)
806
+ distances, indices = neighbour_tree.query(graph, # let's get all neighbours
807
+ k=50) # projection_tags['number_of_nearest_neighbours']*2 + 1)
808
+ # print(projection_tags['number_of_nearest_neighbours'] * 2 + 1)
809
+ visited = [] # the atoms we visited
810
+ ideal = [] # atoms at ideal lattice
811
+ sub_lattice = [] # atoms in base and disregarded
812
+ queue = [initial]
813
+ ideal_queue = [graph[initial]]
814
+
815
+ while queue:
816
+ node = queue.pop(0)
817
+ ideal_node = ideal_queue.pop(0)
818
+
819
+ if node not in visited:
820
+ visited.append(node)
821
+ ideal.append(ideal_node)
822
+ # print(node,ideal_node)
823
+ neighbors = indices[node]
824
+ for i, neighbour in enumerate(neighbors):
825
+ if neighbour not in visited:
826
+ distance_to_ideal = np.linalg.norm(near + graph[node] - graph[neighbour], axis=1)
827
+
828
+ if np.min(distance_to_ideal) < projection_tags['allowed_variation']:
829
+ direction = np.argmin(distance_to_ideal)
830
+ if direction > 3: # counting starts at 0
831
+ sub_lattice.append(neighbour)
832
+ elif distances[node, i] < projection_tags['distance_unit_cell'] * 1.05:
833
+ queue.append(neighbour)
834
+ ideal_queue.append(ideal_node + near[direction])
835
+
836
+ return graph[visited], ideal
837
+
838
+ ####################
839
+ # Distortion Matrix
840
+ ####################
841
+ def get_distortion_matrix(atoms, ideal_lattice):
842
+ """ Calculates distortion matrix
843
+
844
+ Calculates the distortion matrix by comparing ideal and distorted Voronoi tiles
845
+ """
846
+
847
+ vor = scipy.spatial.Voronoi(atoms)
848
+
849
+ # determine a middle Voronoi tile
850
+ ideal_vor = scipy.spatial.Voronoi(ideal_lattice)
851
+ near_center = np.average(ideal_lattice, axis=0)
852
+ index = np.argmin(np.linalg.norm(ideal_lattice - near_center, axis=0))
853
+
854
+ # the ideal vertices fo such an Voronoi tile (are there crystals with more than one voronoi?)
855
+ ideal_vertices = ideal_vor.vertices[ideal_vor.regions[ideal_vor.point_region[index]]]
856
+ ideal_vertices = get_significant_vertices(ideal_vertices - np.average(ideal_vertices, axis=0))
857
+
858
+ distortion_matrix = []
859
+ for index in range(vor.points.shape[0]):
860
+ done = int((index + 1) / vor.points.shape[0] * 50)
861
+ sys.stdout.write('\r')
862
+ # progress output :
863
+ sys.stdout.write("[%-50s] %d%%" % ('=' * done, 2 * done))
864
+ sys.stdout.flush()
865
+
866
+ # determine vertices of Voronoi polygons of an atom with number index
867
+ poly_point = vor.points[index]
868
+ poly_vertices = get_significant_vertices(vor.vertices[vor.regions[vor.point_region[index]]] - poly_point)
869
+
870
+ # where ATOM has to be moved (not pixel)
871
+ ideal_point = ideal_lattice[index]
872
+
873
+ # transform voronoi to ideal one and keep transformation matrix A
874
+ uncorrected, corrected, aa = transform_voronoi(poly_vertices, ideal_vertices)
875
+
876
+ # pixel positions
877
+ corrected = corrected + ideal_point + (np.rint(poly_point) - poly_point)
878
+ for i in range(len(corrected)):
879
+ # original image pixels
880
+ x, y = uncorrected[i] + np.rint(poly_point)
881
+ # collect the two origin and target coordinates and store
882
+ distortion_matrix.append([x, y, corrected[i, 0], corrected[i, 1]])
883
+ print()
884
+ return np.array(distortion_matrix)
885
+
886
+
887
+ def undistort(distortion_matrix, image_data):
888
+ """ Undistort image according to distortion matrix
889
+
890
+ Uses the griddata interpolation of scipy to apply distortion matrix to image.
891
+ The distortion matrix contains in origin and target pixel coordinates
892
+ target is where the pixel has to be moved (floats)
893
+
894
+ Parameters
895
+ ----------
896
+ distortion_matrix: numpy array (Nx2)
897
+ distortion matrix (format N x 2)
898
+ image_data: numpy array or sidpy.Dataset
899
+ image
900
+
901
+ Returns
902
+ -------
903
+ interpolated: numpy array
904
+ undistorted image
905
+ """
906
+
907
+ intensity_values = image_data[(distortion_matrix[:, 0].astype(int), distortion_matrix[:, 1].astype(int))]
908
+
909
+ corrected = distortion_matrix[:, 2:4]
910
+
911
+ size_x, size_y = 2 ** np.round(np.log2(image_data.shape[0:2])) # nearest power of 2
912
+ size_x = int(size_x)
913
+ size_y = int(size_y)
914
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
915
+ print('interpolate')
916
+
917
+ interpolated = scipy.interpolate.griddata(np.array(corrected), np.array(intensity_values), (grid_x, grid_y), method='linear')
918
+ return interpolated
919
+
920
+
921
+ def transform_voronoi(vertices, ideal_voronoi):
922
+ """ find transformation matrix A between a distorted polygon and a perfect reference one
923
+
924
+ Returns
925
+ -------
926
+ uncorrected: list of points:
927
+ all points on a grid within original polygon
928
+ corrected: list of points:
929
+ coordinates of these points where pixel have to move to
930
+ aa: 2x2 matrix A:
931
+ transformation matrix
932
+ """
933
+
934
+ # Find Transformation Matrix, note polygons have to be ordered first.
935
+ sort_vert = []
936
+ for vert in ideal_voronoi:
937
+ sort_vert.append(np.argmin(np.linalg.norm(vertices - vert, axis=1)))
938
+ vertices = np.array(vertices)[sort_vert]
939
+
940
+ # Solve the least squares problem X * A = Y
941
+ # to find our transformation matrix aa = A
942
+ aa, res, rank, s = np.linalg.lstsq(vertices, ideal_voronoi, rcond=None)
943
+
944
+ # expand polygon to include more points in distortion matrix
945
+ vertices2 = vertices + np.sign(vertices) # +np.sign(vertices)
946
+
947
+ ext_v = int(np.abs(vertices2).max() + 1)
948
+
949
+ polygon_grid = np.mgrid[0:ext_v * 2 + 1, :ext_v * 2 + 1] - ext_v
950
+ polygon_grid = np.swapaxes(polygon_grid, 0, 2)
951
+ polygon_array = polygon_grid.reshape(-1, polygon_grid.shape[-1])
952
+
953
+ p = points_in_poly(polygon_array, vertices2)
954
+ uncorrected = polygon_array[p]
955
+
956
+ corrected = np.dot(uncorrected, aa)
957
+
958
+ return uncorrected, corrected, aa
959
+
960
+
961
+ def get_maximum_view(distortion_matrix):
962
+ distortion_matrix_extent = np.ones(distortion_matrix.shape[1:], dtype=int)
963
+ distortion_matrix_extent[distortion_matrix[0] == -1000.] = 0
964
+
965
+ area = distortion_matrix_extent
966
+ view_square = np.array([0, distortion_matrix.shape[1] - 1, 0, distortion_matrix.shape[2] - 1], dtype=int)
967
+ while np.array(np.where(area == 0)).shape[1] > 0:
968
+ view_square = view_square + [1, -1, 1, -1]
969
+ area = distortion_matrix_extent[view_square[0]:view_square[1], view_square[2]:view_square[3]]
970
+
971
+ change = [-int(np.sum(np.min(distortion_matrix_extent[:view_square[0], view_square[2]:view_square[3]], axis=1))),
972
+ int(np.sum(np.min(distortion_matrix_extent[view_square[1]:, view_square[2]:view_square[3]], axis=1))),
973
+ -int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], :view_square[2]], axis=0))),
974
+ int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], view_square[3]:], axis=0)))]
975
+
976
+ return np.array(view_square) + change
977
+
978
+
979
+ def get_significant_vertices(vertices, distance=3):
980
+ """Calculate average for all points that are closer than distance apart, otherwise leave the points alone
981
+
982
+ Parameters
983
+ ----------
984
+ vertices: numpy array (n,2)
985
+ list of points
986
+ distance: float
987
+ (in same scale as points )
988
+
989
+ Returns
990
+ -------
991
+ ideal_vertices: list of floats
992
+ list of points that are all a minimum of 3 apart.
993
+ """
994
+
995
+ tt = scipy.spatial.cKDTree(np.array(vertices))
996
+ near = tt.query_ball_point(vertices, distance)
997
+ ideal_vertices = []
998
+ for indices in near:
999
+ if len(indices) == 1:
1000
+ ideal_vertices.append(vertices[indices][0])
1001
+ else:
1002
+ ideal_vertices.append(np.average(vertices[indices], axis=0))
1003
+ ideal_vertices = np.unique(np.array(ideal_vertices), axis=0)
1004
+ angles = np.arctan2(ideal_vertices[:, 1], ideal_vertices[:, 0])
1005
+ ang_sort = np.argsort(angles)
1006
+
1007
+ ideal_vertices = ideal_vertices[ang_sort]
1008
+
1009
+ return ideal_vertices
1010
+
1011
+
1012
+ def transform_voronoi(vertices, ideal_voronoi):
1013
+ """
1014
+ find transformation matrix A between a polygon and a perfect one
1015
+
1016
+ returns:
1017
+ list of points: all points on a grid within original polygon
1018
+ list of points: coordinates of these points where pixel have to move to
1019
+ 2x2 matrix aa: transformation matrix
1020
+ """
1021
+ # Find Transformation Matrix, note polygons have to be ordered first.
1022
+ sort_vert = []
1023
+ for vert in ideal_voronoi:
1024
+ sort_vert.append(np.argmin(np.linalg.norm(vertices - vert, axis=1)))
1025
+ vertices = np.array(vertices)[sort_vert]
1026
+
1027
+ # Solve the least squares problem X * A = Y
1028
+ # to find our transformation matrix A
1029
+ aa, res, rank, s = np.linalg.lstsq(vertices, ideal_voronoi, rcond=None)
1030
+
1031
+ # expand polygon to include more points in distortion matrix
1032
+ vertices2 = vertices + np.sign(vertices) # +np.sign(vertices)
1033
+
1034
+ ext_v = int(np.abs(vertices2).max() + 1)
1035
+
1036
+ polygon_grid = np.mgrid[0:ext_v * 2 + 1, :ext_v * 2 + 1] - ext_v
1037
+ polygon_grid = np.swapaxes(polygon_grid, 0, 2)
1038
+ polygon_array = polygon_grid.reshape(-1, polygon_grid.shape[-1])
1039
+
1040
+ p = points_in_poly(polygon_array, vertices2)
1041
+ uncorrected = polygon_array[p]
1042
+
1043
+ corrected = np.dot(uncorrected, aa)
1044
+
1045
+ return uncorrected, corrected, aa
1046
+
1047
+
1048
+
1049
+ def undistort_sitk(image_data, distortion_matrix):
1050
+ """ use simple ITK to undistort image
1051
+
1052
+ Parameters
1053
+ ----------
1054
+ image_data: numpy array with size NxM
1055
+ distortion_matrix: sidpy.Dataset or numpy array with size 2 x P x Q
1056
+ with P, Q >= M, N
1057
+
1058
+ Returns
1059
+ -------
1060
+ image: numpy array MXN
1061
+
1062
+ """
1063
+ resampler = sitk.ResampleImageFilter()
1064
+ resampler.SetReferenceImage(sitk.GetImageFromArray(image_data))
1065
+ resampler.SetInterpolator(sitk.sitkBSpline)
1066
+ resampler.SetDefaultPixelValue(0)
1067
+
1068
+ distortion_matrix2 = distortion_matrix[:, :image_data.shape[0], :image_data.shape[1]]
1069
+
1070
+ displ2 = sitk.Compose(
1071
+ [sitk.GetImageFromArray(-distortion_matrix2[1]), sitk.GetImageFromArray(-distortion_matrix2[0])])
1072
+ out_tx = sitk.DisplacementFieldTransform(displ2)
1073
+ resampler.SetTransform(out_tx)
1074
+ out = resampler.Execute(sitk.GetImageFromArray(image_data))
1075
+ return sitk.GetArrayFromImage(out)
1076
+
1077
+
1078
+ def undistort_stack_sitk(distortion_matrix, image_stack):
1079
+ """
1080
+ use simple ITK to undistort stack of image
1081
+ input:
1082
+ image: numpy array with size NxM
1083
+ distortion_matrix: h5 Dataset or numpy array with size 2 x P x Q
1084
+ with P, Q >= M, N
1085
+ output:
1086
+ image M, N
1087
+
1088
+ """
1089
+
1090
+ resampler = sitk.ResampleImageFilter()
1091
+ resampler.SetReferenceImage(sitk.GetImageFromArray(image_stack[0]))
1092
+ resampler.SetInterpolator(sitk.sitkBSpline)
1093
+ resampler.SetDefaultPixelValue(0)
1094
+
1095
+ displ2 = sitk.Compose(
1096
+ [sitk.GetImageFromArray(-distortion_matrix[1]), sitk.GetImageFromArray(-distortion_matrix[0])])
1097
+ out_tx = sitk.DisplacementFieldTransform(displ2)
1098
+ resampler.SetTransform(out_tx)
1099
+
1100
+ interpolated = np.zeros(image_stack.shape)
1101
+
1102
+ nimages = image_stack.shape[0]
1103
+
1104
+ if QT_available:
1105
+ progress = pyTEMlib.sidpy_tools.ProgressDialog("Correct Scan Distortions", nimages)
1106
+
1107
+ for i in range(nimages):
1108
+ if QT_available:
1109
+ progress.setValue(i)
1110
+ out = resampler.Execute(sitk.GetImageFromArray(image_stack[i]))
1111
+ interpolated[i] = sitk.GetArrayFromImage(out)
1112
+
1113
+ progress.setValue(nimages)
1114
+
1115
+ if QT_available:
1116
+ progress.setValue(nimages)
1117
+
1118
+ return interpolated
1119
+
1120
+
1121
+ def undistort_stack(distortion_matrix, data):
1122
+ """ Undistort stack with distortion matrix
1123
+
1124
+ Use the griddata interpolation of scipy to apply distortion matrix to image
1125
+ The distortion matrix contains in each pixel where the pixel has to be moved (floats)
1126
+
1127
+ Parameters
1128
+ ----------
1129
+ distortion_matrix: numpy array
1130
+ distortion matrix to undistort image (format image.shape[0], image.shape[2], 2)
1131
+ data: numpy array or sidpy.Dataset
1132
+ image
1133
+ """
1134
+
1135
+ corrected = distortion_matrix[:, 2:4]
1136
+ intensity_values = data[:, distortion_matrix[:, 0].astype(int), distortion_matrix[:, 1].astype(int)]
1137
+
1138
+ size_x, size_y = 2 ** np.round(np.log2(data.shape[1:])) # nearest power of 2
1139
+ size_x = int(size_x)
1140
+ size_y = int(size_y)
1141
+
1142
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
1143
+ print('interpolate')
1144
+
1145
+ interpolated = np.zeros([data.shape[0], size_x, size_y])
1146
+ nimages = data.shape[0]
1147
+ done = 0
1148
+
1149
+ if QT_available:
1150
+ progress = ft.ProgressDialog("Correct Scan Distortions", nimages)
1151
+ for i in range(nimages):
1152
+ if QT_available:
1153
+ progress.set_value(i)
1154
+ elif done < int((i + 1) / nimages * 50):
1155
+ done = int((i + 1) / nimages * 50)
1156
+ sys.stdout.write('\r')
1157
+ # progress output :
1158
+ sys.stdout.write("[%-50s] %d%%" % ('=' * done, 2 * done))
1159
+ sys.stdout.flush()
1160
+
1161
+ interpolated[i, :, :] = griddata(corrected, intensity_values[i, :], (grid_x, grid_y), method='linear')
1162
+ if QT_available:
1163
+ progress.set_value(nimages)
1164
+ print(':-)')
1165
+ print('You have successfully completed undistortion of image stack')
1166
+ return interpolated