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