pyTEMlib 0.2025.4.1__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.
- build/lib/pyTEMlib/__init__.py +33 -0
- build/lib/pyTEMlib/animation.py +640 -0
- build/lib/pyTEMlib/atom_tools.py +238 -0
- build/lib/pyTEMlib/config_dir.py +31 -0
- build/lib/pyTEMlib/crystal_tools.py +1219 -0
- build/lib/pyTEMlib/diffraction_plot.py +756 -0
- build/lib/pyTEMlib/dynamic_scattering.py +293 -0
- build/lib/pyTEMlib/eds_tools.py +826 -0
- build/lib/pyTEMlib/eds_xsections.py +432 -0
- build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
- build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
- build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- build/lib/pyTEMlib/file_reader.py +274 -0
- build/lib/pyTEMlib/file_tools.py +811 -0
- build/lib/pyTEMlib/get_bote_salvat.py +69 -0
- build/lib/pyTEMlib/graph_tools.py +1153 -0
- build/lib/pyTEMlib/graph_viz.py +599 -0
- build/lib/pyTEMlib/image/__init__.py +37 -0
- build/lib/pyTEMlib/image/image_atoms.py +270 -0
- build/lib/pyTEMlib/image/image_clean.py +197 -0
- build/lib/pyTEMlib/image/image_distortion.py +299 -0
- build/lib/pyTEMlib/image/image_fft.py +277 -0
- build/lib/pyTEMlib/image/image_graph.py +926 -0
- build/lib/pyTEMlib/image/image_registration.py +316 -0
- build/lib/pyTEMlib/image/image_utilities.py +309 -0
- build/lib/pyTEMlib/image/image_window.py +421 -0
- build/lib/pyTEMlib/image_tools.py +699 -0
- build/lib/pyTEMlib/interactive_image.py +1 -0
- build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
- build/lib/pyTEMlib/microscope.py +61 -0
- build/lib/pyTEMlib/probe_tools.py +906 -0
- build/lib/pyTEMlib/sidpy_tools.py +153 -0
- build/lib/pyTEMlib/simulation_tools.py +104 -0
- build/lib/pyTEMlib/test.py +437 -0
- build/lib/pyTEMlib/utilities.py +314 -0
- build/lib/pyTEMlib/version.py +5 -0
- build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
- pyTEMlib/__init__.py +25 -3
- pyTEMlib/animation.py +31 -22
- pyTEMlib/atom_tools.py +29 -34
- pyTEMlib/config_dir.py +2 -28
- pyTEMlib/crystal_tools.py +129 -165
- pyTEMlib/eds_tools.py +559 -342
- pyTEMlib/eds_xsections.py +432 -0
- pyTEMlib/eels_tools/__init__.py +44 -0
- pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- pyTEMlib/eels_tools/eels_database.py +134 -0
- pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- pyTEMlib/file_reader.py +274 -0
- pyTEMlib/file_tools.py +260 -1130
- pyTEMlib/get_bote_salvat.py +69 -0
- pyTEMlib/graph_tools.py +101 -174
- pyTEMlib/graph_viz.py +150 -0
- pyTEMlib/image/__init__.py +37 -0
- pyTEMlib/image/image_atoms.py +270 -0
- pyTEMlib/image/image_clean.py +197 -0
- pyTEMlib/image/image_distortion.py +299 -0
- pyTEMlib/image/image_fft.py +277 -0
- pyTEMlib/image/image_graph.py +926 -0
- pyTEMlib/image/image_registration.py +316 -0
- pyTEMlib/image/image_utilities.py +309 -0
- pyTEMlib/image/image_window.py +421 -0
- pyTEMlib/image_tools.py +154 -915
- pyTEMlib/kinematic_scattering.py +1 -1
- pyTEMlib/probe_tools.py +1 -1
- pyTEMlib/test.py +437 -0
- pyTEMlib/utilities.py +314 -0
- pyTEMlib/version.py +2 -3
- pyTEMlib/xrpa_x_sections.py +14 -10
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
- pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
- pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
- pyTEMlib/core_loss_widget.py +0 -721
- pyTEMlib/eels_dialog.py +0 -754
- pyTEMlib/eels_dialog_utilities.py +0 -1199
- pyTEMlib/eels_tools.py +0 -2359
- pyTEMlib/file_tools_qt.py +0 -193
- pyTEMlib/image_dialog.py +0 -158
- pyTEMlib/image_dlg.py +0 -146
- pyTEMlib/info_widget.py +0 -1086
- pyTEMlib/info_widget3.py +0 -1120
- pyTEMlib/low_loss_widget.py +0 -479
- pyTEMlib/peak_dialog.py +0 -1129
- pyTEMlib/peak_dlg.py +0 -286
- pytemlib-0.2025.4.1.dist-info/RECORD +0 -38
- pytemlib-0.2025.4.1.dist-info/top_level.txt +0 -1
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
- {pytemlib-0.2025.4.1.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
|