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