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,1196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kinematic_scattering
|
|
3
|
+
Copyright by Gerd Duscher
|
|
4
|
+
|
|
5
|
+
The University of Tennessee, Knoxville
|
|
6
|
+
Department of Materials Science & Engineering
|
|
7
|
+
|
|
8
|
+
Sources:
|
|
9
|
+
Scattering Theory:
|
|
10
|
+
Zuo and Spence, "Advanced TEM", 2017
|
|
11
|
+
|
|
12
|
+
Spence and Zuo, Electron Microdiffraction, Plenum 1992
|
|
13
|
+
|
|
14
|
+
Atomic Form Factor:
|
|
15
|
+
Kirkland: Advanced Computing in Electron Microscopy 2nd edition
|
|
16
|
+
Appendix C
|
|
17
|
+
|
|
18
|
+
Units:
|
|
19
|
+
everything is in SI units, except length which is given in Angstrom.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
See the notebooks for examples of these routines
|
|
23
|
+
|
|
24
|
+
All the input and output is done through a ase.Atoms object and the dictionary in the info attribute
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# numerical packages used
|
|
28
|
+
import numpy as np
|
|
29
|
+
import scipy
|
|
30
|
+
import itertools
|
|
31
|
+
|
|
32
|
+
import ase
|
|
33
|
+
import ase.build
|
|
34
|
+
|
|
35
|
+
# plotting package used
|
|
36
|
+
import matplotlib.pylab as plt # basic plotting
|
|
37
|
+
|
|
38
|
+
import pyTEMlib.file_tools as ft
|
|
39
|
+
from pyTEMlib.crystal_tools import *
|
|
40
|
+
from pyTEMlib.diffraction_plot import *
|
|
41
|
+
|
|
42
|
+
_version_ = "0.2022.1.0"
|
|
43
|
+
|
|
44
|
+
print(f'Using kinematic_scattering library version {_version_ } by G.Duscher')
|
|
45
|
+
|
|
46
|
+
inputKeys = ['acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max']
|
|
47
|
+
optional_inputKeys = ['crystal', 'lattice_parameter_nm', 'convergence_angle_mrad', 'mistilt', 'thickness',
|
|
48
|
+
'dynamic correction', 'dynamic correction K0']
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_poscar(filename):
|
|
52
|
+
print('read_poscar and read_cif moved to file_tools, \n'
|
|
53
|
+
'please use that library in the future!')
|
|
54
|
+
ft.read_poscar(filename)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def example(verbose=True):
|
|
58
|
+
"""
|
|
59
|
+
same as Zuo_fig_3_18
|
|
60
|
+
"""
|
|
61
|
+
print('\n##########################')
|
|
62
|
+
print('# Start of Example Input #')
|
|
63
|
+
print('##########################\n')
|
|
64
|
+
print('Define only mandatory input: ', inputKeys)
|
|
65
|
+
print(' Kinematic diffraction routine will set optional input : ', optional_inputKeys)
|
|
66
|
+
|
|
67
|
+
return Zuo_fig_3_18(verbose=verbose)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def Zuo_fig_3_18(verbose=True):
|
|
71
|
+
"""
|
|
72
|
+
Input for Figure 3.18 in Zuo and Spence \"Advanced TEM\", 2017
|
|
73
|
+
|
|
74
|
+
This input acts as an example as well as a reference
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
-----------
|
|
78
|
+
verbose: boolean:
|
|
79
|
+
optional to see output
|
|
80
|
+
Returns:
|
|
81
|
+
-------
|
|
82
|
+
atoms: ase.Atoms
|
|
83
|
+
Silicon crystal structure
|
|
84
|
+
e
|
|
85
|
+
dictionary: tags is the dictionary of all input and output parameter needed to reproduce that figure.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# INPUT
|
|
89
|
+
# Create Silicon structure (Could be produced with Silicon routine)
|
|
90
|
+
if verbose:
|
|
91
|
+
print('Sample Input for Figure 3.18 in Zuo and Spence \"Advanced TEM\", 2017')
|
|
92
|
+
a = 5.14 # A
|
|
93
|
+
atoms = ase.build.bulk('Si', 'diamond', a=a, cubic=True)
|
|
94
|
+
|
|
95
|
+
experiment = {'acceleration_voltage_V': 99.2 * 1000.0, # V
|
|
96
|
+
'convergence_angle_mrad': 7.15, # mrad;
|
|
97
|
+
'zone_hkl': np.array([-2, 2, 1]),
|
|
98
|
+
'mistilt': np.array([0, 0, 0]), # mistilt in degrees
|
|
99
|
+
'Sg_max': .03, # 1/A maximum allowed excitation error
|
|
100
|
+
'hkl_max': 9 # Highest evaluated Miller indices
|
|
101
|
+
}
|
|
102
|
+
# Define Experimental Conditions
|
|
103
|
+
if verbose:
|
|
104
|
+
print('###########################')
|
|
105
|
+
print('# Experimental Conditions #')
|
|
106
|
+
print('###########################')
|
|
107
|
+
|
|
108
|
+
for key, value in experiment.items():
|
|
109
|
+
print(f'tags[\'{key}\'] =', value)
|
|
110
|
+
|
|
111
|
+
print('##################')
|
|
112
|
+
print('# Output Options #')
|
|
113
|
+
print('##################')
|
|
114
|
+
|
|
115
|
+
# Output options
|
|
116
|
+
output = {'background': 'black', # 'white' 'grey'
|
|
117
|
+
'color_map': 'plasma',
|
|
118
|
+
'plot_HOLZ': True,
|
|
119
|
+
'plot_HOLZ_excess': True,
|
|
120
|
+
'plot_Kikuchi': True,
|
|
121
|
+
'plot_reflections': True,
|
|
122
|
+
'label_HOLZ': False,
|
|
123
|
+
'label_Kikuchi': False,
|
|
124
|
+
'label_reflections': False,
|
|
125
|
+
'label_color': 'black',
|
|
126
|
+
'label_size': 10,
|
|
127
|
+
'color_Laue_Zones': ['red', 'blue', 'green', 'blue', 'green'], # for OLZ give a sequence
|
|
128
|
+
'color_Kikuchi': 'green',
|
|
129
|
+
'linewidth_HOLZ': -1, # -1: linewidth according to intensity (structure factor F^2)
|
|
130
|
+
'linewidth_Kikuchi': -1, # -1: linewidth according to intensity (structure factor F^2)
|
|
131
|
+
'color_reflections': 'intensity', # 'Laue Zone'
|
|
132
|
+
'color_zero': 'white', # 'None', 'white', 'blue'
|
|
133
|
+
'color_ring_zero': 'None' # 'Red' #'white' #, 'None'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if verbose:
|
|
137
|
+
for key, value in output.items():
|
|
138
|
+
print(f'tags[\'{key}\'] =', value)
|
|
139
|
+
print('########################')
|
|
140
|
+
print('# End of Example Input #')
|
|
141
|
+
print('########################\n\n')
|
|
142
|
+
|
|
143
|
+
if atoms.info is None:
|
|
144
|
+
atoms.info = {}
|
|
145
|
+
atoms.info['experimental'] = experiment
|
|
146
|
+
atoms.info['output'] = output
|
|
147
|
+
|
|
148
|
+
return atoms
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_rotation_matrix(angles, in_radians=False):
|
|
152
|
+
""" Rotation of zone axis by mistilt
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
angles: ist or numpy array of float
|
|
157
|
+
list of mistilt angles (default in degrees)
|
|
158
|
+
in_radians: boolean default False
|
|
159
|
+
default is angles in degrees
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
rotation_matrix: np.ndarray (3x3)
|
|
164
|
+
rotation matrix in 3d
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
if not isinstance(angles, (np.ndarray, list)):
|
|
168
|
+
raise TypeError('angles must be a list of float of length 3')
|
|
169
|
+
if len(angles) != 3:
|
|
170
|
+
raise TypeError('angles must be a list of float of length 3')
|
|
171
|
+
|
|
172
|
+
if in_radians:
|
|
173
|
+
alpha, beta, gamma = angles
|
|
174
|
+
else:
|
|
175
|
+
alpha, beta, gamma = np.radians(angles)
|
|
176
|
+
# first we rotate alpha about x-axis
|
|
177
|
+
c, s = np.cos(alpha), np.sin(alpha)
|
|
178
|
+
rot_x = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
179
|
+
|
|
180
|
+
# second we rotate beta about y-axis
|
|
181
|
+
c, s = np.cos(beta), np.sin(beta)
|
|
182
|
+
rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
|
183
|
+
|
|
184
|
+
# third we rotate gamma about z-axis
|
|
185
|
+
c, s = np.cos(gamma), np.sin(gamma)
|
|
186
|
+
rot_z = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
|
187
|
+
return np.dot(np.dot(rot_x, rot_y), rot_z)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def zone_mistilt(zone, angles):
|
|
191
|
+
""" Rotation of zone axis by mistilt
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
zone: list or numpy array of int
|
|
196
|
+
zone axis in Miller indices
|
|
197
|
+
angles: ist or numpy array of float
|
|
198
|
+
list of mistilt angles in degree
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
new_zone_axis: np.ndarray (3)
|
|
203
|
+
new tilted zone axis
|
|
204
|
+
"""
|
|
205
|
+
if not isinstance(zone, (np.ndarray, list)):
|
|
206
|
+
raise TypeError('Miller indices must be a list of int of length 3')
|
|
207
|
+
|
|
208
|
+
rotation_matrix = get_rotation_matrix(angles)
|
|
209
|
+
return np.dot(zone, rotation_matrix)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_metric_tensor(matrix):
|
|
213
|
+
"""The metric tensor of the lattice."""
|
|
214
|
+
metric_tensor2 = np.dot(matrix, matrix.T)
|
|
215
|
+
return metric_tensor2
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def vector_norm(g):
|
|
219
|
+
""" Length of vector
|
|
220
|
+
|
|
221
|
+
depreciated - use np.linalg.norm
|
|
222
|
+
"""
|
|
223
|
+
g = np.array(g)
|
|
224
|
+
return np.sqrt(g[:, 0] ** 2 + g[:, 1] ** 2 + g[:, 2] ** 2)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_wavelength(acceleration_voltage):
|
|
228
|
+
"""
|
|
229
|
+
Calculates the relativistic corrected de Broglie wavelength of an electron in Angstrom
|
|
230
|
+
|
|
231
|
+
Parameter:
|
|
232
|
+
---------
|
|
233
|
+
acceleration_voltage: float
|
|
234
|
+
acceleration voltage in volt
|
|
235
|
+
Returns:
|
|
236
|
+
-------
|
|
237
|
+
wavelength: float
|
|
238
|
+
wave length in Angstrom (= meter *10**10)
|
|
239
|
+
"""
|
|
240
|
+
if not isinstance(acceleration_voltage, (int, float)):
|
|
241
|
+
raise TypeError('Acceleration voltage has to be a real number')
|
|
242
|
+
|
|
243
|
+
E = acceleration_voltage * scipy.constants.elementary_charge
|
|
244
|
+
h = scipy.constants.Planck
|
|
245
|
+
m0 = scipy.constants.electron_mass
|
|
246
|
+
c = scipy.constants.speed_of_light
|
|
247
|
+
wavelength = h / np.sqrt(2 * m0 * E * (1 + (E / (2 * m0 * c ** 2))))
|
|
248
|
+
return wavelength * 10**10
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_all_miller_indices(hkl_max):
|
|
252
|
+
h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller Indices
|
|
253
|
+
hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
|
|
254
|
+
|
|
255
|
+
# delete [0,0,0]
|
|
256
|
+
index_center = int(len(hkl) / 2)
|
|
257
|
+
hkl = np.delete(hkl, index_center, axis=0) # delete [0,0,0]
|
|
258
|
+
return hkl
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def find_nearest_zone_axis(tags):
|
|
262
|
+
"""Test all zone axis up to a maximum of hkl_max"""
|
|
263
|
+
|
|
264
|
+
hkl_max = 5
|
|
265
|
+
# Make all hkl indices
|
|
266
|
+
zones_hkl = get_all_miller_indices(hkl_max)
|
|
267
|
+
|
|
268
|
+
# make zone axis in reciprocal space
|
|
269
|
+
zones_g = np.dot(zones_hkl, tags['reciprocal_unit_cell']) # all evaluated reciprocal_unit_cell points
|
|
270
|
+
|
|
271
|
+
# make zone axis in microscope coordinates of reciprocal space
|
|
272
|
+
zones_g = np.dot(zones_g, tags['rotation_matrix']) # rotate these reciprocal_unit_cell points
|
|
273
|
+
|
|
274
|
+
# calculate angles with z-axis
|
|
275
|
+
zones_g_norm = vector_norm(zones_g)
|
|
276
|
+
z_axis = np.array([0, 0, 1])
|
|
277
|
+
|
|
278
|
+
zones_angles = np.abs(np.arccos(np.dot((zones_g.T / zones_g_norm).T, z_axis)))
|
|
279
|
+
|
|
280
|
+
# get smallest angle
|
|
281
|
+
smallest = (zones_angles - zones_angles.min()) < 0.001
|
|
282
|
+
if smallest.sum() > 1: # multiples of Miller index of zone axis have same angle
|
|
283
|
+
zone = zones_hkl[smallest]
|
|
284
|
+
zone_index = abs(zone).sum(axis=1)
|
|
285
|
+
ind = zone_index.argmin()
|
|
286
|
+
zone_hkl = zone[ind]
|
|
287
|
+
else:
|
|
288
|
+
zone_hkl = zones_hkl[smallest][0]
|
|
289
|
+
|
|
290
|
+
tags['nearest_zone_axis'] = zone_hkl
|
|
291
|
+
|
|
292
|
+
# get other zone axes up to 5 degrees away
|
|
293
|
+
others = np.logical_not(smallest)
|
|
294
|
+
next_smallest = (zones_angles[others]) < np.deg2rad(5.)
|
|
295
|
+
ind = np.argsort((zones_angles[others])[next_smallest])
|
|
296
|
+
|
|
297
|
+
tags['next_nearest_zone_axes'] = ((zones_hkl[others])[next_smallest])[ind]
|
|
298
|
+
|
|
299
|
+
return zone_hkl
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def find_angles(zone):
|
|
303
|
+
"""Microscope stage coordinates of zone"""
|
|
304
|
+
|
|
305
|
+
# rotation around y-axis
|
|
306
|
+
r = np.sqrt(zone[1] ** 2 + zone[2] ** 2)
|
|
307
|
+
alpha = np.arctan(zone[0] / r)
|
|
308
|
+
if zone[2] < 0:
|
|
309
|
+
alpha = np.pi - alpha
|
|
310
|
+
# rotation around x-axis
|
|
311
|
+
if zone[2] == 0:
|
|
312
|
+
beta = np.pi / 2 * np.sign(zone[1])
|
|
313
|
+
else:
|
|
314
|
+
beta = (np.arctan(zone[1] / zone[2]))
|
|
315
|
+
return alpha, beta
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def stage_rotation_matrix(alpha, beta):
|
|
319
|
+
""" Microscope stage coordinate system """
|
|
320
|
+
|
|
321
|
+
# FIRST we rotate beta about x-axis
|
|
322
|
+
angles = [beta, alpha, 0.]
|
|
323
|
+
return get_rotation_matrix(angles, in_radians=True)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ##################
|
|
327
|
+
# Determine rotation matrix to tilt zone axis onto z-axis
|
|
328
|
+
# We determine spherical coordinates to do that
|
|
329
|
+
# ##################
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_zone_rotation(tags):
|
|
333
|
+
"""zone axis in global coordinate system"""
|
|
334
|
+
|
|
335
|
+
zone_hkl = tags['zone_hkl']
|
|
336
|
+
zone = np.dot(zone_hkl, tags['reciprocal_unit_cell'])
|
|
337
|
+
|
|
338
|
+
# angle of zone with Z around x,y:
|
|
339
|
+
alpha, beta = find_angles(zone)
|
|
340
|
+
|
|
341
|
+
alpha = alpha + tags['mistilt_alpha']
|
|
342
|
+
beta = beta + tags['mistilt_beta']
|
|
343
|
+
|
|
344
|
+
tags['y-axis rotation alpha'] = alpha
|
|
345
|
+
tags['x-axis rotation beta'] = beta
|
|
346
|
+
|
|
347
|
+
tags['rotation_matrix'] = rotation_matrix = stage_rotation_matrix(alpha, -beta)
|
|
348
|
+
|
|
349
|
+
# the rotation now makes z-axis coincide with plane normal
|
|
350
|
+
|
|
351
|
+
zone_nearest = find_nearest_zone_axis(tags)
|
|
352
|
+
tags['nearest_zone_axis'] = zone_nearest
|
|
353
|
+
|
|
354
|
+
# tilt angles of coordinates of nearest zone
|
|
355
|
+
zone_nearest = np.dot(zone_nearest, tags['reciprocal_unit_cell'])
|
|
356
|
+
|
|
357
|
+
alpha_nearest, beta_nearest = find_angles(zone_nearest)
|
|
358
|
+
|
|
359
|
+
# calculate mistilt of nearest zone axis
|
|
360
|
+
tags['mistilt_nearest_zone alpha'] = alpha - alpha_nearest
|
|
361
|
+
tags['mistilt_nearest_zone beta'] = beta - beta_nearest
|
|
362
|
+
|
|
363
|
+
tags['nearest_zone_axes'] = {}
|
|
364
|
+
tags['nearest_zone_axes']['0'] = {}
|
|
365
|
+
tags['nearest_zone_axes']['0']['hkl'] = tags['nearest_zone_axis']
|
|
366
|
+
tags['nearest_zone_axes']['0']['mistilt_alpha'] = alpha - alpha_nearest
|
|
367
|
+
tags['nearest_zone_axes']['0']['mistilt_beta'] = beta - beta_nearest
|
|
368
|
+
|
|
369
|
+
# find polar coordinates of next nearest zones
|
|
370
|
+
tags['nearest_zone_axes']['amount'] = len(tags['next_nearest_zone_axes']) + 1
|
|
371
|
+
|
|
372
|
+
for i in range(len(tags['next_nearest_zone_axes'])):
|
|
373
|
+
zone_n = tags['next_nearest_zone_axes'][i]
|
|
374
|
+
tags['nearest_zone_axes'][str(i + 1)] = {}
|
|
375
|
+
tags['nearest_zone_axes'][str(i + 1)]['hkl'] = zone_n
|
|
376
|
+
|
|
377
|
+
zone_near = np.dot(zone_n, tags['reciprocal_unit_cell'])
|
|
378
|
+
# zone_near_g = np.dot(zone_near,rotation_matrix)
|
|
379
|
+
|
|
380
|
+
tags['nearest_zone_axes'][str(i + 1)]['g'] = zone_near
|
|
381
|
+
alpha_nearest, beta_nearest = find_angles(zone_near)
|
|
382
|
+
|
|
383
|
+
tags['nearest_zone_axes'][str(i + 1)]['mistilt_alpha'] = alpha - alpha_nearest
|
|
384
|
+
tags['nearest_zone_axes'][str(i + 1)]['mistilt_beta'] = beta - beta_nearest
|
|
385
|
+
# print('other' , i, np.rad2deg([alpha, alpha_nearest, beta, beta_nearest]))
|
|
386
|
+
|
|
387
|
+
return rotation_matrix
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def check_sanity(atoms, verbose_level=0):
|
|
391
|
+
"""
|
|
392
|
+
Check sanity of input parameters
|
|
393
|
+
"""
|
|
394
|
+
stop = False
|
|
395
|
+
output = atoms.info['output']
|
|
396
|
+
tags = atoms.info['experimental']
|
|
397
|
+
for key in ['acceleration_voltage_V']:
|
|
398
|
+
if key not in tags:
|
|
399
|
+
print(f'Necessary parameter {key} not defined')
|
|
400
|
+
stop = True
|
|
401
|
+
if 'SpotPattern' not in output:
|
|
402
|
+
output['SpotPattern'] = False
|
|
403
|
+
if output['SpotPattern']:
|
|
404
|
+
if 'zone_hkl' not in tags:
|
|
405
|
+
print(' No zone_hkl defined')
|
|
406
|
+
stop = True
|
|
407
|
+
if 'Sg_max' not in tags:
|
|
408
|
+
print(' No Sg_max defined')
|
|
409
|
+
stop = True
|
|
410
|
+
if 'hkl_max' not in tags:
|
|
411
|
+
print(' No hkl_max defined')
|
|
412
|
+
stop = True
|
|
413
|
+
|
|
414
|
+
if stop:
|
|
415
|
+
print('Input is not complete, stopping')
|
|
416
|
+
print('Try \'example()\' for example input')
|
|
417
|
+
return False
|
|
418
|
+
############################################
|
|
419
|
+
# Check optional input
|
|
420
|
+
############################################
|
|
421
|
+
|
|
422
|
+
if output['SpotPattern']:
|
|
423
|
+
if 'mistilt_alpha degree' not in tags:
|
|
424
|
+
# mistilt is in microscope coordinates
|
|
425
|
+
tags['mistilt_alpha'] = tags['mistilt_alpha degree'] = 0.0
|
|
426
|
+
if verbose_level > 0:
|
|
427
|
+
print('Setting undefined input: tags[\'mistilt_alpha\'] = 0.0 ')
|
|
428
|
+
else:
|
|
429
|
+
tags['mistilt_alpha'] = np.deg2rad(tags['mistilt_alpha degree'])
|
|
430
|
+
|
|
431
|
+
if 'mistilt_beta degree' not in tags:
|
|
432
|
+
# mistilt is in microscope coordinates
|
|
433
|
+
tags['mistilt_beta'] = tags['mistilt_beta degree'] = 0.0
|
|
434
|
+
if verbose_level > 0:
|
|
435
|
+
print('Setting undefined input: tags[\'mistilt_beta\'] = 0.0')
|
|
436
|
+
else:
|
|
437
|
+
tags['mistilt_beta'] = np.deg2rad(tags['mistilt_beta degree'])
|
|
438
|
+
|
|
439
|
+
if 'convergence_angle_mrad' not in tags:
|
|
440
|
+
tags['convergence_angle_mrad'] = 0.
|
|
441
|
+
if verbose_level > 0:
|
|
442
|
+
print('Setting undefined input: tags[\'convergence_angle_mrad\'] = 0')
|
|
443
|
+
|
|
444
|
+
if 'thickness' not in tags:
|
|
445
|
+
tags['thickness'] = 0.
|
|
446
|
+
if verbose_level > 0:
|
|
447
|
+
print('Setting undefined input: tags[\'thickness\'] = 0')
|
|
448
|
+
if 'dynamic correction' not in tags:
|
|
449
|
+
tags['dynamic correction'] = 0.
|
|
450
|
+
if verbose_level > 0:
|
|
451
|
+
print('Setting undefined input: tags[\'dynamic correction\'] = False')
|
|
452
|
+
if 'dynamic correction K0' not in tags:
|
|
453
|
+
tags['dynamic correction K0'] = 0.
|
|
454
|
+
if verbose_level > 0:
|
|
455
|
+
print('Setting undefined input: tags[\'dynamic correction k0\'] = False')
|
|
456
|
+
return not stop
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def scattering_matrix(tags, verbose_level=1):
|
|
460
|
+
""" Scattering matrix"""
|
|
461
|
+
if not check_sanity(tags, verbose_level):
|
|
462
|
+
return
|
|
463
|
+
# ##
|
|
464
|
+
# Pair distribution Function
|
|
465
|
+
# ##
|
|
466
|
+
unit_cell = np.array(tags['unit_cell'])
|
|
467
|
+
base = tags['base']
|
|
468
|
+
|
|
469
|
+
atom_coordinates = np.dot(base, unit_cell)
|
|
470
|
+
|
|
471
|
+
n = 20
|
|
472
|
+
x = np.linspace(-n, n, 2 * n + 1) # all evaluated multiples of x
|
|
473
|
+
xyz = np.array(list(itertools.product(x, x, x))) # all evaluated multiples in all direction
|
|
474
|
+
|
|
475
|
+
mat = np.dot(xyz, unit_cell) # all evaluated unit_cells
|
|
476
|
+
|
|
477
|
+
atom = {}
|
|
478
|
+
|
|
479
|
+
for i in range(len(atom_coordinates)):
|
|
480
|
+
distances = np.linalg.norm(mat + atom_coordinates[i], axis=1)
|
|
481
|
+
if i == 0:
|
|
482
|
+
all_distances = distances
|
|
483
|
+
else:
|
|
484
|
+
all_distances = np.append(all_distances, distances)
|
|
485
|
+
unique, counts = np.unique(distances, return_counts=True)
|
|
486
|
+
|
|
487
|
+
atom[str(i)] = dict(zip(unique, counts))
|
|
488
|
+
print(atom[str(i)])
|
|
489
|
+
|
|
490
|
+
all_distances = np.append(all_distances, distances)
|
|
491
|
+
unique, counts = np.unique(all_distances, return_counts=True)
|
|
492
|
+
|
|
493
|
+
plt.plot(unique, counts)
|
|
494
|
+
plt.show()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def ring_pattern_calculation(atoms, verbose=False):
|
|
498
|
+
"""
|
|
499
|
+
Calculate the ring diffraction pattern of a crystal structure
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
atoms: Crystal
|
|
504
|
+
crystal structure
|
|
505
|
+
verbose: verbose print-outs
|
|
506
|
+
set to False
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
tags: dict
|
|
510
|
+
dictionary with diffraction information added
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
# Check sanity
|
|
514
|
+
if not check_sanity(atoms, verbose):
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
tags = atoms.info['experimental']
|
|
518
|
+
# wavelength
|
|
519
|
+
tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
|
|
520
|
+
|
|
521
|
+
# volume of unit_cell
|
|
522
|
+
unit_cell = atoms.cell.array
|
|
523
|
+
metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
|
|
524
|
+
tags['metric_tensor'] = metric_tensor
|
|
525
|
+
# volume_unit_cell = np.sqrt(np.linalg.det(metric_tensor))
|
|
526
|
+
|
|
527
|
+
# reciprocal_unit_cell
|
|
528
|
+
|
|
529
|
+
# We use the linear algebra package of numpy to invert the unit_cell "matrix"
|
|
530
|
+
reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
|
|
531
|
+
tags['reciprocal_unit_cell'] = reciprocal_unit_cell
|
|
532
|
+
# inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
|
|
533
|
+
|
|
534
|
+
hkl_max = tags['hkl_max']
|
|
535
|
+
hkl = get_all_miller_indices(hkl_max)
|
|
536
|
+
|
|
537
|
+
g_hkl = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
|
|
538
|
+
|
|
539
|
+
##################################
|
|
540
|
+
# Calculate Structure Factors
|
|
541
|
+
#################################
|
|
542
|
+
|
|
543
|
+
structure_factors = []
|
|
544
|
+
for j in range(len(g_hkl)):
|
|
545
|
+
F = 0
|
|
546
|
+
for b in range(len(atoms)):
|
|
547
|
+
f = feq(atoms[b].symbol, np.linalg.norm(g_hkl[j]))
|
|
548
|
+
F += f * np.exp(-2 * np.pi * 1j * (hkl[j] * atoms.get_scaled_positions()[b]).sum())
|
|
549
|
+
|
|
550
|
+
structure_factors.append(F)
|
|
551
|
+
|
|
552
|
+
F = np.array(structure_factors) # structure factors
|
|
553
|
+
|
|
554
|
+
# Sort reflection in allowed and forbidden #
|
|
555
|
+
|
|
556
|
+
allowed = np.absolute(F) > 0.000001 # allowed within numerical error
|
|
557
|
+
|
|
558
|
+
if verbose:
|
|
559
|
+
print('Of the {0} possible reflection {1} are allowed.'.format(hkl.shape[0], allowed.sum()))
|
|
560
|
+
|
|
561
|
+
# information of allowed reflections
|
|
562
|
+
hkl_allowed = hkl[allowed][:]
|
|
563
|
+
g_allowed = g_hkl[allowed, :]
|
|
564
|
+
F_allowed = F[allowed]
|
|
565
|
+
g_norm_allowed = vector_norm(g_allowed) # length of all vectors = 1/
|
|
566
|
+
|
|
567
|
+
ind = np.argsort(g_norm_allowed)
|
|
568
|
+
g_norm_sorted = g_norm_allowed[ind]
|
|
569
|
+
hkl_sorted = hkl_allowed[ind][:]
|
|
570
|
+
F_sorted = F_allowed[ind]
|
|
571
|
+
|
|
572
|
+
unique, counts = np.unique(np.around(g_norm_sorted, decimals=5), return_counts=True)
|
|
573
|
+
if verbose:
|
|
574
|
+
print('Of the {0} allowed reflection {1} have unique distances.'.format(allowed.sum(), len(unique)))
|
|
575
|
+
|
|
576
|
+
reflections_d = []
|
|
577
|
+
reflections_m = []
|
|
578
|
+
reflections_F = []
|
|
579
|
+
|
|
580
|
+
start = 0
|
|
581
|
+
for i in range(len(unique)):
|
|
582
|
+
end = start + counts[i]
|
|
583
|
+
hkl_max = np.argmax(hkl_sorted[start:end].sum(axis=1))
|
|
584
|
+
|
|
585
|
+
reflections_d.append(g_norm_sorted[start])
|
|
586
|
+
reflections_m.append(hkl_sorted[start + hkl_max])
|
|
587
|
+
reflections_F.append(F_sorted[start]) # :end].sum())
|
|
588
|
+
|
|
589
|
+
start = end
|
|
590
|
+
|
|
591
|
+
if verbose:
|
|
592
|
+
print('\n\n [hkl] \t 1/d [1/nm] \t d [nm] \t F^2 ')
|
|
593
|
+
for i in range(len(unique)):
|
|
594
|
+
print(' {0} \t {1:.2f} \t {2:.4f} \t {3:.2f} '
|
|
595
|
+
.format(reflections_m[i], unique[i]*10., 1 / unique[i]/10., np.real(reflections_F[i]) ** 2))
|
|
596
|
+
|
|
597
|
+
atoms.info['Ring_Pattern'] = {}
|
|
598
|
+
atoms.info['Ring_Pattern']['allowed'] = {}
|
|
599
|
+
atoms.info['Ring_Pattern']['allowed']['hkl'] = reflections_m
|
|
600
|
+
atoms.info['Ring_Pattern']['allowed']['g norm'] = unique
|
|
601
|
+
atoms.info['Ring_Pattern']['allowed']['structure factor'] = reflections_F
|
|
602
|
+
atoms.info['Ring_Pattern']['allowed']['multiplicity'] = counts
|
|
603
|
+
|
|
604
|
+
atoms.info['Ring_Pattern']['profile_x'] = np.linspace(0, unique.max(), 2048)
|
|
605
|
+
step_size = atoms.info['Ring_Pattern']['profile_x'][1]
|
|
606
|
+
intensity = np.zeros(2048)
|
|
607
|
+
x_index = [(unique / step_size + 0.5).astype(int)]
|
|
608
|
+
intensity[x_index] = np.array(np.real(reflections_F)) * np.array(np.real(reflections_F))
|
|
609
|
+
atoms.info['Ring_Pattern']['profile_y delta'] = intensity
|
|
610
|
+
|
|
611
|
+
def gaussian(xx, pp):
|
|
612
|
+
s1 = pp[2] / 2.3548
|
|
613
|
+
prefactor = 1.0 / np.sqrt(2 * np.pi * s1 ** 2)
|
|
614
|
+
y = (pp[1] * prefactor) * np.exp(-(xx - pp[0]) ** 2 / (2 * s1 ** 2))
|
|
615
|
+
return y
|
|
616
|
+
|
|
617
|
+
if 'thickness' in tags:
|
|
618
|
+
if tags['thickness'] > 0:
|
|
619
|
+
x = np.linspace(-1024, 1023, 2048) * step_size
|
|
620
|
+
p = [0.0, 1, 2 / tags['thickness']]
|
|
621
|
+
|
|
622
|
+
gauss = gaussian(x, p)
|
|
623
|
+
intensity = np.convolve(np.array(intensity), np.array(gauss), mode='same')
|
|
624
|
+
atoms.info['Ring_Pattern']['profile_y'] = intensity
|
|
625
|
+
|
|
626
|
+
# Make pretty labels
|
|
627
|
+
hkl_allowed = reflections_m
|
|
628
|
+
hkl_label = make_pretty_labels(hkl_allowed)
|
|
629
|
+
atoms.info['Ring_Pattern']['allowed']['label'] = hkl_label
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def get_dynamically_allowed(atoms, verbose=False):
|
|
633
|
+
if not isinstance(atoms, ase.Atoms):
|
|
634
|
+
print('we need an ase atoms object as input')
|
|
635
|
+
if 'diffraction' not in atoms.info:
|
|
636
|
+
print('Run the kinematic_scattering function first')
|
|
637
|
+
|
|
638
|
+
# Dynamically Allowed Reflection
|
|
639
|
+
|
|
640
|
+
dif = atoms.info['diffraction']
|
|
641
|
+
hkl_allowed = dif['allowed']['hkl']
|
|
642
|
+
hkl_forbidden = dif['forbidden']['hkl']
|
|
643
|
+
indices = range(len(hkl_allowed))
|
|
644
|
+
combinations = [list(x) for x in itertools.permutations(indices, 2)]
|
|
645
|
+
hkl_forbidden = hkl_forbidden.tolist()
|
|
646
|
+
dynamically_allowed = np.zeros(len(hkl_forbidden), dtype=bool)
|
|
647
|
+
for [i, j] in combinations:
|
|
648
|
+
possible = (hkl_allowed[i] + hkl_allowed[j]).tolist()
|
|
649
|
+
if possible in hkl_forbidden:
|
|
650
|
+
dynamically_allowed[hkl_forbidden.index(possible)] = True
|
|
651
|
+
dif['forbidden']['dynamically_allowed'] = dynamically_allowed
|
|
652
|
+
|
|
653
|
+
if verbose:
|
|
654
|
+
print(f"Of the {len(hkl_forbidden)} forbidden reflection {dynamically_allowed.sum()} "
|
|
655
|
+
f"can be dynamically activated.")
|
|
656
|
+
# print(dif['forbidden']['hkl'][dynamically_allowed])
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def kinematic_scattering(atoms, verbose=False):
|
|
660
|
+
"""
|
|
661
|
+
All kinematic scattering calculation
|
|
662
|
+
|
|
663
|
+
Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
|
|
664
|
+
|
|
665
|
+
Parameters
|
|
666
|
+
----------
|
|
667
|
+
atoms: ase.Atoms
|
|
668
|
+
object with crystal structure:
|
|
669
|
+
and with experimental parameters in info attribute:
|
|
670
|
+
'acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max'
|
|
671
|
+
Optional parameters are:
|
|
672
|
+
'mistilt', convergence_angle_mrad', and 'crystal_name'
|
|
673
|
+
verbose = True will give extended output of the calculation
|
|
674
|
+
verbose: boolean
|
|
675
|
+
default is False
|
|
676
|
+
|
|
677
|
+
Returns
|
|
678
|
+
-------
|
|
679
|
+
atoms:
|
|
680
|
+
There are three sub_dictionaries in info attribute:
|
|
681
|
+
['allowed'], ['forbidden'], and ['HOLZ']
|
|
682
|
+
['allowed'] and ['forbidden'] dictionaries contain:
|
|
683
|
+
['Sg'], ['hkl'], ['g'], ['structure factor'], ['intensities'],
|
|
684
|
+
['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], ['HHOLZ'], ['label'], and ['Laue_zone']
|
|
685
|
+
the ['HOLZ'] dictionary contains:
|
|
686
|
+
['slope'], ['distance'], ['theta'], ['g_deficient'], ['g_excess'], ['hkl'], ['intensities'],
|
|
687
|
+
['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], and ['HHOLZ']
|
|
688
|
+
Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
|
|
689
|
+
|
|
690
|
+
There are also a few parameters stored in the main dictionary:
|
|
691
|
+
['wave_length_nm'], ['reciprocal_unit_cell'], ['inner_potential_V'], ['incident_wave_vector'],
|
|
692
|
+
['volume'], ['theta'], ['phi'], and ['incident_wave_vector_vacuum']
|
|
693
|
+
"""
|
|
694
|
+
|
|
695
|
+
# Check sanity
|
|
696
|
+
if atoms.info is None:
|
|
697
|
+
atoms.info = {'output': {}, 'experimental': {}}
|
|
698
|
+
if 'output' in atoms.info:
|
|
699
|
+
output = atoms.info['output']
|
|
700
|
+
else:
|
|
701
|
+
output = atoms.info['output'] = {}
|
|
702
|
+
|
|
703
|
+
output['SpotPattern'] = True
|
|
704
|
+
|
|
705
|
+
if 'experimental' not in atoms.info:
|
|
706
|
+
tags = atoms.info['experimental'] = {}
|
|
707
|
+
|
|
708
|
+
if not check_sanity(atoms):
|
|
709
|
+
print('Input is not complete, stopping')
|
|
710
|
+
print('Try \'example()\' for example input')
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
tags = atoms.info['experimental']
|
|
714
|
+
|
|
715
|
+
tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
|
|
716
|
+
|
|
717
|
+
# ###########################################
|
|
718
|
+
# reciprocal_unit_cell
|
|
719
|
+
# ###########################################
|
|
720
|
+
unit_cell = atoms.cell.array
|
|
721
|
+
tags['unit_cell'] = unit_cell
|
|
722
|
+
metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
|
|
723
|
+
tags['metric_tensor'] = metric_tensor
|
|
724
|
+
volume_unit_cell = atoms.cell.volume
|
|
725
|
+
|
|
726
|
+
# We use the linear algebra package of numpy to invert the unit_cell "matrix"
|
|
727
|
+
reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
|
|
728
|
+
tags['reciprocal_unit_cell'] = reciprocal_unit_cell
|
|
729
|
+
inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
|
|
730
|
+
|
|
731
|
+
# ###########################################
|
|
732
|
+
# Incident wave vector k0 in vacuum and material
|
|
733
|
+
# ###########################################
|
|
734
|
+
|
|
735
|
+
# Incident wave vector K0 in vacuum and material
|
|
736
|
+
u0 = 0.0 # in (Ang)
|
|
737
|
+
# atom form factor of zero reflection angle is the inner potential in 1/A
|
|
738
|
+
for i in range(len(atoms)):
|
|
739
|
+
u0 += feq(atoms[i].symbol, 0.0)
|
|
740
|
+
|
|
741
|
+
angstrom_conversion = 1.0e10 # So [1A (in m)] * angstrom_conversion = 1
|
|
742
|
+
# NanometerConversion = 1.0e9
|
|
743
|
+
|
|
744
|
+
e = scipy.constants.elementary_charge
|
|
745
|
+
h = scipy.constants.Planck
|
|
746
|
+
m0 = scipy.constants.electron_mass
|
|
747
|
+
|
|
748
|
+
scattering_factor_to_volts = (h**2) * (1e10**2) / (2 * np.pi * m0 * e) * volume_unit_cell
|
|
749
|
+
tags['inner_potential_V'] = u0 * scattering_factor_to_volts
|
|
750
|
+
if verbose:
|
|
751
|
+
print(f'The inner potential is {u0:.1f} V')
|
|
752
|
+
|
|
753
|
+
# Calculating incident wave vector magnitude 'k0' in material
|
|
754
|
+
wl = tags['wave_length']
|
|
755
|
+
tags['incident_wave_vector_vacuum'] = 1 / wl
|
|
756
|
+
|
|
757
|
+
k_0 = tags['incident_wave_vector'] = np.sqrt(1 / wl**2 + u0/volume_unit_cell) # 1/Ang
|
|
758
|
+
|
|
759
|
+
tags['convergence_angle_A-1'] = k_0*np.sin(tags['convergence_angle_mrad']/1000.)
|
|
760
|
+
|
|
761
|
+
if verbose:
|
|
762
|
+
print(f"Using an acceleration voltage of {tags['acceleration_voltage_V']/1000:.1f}kV")
|
|
763
|
+
print(f'Magnitude of incident wave vector in material: {k_0:.4f} 1/Ang and in vacuum: {1/wl:.4f} 1/Ang')
|
|
764
|
+
print(f"Which is an wave length of {1/k_0 * 100.:.3f} pm in the material and {wl * 100.:.3f} pm "
|
|
765
|
+
f"in the vacuum")
|
|
766
|
+
print(f"The convergence angle of {tags['convergence_angle_mrad']:.1f}mrad "
|
|
767
|
+
f"= {tags['convergence_angle_A-1']:.2f} 1/A")
|
|
768
|
+
print(f"Magnitude of incident wave vector in material: {k_0:.1f} 1/A which is a wavelength {100/k_0:.3f} pm")
|
|
769
|
+
|
|
770
|
+
# ############
|
|
771
|
+
# Rotate
|
|
772
|
+
# ############
|
|
773
|
+
|
|
774
|
+
# get rotation matrix to rotate zone axis onto z-axis
|
|
775
|
+
rotation_matrix = get_zone_rotation(tags)
|
|
776
|
+
|
|
777
|
+
if verbose:
|
|
778
|
+
print(f"Rotation alpha {np.rad2deg(tags['y-axis rotation alpha']):.1f} degree, "
|
|
779
|
+
f" beta {np.rad2deg(tags['x-axis rotation beta']):.1f} degree")
|
|
780
|
+
print(f"from zone axis {tags['zone_hkl']}")
|
|
781
|
+
print(f"Tilting {1} by {np.rad2deg(tags['mistilt_alpha']):.2f} "
|
|
782
|
+
f" in alpha and {np.rad2deg(tags['mistilt_beta']):.2f} in beta direction results in :")
|
|
783
|
+
# list(tags['zone_hkl'])
|
|
784
|
+
#
|
|
785
|
+
# print(f"zone axis {list(tags['nearest_zone_axis'])} with a mistilt of "
|
|
786
|
+
# f"{np.rad2deg(tags['mistilt_nearest_zone alpha']):.2f} in alpha "
|
|
787
|
+
# f"and {np.rad2deg(tags['mistilt_nearest_zone beta']):.2f} in beta direction")
|
|
788
|
+
nearest = tags['nearest_zone_axes']
|
|
789
|
+
print('Next nearest zone axes are:')
|
|
790
|
+
for i in range(1, nearest['amount']):
|
|
791
|
+
print(f"{nearest[str(i)]['hkl']}: mistilt: {np.rad2deg(nearest[str(i)]['mistilt_alpha']):6.2f}, "
|
|
792
|
+
f"{np.rad2deg(nearest[str(i)]['mistilt_beta']):6.2f}")
|
|
793
|
+
# rotate incident wave vector
|
|
794
|
+
k0_unit_vector = np.array([0, 0, 1]) # incident unit wave vector
|
|
795
|
+
k0_vector = k0_unit_vector * k_0 # incident wave vector
|
|
796
|
+
cent = k0_vector # center of Ewald sphere
|
|
797
|
+
|
|
798
|
+
if verbose:
|
|
799
|
+
print('Center of Ewald sphere ', k0_vector)
|
|
800
|
+
|
|
801
|
+
# #######################
|
|
802
|
+
# Find all Miller indices whose reciprocal point lies near the Ewald sphere with radius k_0
|
|
803
|
+
# within a maximum excitation error Sg
|
|
804
|
+
# #######################
|
|
805
|
+
|
|
806
|
+
hkl_max = tags['hkl_max']
|
|
807
|
+
Sg_max = tags['Sg_max'] # 1/Ang maximum allowed excitation error
|
|
808
|
+
|
|
809
|
+
h = np.linspace(-hkl_max, hkl_max, 2*hkl_max+1) # all evaluated single Miller Indices
|
|
810
|
+
hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
|
|
811
|
+
g_non_rot = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
|
|
812
|
+
g_norm = np.linalg.norm(g_non_rot, axis=1) # length of all vectors
|
|
813
|
+
not_zero = g_norm > 0
|
|
814
|
+
g_non_rot = g_non_rot[not_zero] # zero reflection will make problems further on, so we exclude it.
|
|
815
|
+
g_norm = g_norm[not_zero]
|
|
816
|
+
hkl_all = hkl[not_zero]
|
|
817
|
+
g = np.dot(g_non_rot, rotation_matrix)
|
|
818
|
+
|
|
819
|
+
# #######################
|
|
820
|
+
# Calculate excitation errors for all reciprocal_unit_cell points
|
|
821
|
+
# #######################
|
|
822
|
+
|
|
823
|
+
# Zuo and Spence, 'Adv TEM', 2017 -- Eq 3:14
|
|
824
|
+
S = (k_0**2-np.linalg.norm(g - k0_vector, axis=1)**2)/(2*k_0)
|
|
825
|
+
|
|
826
|
+
# g_mz = g - k0_vector
|
|
827
|
+
# in_sqrt = g_mz[:, 2]**2 + np.linalg.norm(g_mz, axis=1)**2 - k_0**2
|
|
828
|
+
# in_sqrt[in_sqrt < 0] = 0.
|
|
829
|
+
# S = -g_mz[:, 2] - np.sqrt(in_sqrt)
|
|
830
|
+
|
|
831
|
+
# #######################
|
|
832
|
+
# Determine reciprocal_unit_cell points with excitation error less than the maximum allowed one: Sg_max
|
|
833
|
+
# #######################
|
|
834
|
+
|
|
835
|
+
reflections = abs(S) < Sg_max # This is now a boolean array with True for all possible reflections
|
|
836
|
+
|
|
837
|
+
Sg = S[reflections]
|
|
838
|
+
g_hkl = g[reflections]
|
|
839
|
+
g_hkl_non_rot = g_non_rot[reflections]
|
|
840
|
+
hkl = hkl_all[reflections]
|
|
841
|
+
g_norm = g_norm[reflections]
|
|
842
|
+
|
|
843
|
+
if verbose:
|
|
844
|
+
print('Of the {0} tested reciprocal_unit_cell points, {1} have an excitation error less than {2:.2f} 1/nm'.
|
|
845
|
+
format(len(g), len(g_hkl), Sg_max))
|
|
846
|
+
|
|
847
|
+
# #################################
|
|
848
|
+
# Calculate Structure Factors
|
|
849
|
+
# ################################
|
|
850
|
+
|
|
851
|
+
structure_factors = []
|
|
852
|
+
for j in range(len(g_hkl)):
|
|
853
|
+
F = 0
|
|
854
|
+
for b in range(len(atoms)):
|
|
855
|
+
f = feq(atoms[b].symbol, g_norm[j]) # Atomic form factor for element and momentum change (g vector)
|
|
856
|
+
F += f * np.exp(-2*np.pi*1j*(g_hkl_non_rot[j]*atoms.positions[b]).sum())
|
|
857
|
+
structure_factors.append(F)
|
|
858
|
+
F = structure_factors = np.array(structure_factors)
|
|
859
|
+
|
|
860
|
+
# ###########################################
|
|
861
|
+
# Sort reflection in allowed and forbidden #
|
|
862
|
+
# ###########################################
|
|
863
|
+
|
|
864
|
+
allowed = np.absolute(F) > 0.000001 # allowed within numerical error
|
|
865
|
+
|
|
866
|
+
if verbose:
|
|
867
|
+
print('Of the {0} possible reflection {1} are allowed.'.format(hkl.shape[0], allowed.sum()))
|
|
868
|
+
|
|
869
|
+
# information of allowed reflections
|
|
870
|
+
s_g_allowed = Sg[allowed]
|
|
871
|
+
hkl_allowed = hkl[allowed][:]
|
|
872
|
+
g_allowed = g_hkl[allowed, :]
|
|
873
|
+
F_allowed = F[allowed]
|
|
874
|
+
g_norm_allowed = g_norm[allowed]
|
|
875
|
+
|
|
876
|
+
atoms.info['diffraction'] = {}
|
|
877
|
+
dif = atoms.info['diffraction']
|
|
878
|
+
dif['allowed'] = {}
|
|
879
|
+
dif['allowed']['Sg'] = s_g_allowed
|
|
880
|
+
dif['allowed']['hkl'] = hkl_allowed
|
|
881
|
+
dif['allowed']['g'] = g_allowed
|
|
882
|
+
dif['allowed']['structure factor'] = F_allowed
|
|
883
|
+
|
|
884
|
+
# Calculate Extinction Distance Reimer 7.23
|
|
885
|
+
# - makes only sense for non-zero F
|
|
886
|
+
|
|
887
|
+
xi_g = np.real(np.pi * volume_unit_cell * k_0 / F_allowed)
|
|
888
|
+
|
|
889
|
+
# ###########################
|
|
890
|
+
# Calculate Intensities (of allowed reflections)
|
|
891
|
+
# ###########################
|
|
892
|
+
|
|
893
|
+
# Calculate Intensity of beams Reimer 7.25
|
|
894
|
+
if 'thickness' not in tags:
|
|
895
|
+
tags['thickness'] = 0.
|
|
896
|
+
thickness = tags['thickness']
|
|
897
|
+
if thickness > 0.1:
|
|
898
|
+
I_g = np.real(np.pi ** 2 / xi_g ** 2 * np.sin(np.pi * thickness * s_g_allowed) ** 2 / (np.pi * s_g_allowed)**2)
|
|
899
|
+
dif['allowed']['Ig'] = I_g
|
|
900
|
+
|
|
901
|
+
dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
|
|
902
|
+
|
|
903
|
+
# information of forbidden reflections
|
|
904
|
+
forbidden = np.logical_not(allowed)
|
|
905
|
+
Sg_forbidden = Sg[forbidden]
|
|
906
|
+
hkl_forbidden = hkl[forbidden]
|
|
907
|
+
g_forbidden = g_hkl[forbidden]
|
|
908
|
+
# F_forbidden = F[forbidden]
|
|
909
|
+
|
|
910
|
+
dif['forbidden'] = {}
|
|
911
|
+
dif['forbidden']['Sg'] = Sg_forbidden
|
|
912
|
+
dif['forbidden']['hkl'] = hkl_forbidden
|
|
913
|
+
dif['forbidden']['g'] = g_forbidden
|
|
914
|
+
|
|
915
|
+
# ##########################
|
|
916
|
+
# Make pretty labels
|
|
917
|
+
# ##########################
|
|
918
|
+
hkl_label = make_pretty_labels(hkl_allowed)
|
|
919
|
+
dif['allowed']['label'] = hkl_label
|
|
920
|
+
hkl_label = make_pretty_labels(hkl_forbidden)
|
|
921
|
+
dif['forbidden']['label'] = hkl_label
|
|
922
|
+
|
|
923
|
+
# Center of Laue Circle
|
|
924
|
+
laue_circle = np.dot(tags['nearest_zone_axis'], tags['reciprocal_unit_cell'])
|
|
925
|
+
laue_circle = np.dot(laue_circle, rotation_matrix)
|
|
926
|
+
laue_circle = laue_circle / np.linalg.norm(laue_circle) * k_0
|
|
927
|
+
laue_circle[2] = 0
|
|
928
|
+
|
|
929
|
+
dif['Laue_circle'] = laue_circle
|
|
930
|
+
if verbose:
|
|
931
|
+
print('Laue_circle', laue_circle)
|
|
932
|
+
|
|
933
|
+
# ###########################
|
|
934
|
+
# Calculate Laue Zones (of allowed reflections)
|
|
935
|
+
# ###########################
|
|
936
|
+
# Below is the expression given in most books.
|
|
937
|
+
# However, that would only work for orthogonal crystal systems
|
|
938
|
+
# Laue_Zone = abs(np.dot(hkl_allowed,tags['zone_hkl'])) # works only for orthogonal systems
|
|
939
|
+
|
|
940
|
+
# This expression works for all crystal systems
|
|
941
|
+
# Remember we have already tilted, and so the dot product is trivial and gives only the z-component.
|
|
942
|
+
length_zone_axis = np.linalg.norm(np.dot(tags['zone_hkl'], tags['unit_cell']))
|
|
943
|
+
laue_zone = abs(np.dot(hkl_allowed, tags['nearest_zone_axis']))
|
|
944
|
+
dif['allowed']['Laue_Zone'] = laue_zone
|
|
945
|
+
|
|
946
|
+
ZOLZ_forbidden = abs(np.floor(g_forbidden[:, 2]*length_zone_axis+0.5)) == 0
|
|
947
|
+
|
|
948
|
+
dif['forbidden']['Laue_Zone'] = ZOLZ_forbidden
|
|
949
|
+
ZOLZ = laue_zone == 0
|
|
950
|
+
FOLZ = laue_zone == 1
|
|
951
|
+
SOLZ = laue_zone == 2
|
|
952
|
+
HOLZ = laue_zone > 0
|
|
953
|
+
HOLZp = laue_zone > 2
|
|
954
|
+
|
|
955
|
+
dif['allowed']['ZOLZ'] = ZOLZ
|
|
956
|
+
dif['allowed']['FOLZ'] = FOLZ
|
|
957
|
+
dif['allowed']['SOLZ'] = SOLZ
|
|
958
|
+
dif['allowed']['HOLZ'] = HOLZ
|
|
959
|
+
dif['allowed']['HOLZ_plus'] = dif['allowed']['HHOLZ'] = HOLZp
|
|
960
|
+
|
|
961
|
+
if verbose:
|
|
962
|
+
print(' There are {0} allowed reflections in the zero order Laue Zone'.format(ZOLZ.sum()))
|
|
963
|
+
print(' There are {0} allowed reflections in the first order Laue Zone'.format((laue_zone == 1).sum()))
|
|
964
|
+
print(' There are {0} allowed reflections in the second order Laue Zone'.format((laue_zone == 2).sum()))
|
|
965
|
+
print(' There are {0} allowed reflections in the other higher order Laue Zones'.format((laue_zone > 2).sum()))
|
|
966
|
+
|
|
967
|
+
if verbose == 2:
|
|
968
|
+
print(' hkl \t Laue zone \t Intensity (*1 and \t log) \t length \n')
|
|
969
|
+
for i in range(len(hkl_allowed)):
|
|
970
|
+
print(' {0} \t {1} \t {2:.3f} \t {3:.3f} \t {4:.3f} '.format(hkl_allowed[i], g_allowed[i],
|
|
971
|
+
intensities[i], np.log(intensities[i]+1),
|
|
972
|
+
g_norm_allowed[i]))
|
|
973
|
+
|
|
974
|
+
# ##########################
|
|
975
|
+
# Dynamically Activated forbidden reflections
|
|
976
|
+
# ##########################
|
|
977
|
+
|
|
978
|
+
double_diffraction = (np.sum(np.array(list(itertools.combinations(hkl_allowed[ZOLZ], 2))), axis=1))
|
|
979
|
+
|
|
980
|
+
dynamical_allowed = []
|
|
981
|
+
still_forbidden = []
|
|
982
|
+
for i, hkl in enumerate(hkl_forbidden):
|
|
983
|
+
if ZOLZ_forbidden[i]:
|
|
984
|
+
if hkl.tolist() in double_diffraction.tolist():
|
|
985
|
+
dynamical_allowed.append(i)
|
|
986
|
+
else:
|
|
987
|
+
still_forbidden.append(i)
|
|
988
|
+
dif['forbidden']['dynamically_activated'] = dynamical_allowed
|
|
989
|
+
dif['forbidden']['forbidden'] = dynamical_allowed
|
|
990
|
+
if verbose:
|
|
991
|
+
print('Length of zone axis vector in real space {0} nm'.format(np.round(length_zone_axis, 3)))
|
|
992
|
+
print(f'There are {len(dynamical_allowed)} forbidden but dynamical activated diffraction spots:')
|
|
993
|
+
# print(tags['forbidden']['hkl'][dynamical_allowed])
|
|
994
|
+
|
|
995
|
+
# ###################################
|
|
996
|
+
# Calculate HOLZ and Kikuchi Lines #
|
|
997
|
+
# ###################################
|
|
998
|
+
|
|
999
|
+
# Dynamic Correction
|
|
1000
|
+
|
|
1001
|
+
# Equation Spence+Zuo 3.86a
|
|
1002
|
+
gamma_1 = - 1./(2.*k_0) * (intensities / (2.*k_0*s_g_allowed)).sum()
|
|
1003
|
+
# print('gamma_1',gamma_1)
|
|
1004
|
+
|
|
1005
|
+
# Equation Spence+Zuo 3.84
|
|
1006
|
+
Kg = k_0 - k_0*gamma_1/(g_allowed[:, 2]+1e-15)
|
|
1007
|
+
Kg[ZOLZ] = k_0
|
|
1008
|
+
|
|
1009
|
+
# Calculate angle between K0 and deficient cone vector
|
|
1010
|
+
# For dynamic calculations K0 is replaced by Kg
|
|
1011
|
+
Kg[:] = k_0
|
|
1012
|
+
d_theta = np.arcsin(g_norm_allowed/Kg/2.)-np.arcsin(np.abs(g_allowed[:, 2])/g_norm_allowed)
|
|
1013
|
+
|
|
1014
|
+
# calculate length of distance of deficient cone to K0 in ZOLZ plane
|
|
1015
|
+
gd_length = 2*np.sin(d_theta/2)*k_0
|
|
1016
|
+
|
|
1017
|
+
# Calculate nearest point of HOLZ and Kikuchi lines
|
|
1018
|
+
g_closest = g_allowed.copy()
|
|
1019
|
+
g_closest = g_closest*(gd_length/np.linalg.norm(g_closest, axis=1))[:, np.newaxis]
|
|
1020
|
+
|
|
1021
|
+
g_closest[:, 2] = 0.
|
|
1022
|
+
|
|
1023
|
+
# calculate and save line in Hough space coordinates (distance and theta)
|
|
1024
|
+
slope = g_closest[:, 0]/(g_closest[:, 1]+1e-10)
|
|
1025
|
+
distance = gd_length
|
|
1026
|
+
theta = np.arctan2(g_allowed[:, 0], g_allowed[:, 1])
|
|
1027
|
+
|
|
1028
|
+
dif['HOLZ'] = {}
|
|
1029
|
+
dif['HOLZ']['slope'] = slope
|
|
1030
|
+
# a line is now given by
|
|
1031
|
+
|
|
1032
|
+
dif['HOLZ']['distance'] = distance
|
|
1033
|
+
dif['HOLZ']['theta'] = theta
|
|
1034
|
+
|
|
1035
|
+
dif['HOLZ']['g_deficient'] = g_closest
|
|
1036
|
+
dif['HOLZ']['g_excess'] = g_closest+g_allowed
|
|
1037
|
+
|
|
1038
|
+
dif['HOLZ']['ZOLZ'] = ZOLZ
|
|
1039
|
+
dif['HOLZ']['HOLZ'] = HOLZ
|
|
1040
|
+
dif['HOLZ']['FOLZ'] = FOLZ
|
|
1041
|
+
dif['HOLZ']['SOLZ'] = SOLZ
|
|
1042
|
+
dif['HOLZ']['HHOLZ'] = HOLZp # even higher HOLZ
|
|
1043
|
+
|
|
1044
|
+
dif['HOLZ']['hkl'] = dif['allowed']['hkl']
|
|
1045
|
+
dif['HOLZ']['intensities'] = intensities
|
|
1046
|
+
|
|
1047
|
+
####################################
|
|
1048
|
+
# Calculate HOLZ and Kikuchi Lines #
|
|
1049
|
+
####################################
|
|
1050
|
+
|
|
1051
|
+
tags_kikuchi = tags.copy()
|
|
1052
|
+
tags_kikuchi['mistilt_alpha'] = 0
|
|
1053
|
+
tags_kikuchi['mistilt_beta'] = 0
|
|
1054
|
+
|
|
1055
|
+
for i in range(1): # tags['nearest_zone_axes']['amount']):
|
|
1056
|
+
|
|
1057
|
+
zone_tags = tags['nearest_zone_axes'][str(i)]
|
|
1058
|
+
tags_kikuchi['zone_hkl'] = zone_tags['hkl']
|
|
1059
|
+
if verbose:
|
|
1060
|
+
print('Calculating Kikuchi lines for zone: ', zone_tags['hkl'])
|
|
1061
|
+
|
|
1062
|
+
tags_kikuchi['Laue_circle'] = laue_circle
|
|
1063
|
+
# Rotate to nearest zone axis
|
|
1064
|
+
rotation_matrix = get_zone_rotation(tags_kikuchi)
|
|
1065
|
+
|
|
1066
|
+
g_kikuchi_all = np.dot(g_non_rot, rotation_matrix)
|
|
1067
|
+
|
|
1068
|
+
ZOLZ = abs(g_kikuchi_all[:, 2]) < .1
|
|
1069
|
+
|
|
1070
|
+
g_kikuchi = g_kikuchi_all[ZOLZ]
|
|
1071
|
+
S = (k_0**2-np.linalg.norm(g_kikuchi - k0_vector, axis=1)**2)/(2*k_0)
|
|
1072
|
+
reflections = abs(S) < .01 # This is now a boolean array with True for all possible reflections
|
|
1073
|
+
g_kikuchi = g_kikuchi[reflections]
|
|
1074
|
+
hkl_kikuchi = (hkl_all[ZOLZ])[reflections]
|
|
1075
|
+
|
|
1076
|
+
structure_factors = []
|
|
1077
|
+
for j in range(len(g_kikuchi)):
|
|
1078
|
+
F = 0
|
|
1079
|
+
for b in range(len(atoms)):
|
|
1080
|
+
f = feq(atoms[b].symbol, np.linalg.norm(g_kikuchi[j]))
|
|
1081
|
+
F += f * np.exp(-2 * np.pi * 1j * (g_kikuchi[j] * atoms.positions[b]).sum())
|
|
1082
|
+
structure_factors.append(F)
|
|
1083
|
+
|
|
1084
|
+
F = np.array(structure_factors)
|
|
1085
|
+
|
|
1086
|
+
allowed_kikuchi = np.absolute(F) > 0.000001
|
|
1087
|
+
|
|
1088
|
+
g_kikuchi = g_kikuchi[allowed_kikuchi]
|
|
1089
|
+
hkl_kikuchi = hkl_kikuchi[allowed_kikuchi]
|
|
1090
|
+
|
|
1091
|
+
gd2 = g_kikuchi / 2.
|
|
1092
|
+
gd2[:, 2] = 0.
|
|
1093
|
+
|
|
1094
|
+
# calculate and save line in Hough space coordinates (distance and theta)
|
|
1095
|
+
slope2 = gd2[:, 0] / (gd2[:, 1] + 1e-20)
|
|
1096
|
+
distance2 = np.sqrt(gd2[:, 0] * gd2[:, 0] + gd2[:, 1] * gd2[:, 1])
|
|
1097
|
+
theta2 = np.arctan(slope2)
|
|
1098
|
+
|
|
1099
|
+
dif['Kikuchi'] = {}
|
|
1100
|
+
dif['Kikuchi']['slope'] = slope2
|
|
1101
|
+
dif['Kikuchi']['distance'] = distance2
|
|
1102
|
+
dif['Kikuchi']['theta'] = theta2
|
|
1103
|
+
dif['Kikuchi']['hkl'] = hkl_kikuchi
|
|
1104
|
+
dif['Kikuchi']['g_hkl'] = g_kikuchi
|
|
1105
|
+
dif['Kikuchi']['g_deficient'] = gd2
|
|
1106
|
+
dif['Kikuchi']['min_dist'] = gd2 + laue_circle
|
|
1107
|
+
|
|
1108
|
+
if verbose:
|
|
1109
|
+
print('pyTEMlib\'s \"kinematic_scattering\" finished')
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def make_pretty_labels(hkls, hex_label=False):
|
|
1113
|
+
"""Make pretty labels
|
|
1114
|
+
|
|
1115
|
+
Parameters
|
|
1116
|
+
----------
|
|
1117
|
+
hkls: np.ndarray
|
|
1118
|
+
a numpy array with all the Miller indices to be labeled
|
|
1119
|
+
hex_label: boolean - optional
|
|
1120
|
+
if True this will make for Miller indices.
|
|
1121
|
+
|
|
1122
|
+
Returns
|
|
1123
|
+
-------
|
|
1124
|
+
hkl_label: list
|
|
1125
|
+
list of labels in Latex format
|
|
1126
|
+
"""
|
|
1127
|
+
hkl_label = []
|
|
1128
|
+
for i in range(len(hkls)):
|
|
1129
|
+
h, k, l = np.array(hkls)[i]
|
|
1130
|
+
|
|
1131
|
+
if h < 0:
|
|
1132
|
+
h_string = r'[$\bar {' + str(int(-h)) + '},'
|
|
1133
|
+
else:
|
|
1134
|
+
h_string = r'[$\bar {' + str(int(h)) + '},'
|
|
1135
|
+
if k < 0:
|
|
1136
|
+
k_string = r'\bar {' + str(int(-k)) + '},'
|
|
1137
|
+
else:
|
|
1138
|
+
k_string = str(int(k)) + ','
|
|
1139
|
+
if hex_label:
|
|
1140
|
+
ii = -(h + k)
|
|
1141
|
+
if ii < 0:
|
|
1142
|
+
k_string = k_string + r'\bar {' + str(int(-ii)) + '},'
|
|
1143
|
+
else:
|
|
1144
|
+
k_string = k_string + str(int(ii)) + ','
|
|
1145
|
+
if l < 0:
|
|
1146
|
+
l_string = r'\bar {' + str(int(-l)) + '} $]'
|
|
1147
|
+
else:
|
|
1148
|
+
l_string = str(int(l)) + '} $]'
|
|
1149
|
+
label = h_string + k_string + l_string
|
|
1150
|
+
hkl_label.append(label)
|
|
1151
|
+
return hkl_label
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def feq(element, q):
|
|
1155
|
+
"""Atomic form factor parametrized in 1/Angstrom but converted to 1/Angstrom
|
|
1156
|
+
|
|
1157
|
+
The atomic form factor is from Kirkland: Advanced Computing in Electron Microscopy 2nd edition, Appendix C.
|
|
1158
|
+
From Appendix C of Kirkland, "Advanced Computing in Electron Microscopy", 3Ard ed.
|
|
1159
|
+
Calculation of electron form factor for specific q:
|
|
1160
|
+
Using equation Kirkland C.15
|
|
1161
|
+
|
|
1162
|
+
Parameters
|
|
1163
|
+
----------
|
|
1164
|
+
element: string
|
|
1165
|
+
element name
|
|
1166
|
+
q: float
|
|
1167
|
+
magnitude of scattering vector in 1/Angstrom -- (=> exp(-i*g.r), physics negative convention)
|
|
1168
|
+
|
|
1169
|
+
Returns
|
|
1170
|
+
-------
|
|
1171
|
+
fL+fG: float
|
|
1172
|
+
atomic scattering vector
|
|
1173
|
+
"""
|
|
1174
|
+
|
|
1175
|
+
if not isinstance(element, str):
|
|
1176
|
+
raise TypeError('Element has to be a string')
|
|
1177
|
+
if element not in electronFF:
|
|
1178
|
+
if len(element) > 2:
|
|
1179
|
+
raise TypeError('Please use standard convention for element abbreviation with not more than two letters')
|
|
1180
|
+
else:
|
|
1181
|
+
raise TypeError('Element {element} not known to electron diffraction should')
|
|
1182
|
+
if not isinstance(q, (float, int)):
|
|
1183
|
+
raise TypeError('Magnitude of scattering vector has to be a number of type float')
|
|
1184
|
+
|
|
1185
|
+
# q is in magnitude of scattering vector in 1/A -- (=> exp(-i*g.r), physics negative convention)
|
|
1186
|
+
param = electronFF[element]
|
|
1187
|
+
f_lorentzian = 0
|
|
1188
|
+
f_gauss = 0
|
|
1189
|
+
for i in range(3):
|
|
1190
|
+
f_lorentzian += param['fa'][i]/(q**2 + param['fb'][i])
|
|
1191
|
+
f_gauss += param['fc'][i]*np.exp(-q**2 * param['fd'][i])
|
|
1192
|
+
|
|
1193
|
+
# Conversion factor from scattering factors to volts. h^2/(2pi*m0*e), see e.g. Kirkland eqn. C.5
|
|
1194
|
+
# !NB RVolume is already in A unlike RPlanckConstant
|
|
1195
|
+
# scattering_factor_to_volts=(PlanckConstant**2)*(AngstromConversion**2)/(2*np.pi*ElectronMass*ElectronCharge)
|
|
1196
|
+
return f_lorentzian+f_gauss # * scattering_factor_to_volts
|