pyTEMlib 0.2023.8.0__py2.py3-none-any.whl → 0.2024.2.0__py2.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.
- pyTEMlib/config_dir.py +0 -1
- pyTEMlib/crystal_tools.py +22 -26
- pyTEMlib/eds_tools.py +499 -46
- pyTEMlib/eels_dialog.py +284 -899
- pyTEMlib/eels_dialog_utilities.py +218 -341
- pyTEMlib/eels_tools.py +1526 -1583
- pyTEMlib/file_tools.py +52 -48
- pyTEMlib/graph_tools.py +3 -4
- pyTEMlib/image_tools.py +171 -41
- pyTEMlib/info_widget.py +618 -276
- pyTEMlib/kinematic_scattering.py +77 -512
- pyTEMlib/peak_dialog.py +162 -288
- pyTEMlib/version.py +2 -2
- pyTEMlib/xrpa_x_sections.py +173 -97
- {pyTEMlib-0.2023.8.0.dist-info → pyTEMlib-0.2024.2.0.dist-info}/LICENSE +1 -1
- {pyTEMlib-0.2023.8.0.dist-info → pyTEMlib-0.2024.2.0.dist-info}/METADATA +2 -2
- pyTEMlib-0.2024.2.0.dist-info/RECORD +35 -0
- {pyTEMlib-0.2023.8.0.dist-info → pyTEMlib-0.2024.2.0.dist-info}/WHEEL +1 -1
- pyTEMlib/eels_dlg.py +0 -252
- pyTEMlib/info_dialog.py +0 -665
- pyTEMlib/info_dlg.py +0 -239
- pyTEMlib/interactive_eels.py +0 -35
- pyTEMlib/viz.py +0 -481
- pyTEMlib-0.2023.8.0.dist-info/RECORD +0 -40
- {pyTEMlib-0.2023.8.0.dist-info → pyTEMlib-0.2024.2.0.dist-info}/entry_points.txt +0 -0
- {pyTEMlib-0.2023.8.0.dist-info → pyTEMlib-0.2024.2.0.dist-info}/top_level.txt +0 -0
pyTEMlib/kinematic_scattering.py
CHANGED
|
@@ -26,9 +26,12 @@ All the input and output is done through a ase.Atoms object and the dictionary i
|
|
|
26
26
|
|
|
27
27
|
# numerical packages used
|
|
28
28
|
import numpy as np
|
|
29
|
-
import scipy
|
|
29
|
+
import scipy
|
|
30
30
|
import itertools
|
|
31
31
|
|
|
32
|
+
import ase
|
|
33
|
+
import ase.build
|
|
34
|
+
|
|
32
35
|
# plotting package used
|
|
33
36
|
import matplotlib.pylab as plt # basic plotting
|
|
34
37
|
|
|
@@ -86,8 +89,6 @@ def Zuo_fig_3_18(verbose=True):
|
|
|
86
89
|
# Create Silicon structure (Could be produced with Silicon routine)
|
|
87
90
|
if verbose:
|
|
88
91
|
print('Sample Input for Figure 3.18 in Zuo and Spence \"Advanced TEM\", 2017')
|
|
89
|
-
import ase
|
|
90
|
-
import ase.build
|
|
91
92
|
a = 5.14 # A
|
|
92
93
|
atoms = ase.build.bulk('Si', 'diamond', a=a, cubic=True)
|
|
93
94
|
|
|
@@ -147,31 +148,31 @@ def Zuo_fig_3_18(verbose=True):
|
|
|
147
148
|
return atoms
|
|
148
149
|
|
|
149
150
|
|
|
150
|
-
def
|
|
151
|
+
def get_rotation_matrix(angles, in_radians=False):
|
|
151
152
|
""" Rotation of zone axis by mistilt
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
rotation_matrix: np.ndarray (3x3)
|
|
164
|
+
rotation matrix in 3d
|
|
165
|
+
"""
|
|
165
166
|
|
|
166
167
|
if not isinstance(angles, (np.ndarray, list)):
|
|
167
168
|
raise TypeError('angles must be a list of float of length 3')
|
|
168
169
|
if len(angles) != 3:
|
|
169
170
|
raise TypeError('angles must be a list of float of length 3')
|
|
170
|
-
if not isinstance(zone, (np.ndarray, list)):
|
|
171
|
-
raise TypeError('Miller indices must be a list of int of length 3')
|
|
172
|
-
|
|
173
|
-
alpha, beta, gamma = np.radians(angles)
|
|
174
171
|
|
|
172
|
+
if in_radians:
|
|
173
|
+
alpha, beta, gamma = angles
|
|
174
|
+
else:
|
|
175
|
+
alpha, beta, gamma = np.radians(angles)
|
|
175
176
|
# first we rotate alpha about x-axis
|
|
176
177
|
c, s = np.cos(alpha), np.sin(alpha)
|
|
177
178
|
rot_x = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
@@ -183,8 +184,29 @@ def zone_mistilt(zone, angles):
|
|
|
183
184
|
# third we rotate gamma about z-axis
|
|
184
185
|
c, s = np.cos(gamma), np.sin(gamma)
|
|
185
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')
|
|
186
207
|
|
|
187
|
-
|
|
208
|
+
rotation_matrix = get_rotation_matrix(angles)
|
|
209
|
+
return np.dot(zone, rotation_matrix)
|
|
188
210
|
|
|
189
211
|
|
|
190
212
|
def get_metric_tensor(matrix):
|
|
@@ -213,12 +235,27 @@ def get_wavelength(acceleration_voltage):
|
|
|
213
235
|
Returns:
|
|
214
236
|
-------
|
|
215
237
|
wavelength: float
|
|
216
|
-
wave length in Angstrom
|
|
238
|
+
wave length in Angstrom (= meter *10**10)
|
|
217
239
|
"""
|
|
218
240
|
if not isinstance(acceleration_voltage, (int, float)):
|
|
219
241
|
raise TypeError('Acceleration voltage has to be a real number')
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
259
|
|
|
223
260
|
|
|
224
261
|
def find_nearest_zone_axis(tags):
|
|
@@ -226,12 +263,7 @@ def find_nearest_zone_axis(tags):
|
|
|
226
263
|
|
|
227
264
|
hkl_max = 5
|
|
228
265
|
# Make all hkl indices
|
|
229
|
-
|
|
230
|
-
hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
|
|
231
|
-
|
|
232
|
-
# delete [0,0,0]
|
|
233
|
-
index = int(len(hkl) / 2)
|
|
234
|
-
zones_hkl = np.delete(hkl, index, axis=0) # delete [0,0,0]
|
|
266
|
+
zones_hkl = get_all_miller_indices(hkl_max)
|
|
235
267
|
|
|
236
268
|
# make zone axis in reciprocal space
|
|
237
269
|
zones_g = np.dot(zones_hkl, tags['reciprocal_unit_cell']) # all evaluated reciprocal_unit_cell points
|
|
@@ -285,15 +317,10 @@ def find_angles(zone):
|
|
|
285
317
|
|
|
286
318
|
def stage_rotation_matrix(alpha, beta):
|
|
287
319
|
""" Microscope stage coordinate system """
|
|
288
|
-
|
|
289
|
-
# FIRST we rotate beta about x-axis
|
|
290
|
-
c, s = np.cos(beta), np.sin(beta)
|
|
291
|
-
rot_x = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
292
|
-
# second we rotate alpha about y-axis
|
|
293
|
-
c, s = np.cos(alpha), np.sin(alpha)
|
|
294
|
-
rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
|
295
320
|
|
|
296
|
-
|
|
321
|
+
# FIRST we rotate beta about x-axis
|
|
322
|
+
angles = [beta, alpha, 0.]
|
|
323
|
+
return get_rotation_matrix(angles, in_radians=True)
|
|
297
324
|
|
|
298
325
|
|
|
299
326
|
# ##################
|
|
@@ -301,7 +328,8 @@ def stage_rotation_matrix(alpha, beta):
|
|
|
301
328
|
# We determine spherical coordinates to do that
|
|
302
329
|
# ##################
|
|
303
330
|
|
|
304
|
-
|
|
331
|
+
|
|
332
|
+
def get_zone_rotation(tags):
|
|
305
333
|
"""zone axis in global coordinate system"""
|
|
306
334
|
|
|
307
335
|
zone_hkl = tags['zone_hkl']
|
|
@@ -504,13 +532,7 @@ def ring_pattern_calculation(atoms, verbose=False):
|
|
|
504
532
|
# inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
|
|
505
533
|
|
|
506
534
|
hkl_max = tags['hkl_max']
|
|
507
|
-
|
|
508
|
-
h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller Indices
|
|
509
|
-
hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
|
|
510
|
-
|
|
511
|
-
# delete [0,0,0]
|
|
512
|
-
index_center = int(len(hkl) / 2)
|
|
513
|
-
hkl = np.delete(hkl, index_center, axis=0) # delete [0,0,0]
|
|
535
|
+
hkl = get_all_miller_indices(hkl_max)
|
|
514
536
|
|
|
515
537
|
g_hkl = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
|
|
516
538
|
|
|
@@ -527,7 +549,7 @@ def ring_pattern_calculation(atoms, verbose=False):
|
|
|
527
549
|
|
|
528
550
|
structure_factors.append(F)
|
|
529
551
|
|
|
530
|
-
F =
|
|
552
|
+
F = np.array(structure_factors) # structure factors
|
|
531
553
|
|
|
532
554
|
# Sort reflection in allowed and forbidden #
|
|
533
555
|
|
|
@@ -673,7 +695,7 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
673
695
|
# Check sanity
|
|
674
696
|
if atoms.info is None:
|
|
675
697
|
atoms.info = {'output': {}, 'experimental': {}}
|
|
676
|
-
|
|
698
|
+
if 'output' in atoms.info:
|
|
677
699
|
output = atoms.info['output']
|
|
678
700
|
else:
|
|
679
701
|
output = atoms.info['output'] = {}
|
|
@@ -719,7 +741,11 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
719
741
|
angstrom_conversion = 1.0e10 # So [1A (in m)] * angstrom_conversion = 1
|
|
720
742
|
# NanometerConversion = 1.0e9
|
|
721
743
|
|
|
722
|
-
|
|
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
|
|
723
749
|
tags['inner_potential_V'] = u0 * scattering_factor_to_volts
|
|
724
750
|
if verbose:
|
|
725
751
|
print(f'The inner potential is {u0:.1f} V')
|
|
@@ -746,7 +772,7 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
746
772
|
# ############
|
|
747
773
|
|
|
748
774
|
# get rotation matrix to rotate zone axis onto z-axis
|
|
749
|
-
rotation_matrix =
|
|
775
|
+
rotation_matrix = get_zone_rotation(tags)
|
|
750
776
|
|
|
751
777
|
if verbose:
|
|
752
778
|
print(f"Rotation alpha {np.rad2deg(tags['y-axis rotation alpha']):.1f} degree, "
|
|
@@ -854,22 +880,7 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
854
880
|
dif['allowed']['hkl'] = hkl_allowed
|
|
855
881
|
dif['allowed']['g'] = g_allowed
|
|
856
882
|
dif['allowed']['structure factor'] = F_allowed
|
|
857
|
-
|
|
858
|
-
# Calculate Extinction Distance Reimer 7.23
|
|
859
|
-
# - makes only sense for non-zero F
|
|
860
|
-
|
|
861
|
-
xi_g = np.real(np.pi * volume_unit_cell * k_0 / F_allowed)
|
|
862
883
|
|
|
863
|
-
# Calculate Intensity of beams Reimer 7.25
|
|
864
|
-
if 'thickness' not in tags:
|
|
865
|
-
tags['thickness'] = 0.
|
|
866
|
-
thickness = tags['thickness']
|
|
867
|
-
if thickness > 0.1:
|
|
868
|
-
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)
|
|
869
|
-
dif['allowed']['Ig'] = I_g
|
|
870
|
-
|
|
871
|
-
dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
|
|
872
|
-
|
|
873
884
|
# Calculate Extinction Distance Reimer 7.23
|
|
874
885
|
# - makes only sense for non-zero F
|
|
875
886
|
|
|
@@ -894,7 +905,7 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
894
905
|
Sg_forbidden = Sg[forbidden]
|
|
895
906
|
hkl_forbidden = hkl[forbidden]
|
|
896
907
|
g_forbidden = g_hkl[forbidden]
|
|
897
|
-
F_forbidden = F[forbidden]
|
|
908
|
+
# F_forbidden = F[forbidden]
|
|
898
909
|
|
|
899
910
|
dif['forbidden'] = {}
|
|
900
911
|
dif['forbidden']['Sg'] = Sg_forbidden
|
|
@@ -1050,7 +1061,7 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
1050
1061
|
|
|
1051
1062
|
tags_kikuchi['Laue_circle'] = laue_circle
|
|
1052
1063
|
# Rotate to nearest zone axis
|
|
1053
|
-
rotation_matrix =
|
|
1064
|
+
rotation_matrix = get_zone_rotation(tags_kikuchi)
|
|
1054
1065
|
|
|
1055
1066
|
g_kikuchi_all = np.dot(g_non_rot, rotation_matrix)
|
|
1056
1067
|
|
|
@@ -1098,452 +1109,6 @@ def kinematic_scattering(atoms, verbose=False):
|
|
|
1098
1109
|
print('pyTEMlib\'s \"kinematic_scattering\" finished')
|
|
1099
1110
|
|
|
1100
1111
|
|
|
1101
|
-
def kinematic_scattering2(atoms, verbose=False):
|
|
1102
|
-
"""
|
|
1103
|
-
All kinematic scattering calculation
|
|
1104
|
-
|
|
1105
|
-
Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
|
|
1106
|
-
|
|
1107
|
-
Parameters
|
|
1108
|
-
----------
|
|
1109
|
-
atoms: ase.Atoms
|
|
1110
|
-
object with crystal structure:
|
|
1111
|
-
and with experimental parameters in info attribute:
|
|
1112
|
-
'acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max'
|
|
1113
|
-
Optional parameters are:
|
|
1114
|
-
'mistilt', convergence_angle_mrad', and 'crystal_name'
|
|
1115
|
-
verbose = True will give extended output of the calculation
|
|
1116
|
-
verbose: boolean
|
|
1117
|
-
default is False
|
|
1118
|
-
|
|
1119
|
-
Returns
|
|
1120
|
-
-------
|
|
1121
|
-
ato,s:
|
|
1122
|
-
There are three sub_dictionaries in info attribute:
|
|
1123
|
-
['allowed'], ['forbidden'], and ['HOLZ']
|
|
1124
|
-
['allowed'] and ['forbidden'] dictionaries contain:
|
|
1125
|
-
['Sg'], ['hkl'], ['g'], ['structure factor'], ['intensities'],
|
|
1126
|
-
['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], ['HHOLZ'], ['label'], and ['Laue_zone']
|
|
1127
|
-
the ['HOLZ'] dictionary contains:
|
|
1128
|
-
['slope'], ['distance'], ['theta'], ['g_deficient'], ['g_excess'], ['hkl'], ['intensities'],
|
|
1129
|
-
['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], and ['HHOLZ']
|
|
1130
|
-
Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
|
|
1131
|
-
|
|
1132
|
-
There are also a few parameters stored in the main dictionary:
|
|
1133
|
-
['wave_length_nm'], ['reciprocal_unit_cell'], ['inner_potential_V'], ['incident_wave_vector'],
|
|
1134
|
-
['volume'], ['theta'], ['phi'], and ['incident_wave_vector_vacuum']
|
|
1135
|
-
"""
|
|
1136
|
-
|
|
1137
|
-
# Check sanity
|
|
1138
|
-
if atoms.info is None:
|
|
1139
|
-
atoms.info = {'output': {}, 'experimental': {}}
|
|
1140
|
-
elif 'output' in atoms.info:
|
|
1141
|
-
output = atoms.info['output']
|
|
1142
|
-
else:
|
|
1143
|
-
output = atoms.info['output'] = {}
|
|
1144
|
-
|
|
1145
|
-
output['SpotPattern'] = True
|
|
1146
|
-
|
|
1147
|
-
if 'experimental' not in atoms.info:
|
|
1148
|
-
tags = atoms.info['experimental'] = {}
|
|
1149
|
-
|
|
1150
|
-
if not check_sanity(atoms):
|
|
1151
|
-
print('Input is not complete, stopping')
|
|
1152
|
-
print('Try \'example()\' for example input')
|
|
1153
|
-
return
|
|
1154
|
-
|
|
1155
|
-
tags = atoms.info['experimental']
|
|
1156
|
-
|
|
1157
|
-
# wavelength
|
|
1158
|
-
tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
|
|
1159
|
-
|
|
1160
|
-
# volume of unit_cell
|
|
1161
|
-
unit_cell = atoms.cell.array
|
|
1162
|
-
metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
|
|
1163
|
-
tags['metric_tensor'] = metric_tensor
|
|
1164
|
-
volume_unit_cell = atoms.cell.volume
|
|
1165
|
-
|
|
1166
|
-
# reciprocal_unit_cell
|
|
1167
|
-
|
|
1168
|
-
# We use the linear algebra package of numpy to invert the unit_cell "matrix"
|
|
1169
|
-
reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
|
|
1170
|
-
tags['reciprocal_unit_cell'] = reciprocal_unit_cell
|
|
1171
|
-
inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
|
|
1172
|
-
|
|
1173
|
-
if verbose:
|
|
1174
|
-
print('reciprocal_unit_cell')
|
|
1175
|
-
print(np.round(reciprocal_unit_cell, 3))
|
|
1176
|
-
|
|
1177
|
-
############################################
|
|
1178
|
-
# Incident wave vector k0 in vacuum and material
|
|
1179
|
-
############################################
|
|
1180
|
-
|
|
1181
|
-
u0 = 0.0 # in (Ang)
|
|
1182
|
-
# atom form factor of zero reflection angle is the inner potential in 1/A
|
|
1183
|
-
for i in range(len(atoms)):
|
|
1184
|
-
u0 += feq(atoms[i].symbol, 0.0)
|
|
1185
|
-
|
|
1186
|
-
scattering_factor_to_volts = (const.h ** 2) * (1e10 ** 2) / (2 * np.pi * const.m_e * const.e) * volume_unit_cell
|
|
1187
|
-
|
|
1188
|
-
tags['inner_potential_V'] = u0 * scattering_factor_to_volts
|
|
1189
|
-
if verbose:
|
|
1190
|
-
print(f'The inner potential is {u0:.1f} V')
|
|
1191
|
-
|
|
1192
|
-
# Calculating incident wave vector magnitude 'k0' in material
|
|
1193
|
-
wl = tags['wave_length']
|
|
1194
|
-
tags['incident_wave_vector_vacuum'] = 1 / wl
|
|
1195
|
-
|
|
1196
|
-
k0 = tags['incident_wave_vector'] = np.sqrt(1 / wl**2 + u0) # 1/Ang
|
|
1197
|
-
|
|
1198
|
-
tags['convergence_angle_A-1'] = k0 * np.sin(tags['convergence_angle_mrad'] / 1000.)
|
|
1199
|
-
if verbose:
|
|
1200
|
-
print(f"Using an acceleration voltage of {tags['acceleration_voltage_V']/1000:.1f}kV")
|
|
1201
|
-
print(f'Magnitude of incident wave vector in material: {k0:.1f} 1/Ang and in vacuum: {1/wl:.1f} 1/Ang')
|
|
1202
|
-
print(f"Which is an wave length of {1 / k0 * 100.:.3f} pm in the material and {wl * 100.:.3f} pm "
|
|
1203
|
-
f"in the vacuum")
|
|
1204
|
-
print(f"The convergence angle of {tags['convergence_angle_mrad']:.1f}mrad "
|
|
1205
|
-
f"= {tags['convergence_angle_A-1']:.2f} 1/A")
|
|
1206
|
-
print(f"Magnitude of incident wave vector in material: {k0:.1f} 1/A which is a wavelength {100/k0:.3f} pm")
|
|
1207
|
-
|
|
1208
|
-
# ############
|
|
1209
|
-
# Rotate
|
|
1210
|
-
# ############
|
|
1211
|
-
|
|
1212
|
-
# get rotation matrix to rotate zone axis onto z-axis
|
|
1213
|
-
rotation_matrix = get_rotation_matrix(tags)
|
|
1214
|
-
|
|
1215
|
-
if verbose:
|
|
1216
|
-
print(f"Rotation alpha {np.rad2deg(tags['y-axis rotation alpha']):.1f} degree, "
|
|
1217
|
-
f" beta {np.rad2deg(tags['x-axis rotation beta']):.1f} degree")
|
|
1218
|
-
print(f"from zone axis {tags['zone_hkl']}")
|
|
1219
|
-
print(f"Tilting {1} by {np.rad2deg(tags['mistilt_alpha']):.2f} "
|
|
1220
|
-
f" in alpha and {np.rad2deg(tags['mistilt_beta']):.2f} in beta direction results in :")
|
|
1221
|
-
# list(tags['zone_hkl'])
|
|
1222
|
-
#
|
|
1223
|
-
# print(f"zone axis {list(tags['nearest_zone_axis'])} with a mistilt of "
|
|
1224
|
-
# f"{np.rad2deg(tags['mistilt_nearest_zone alpha']):.2f} in alpha "
|
|
1225
|
-
# f"and {np.rad2deg(tags['mistilt_nearest_zone beta']):.2f} in beta direction")
|
|
1226
|
-
nearest = tags['nearest_zone_axes']
|
|
1227
|
-
print('Next nearest zone axes are:')
|
|
1228
|
-
for i in range(1, nearest['amount']):
|
|
1229
|
-
print(f"{nearest[str(i)]['hkl']}: mistilt: {np.rad2deg(nearest[str(i)]['mistilt_alpha']):6.2f}, "
|
|
1230
|
-
f"{np.rad2deg(nearest[str(i)]['mistilt_beta']):6.2f}")
|
|
1231
|
-
k0_unit_vector = np.array([0, 0, 1]) # incident unit wave vector
|
|
1232
|
-
k0_vector = k0_unit_vector * k0 # incident wave vector
|
|
1233
|
-
cent = k0_vector # center of Ewald sphere
|
|
1234
|
-
|
|
1235
|
-
if verbose:
|
|
1236
|
-
print('Center of Ewald sphere ', cent)
|
|
1237
|
-
|
|
1238
|
-
# Find all Miller indices whose reciprocal point lays near the Ewald sphere with radius k0
|
|
1239
|
-
# within a maximum excitation error Sg
|
|
1240
|
-
hkl_max = tags['hkl_max']
|
|
1241
|
-
Sg_max = tags['Sg_max'] # 1/A maximum allowed excitation error
|
|
1242
|
-
|
|
1243
|
-
h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller indices
|
|
1244
|
-
hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
|
|
1245
|
-
g_non_rot = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
|
|
1246
|
-
|
|
1247
|
-
g = np.dot(g_non_rot, rotation_matrix) # rotate these reciprocal_unit_cell points
|
|
1248
|
-
g_norm = vector_norm(g) # length of all vectors
|
|
1249
|
-
not_zero = g_norm > 0
|
|
1250
|
-
g = g[not_zero] # zero reflection will make problems further on, so we exclude it.
|
|
1251
|
-
g_non_rot = g_non_rot[not_zero]
|
|
1252
|
-
g_norm = g_norm[not_zero]
|
|
1253
|
-
hkl = hkl[not_zero]
|
|
1254
|
-
|
|
1255
|
-
# Calculate excitation errors for all reciprocal_unit_cell points
|
|
1256
|
-
# Zuo and Spence, 'Adv TEM', 2017 -- Eq 3:14
|
|
1257
|
-
S = (k0 ** 2 - vector_norm(g - cent) ** 2) / (2 * k0)
|
|
1258
|
-
g_mz = g - k0_vector
|
|
1259
|
-
in_sqrt = g_mz[:, 2]**2 + np.linalg.norm(g_mz, axis=1)**2 - k0**2
|
|
1260
|
-
in_sqrt[in_sqrt < 0] = 0.
|
|
1261
|
-
S2 = -g_mz[:, 2] - np.sqrt(in_sqrt)
|
|
1262
|
-
|
|
1263
|
-
# Determine reciprocal_unit_cell points with excitation error less than the maximum allowed one: Sg_max
|
|
1264
|
-
|
|
1265
|
-
reflections = abs(S) < Sg_max # This is now a boolean array with True for all possible reflections
|
|
1266
|
-
hkl_all = hkl.copy()
|
|
1267
|
-
s_g = S[reflections]
|
|
1268
|
-
g_hkl = g[reflections]
|
|
1269
|
-
|
|
1270
|
-
hkl = hkl[reflections]
|
|
1271
|
-
g_hkl_non_rot = g_non_rot[reflections]
|
|
1272
|
-
g_norm = g_norm[reflections]
|
|
1273
|
-
|
|
1274
|
-
if verbose:
|
|
1275
|
-
print(f"Of the {len(g)} tested reciprocal_unit_cell points, {len(g_hkl)} "
|
|
1276
|
-
f"have an excitation error less than {Sg_max:.2f} 1/Angstrom")
|
|
1277
|
-
|
|
1278
|
-
# Calculate Structure Factors
|
|
1279
|
-
base = atoms.positions
|
|
1280
|
-
structure_factors = []
|
|
1281
|
-
for j in range(len(g_hkl)):
|
|
1282
|
-
F = 0
|
|
1283
|
-
for b in range(len(atoms)):
|
|
1284
|
-
f = feq(atoms[b].symbol, np.linalg.norm(g_hkl[j]))
|
|
1285
|
-
F += f * np.exp(-2 * np.pi * 1j * (g_hkl_non_rot[j] * atoms.positions[b]).sum())
|
|
1286
|
-
|
|
1287
|
-
structure_factors.append(F)
|
|
1288
|
-
|
|
1289
|
-
F = structure_factors = np.array(structure_factors)
|
|
1290
|
-
|
|
1291
|
-
# Sort reflection in allowed and forbidden #
|
|
1292
|
-
allowed = np.absolute(F) > 0.000001 # allowed within numerical error
|
|
1293
|
-
|
|
1294
|
-
if verbose:
|
|
1295
|
-
print(f"Of the {hkl.shape[0]} possible reflection {allowed.sum()} are allowed.")
|
|
1296
|
-
|
|
1297
|
-
# information of allowed reflections
|
|
1298
|
-
s_g_allowed = s_g[allowed]
|
|
1299
|
-
hkl_allowed = hkl[allowed][:]
|
|
1300
|
-
g_allowed = g_hkl[allowed, :]
|
|
1301
|
-
F_allowed = F[allowed]
|
|
1302
|
-
g_norm_allowed = g_norm[allowed]
|
|
1303
|
-
|
|
1304
|
-
atoms.info['diffraction'] = {}
|
|
1305
|
-
dif = atoms.info['diffraction']
|
|
1306
|
-
dif['allowed'] = {}
|
|
1307
|
-
dif['allowed']['Sg'] = s_g_allowed
|
|
1308
|
-
dif['allowed']['hkl'] = hkl_allowed
|
|
1309
|
-
dif['allowed']['g'] = g_allowed
|
|
1310
|
-
dif['allowed']['structure factor'] = F_allowed
|
|
1311
|
-
|
|
1312
|
-
# Calculate Extinction Distance Reimer 7.23
|
|
1313
|
-
# - makes only sense for non-zero F
|
|
1314
|
-
|
|
1315
|
-
xi_g = np.real(np.pi * volume_unit_cell * k0 / F_allowed)
|
|
1316
|
-
|
|
1317
|
-
# Calculate Intensity of beams Reimer 7.25
|
|
1318
|
-
if 'thickness' not in tags:
|
|
1319
|
-
tags['thickness'] = 0.
|
|
1320
|
-
thickness = tags['thickness']
|
|
1321
|
-
if thickness > 0.1:
|
|
1322
|
-
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)
|
|
1323
|
-
dif['allowed']['Ig'] = I_g
|
|
1324
|
-
|
|
1325
|
-
dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
|
|
1326
|
-
|
|
1327
|
-
# information of forbidden reflections
|
|
1328
|
-
forbidden = np.logical_not(allowed)
|
|
1329
|
-
s_g_forbidden = s_g[forbidden]
|
|
1330
|
-
hkl_forbidden = hkl[forbidden]
|
|
1331
|
-
g_forbidden = g_hkl[forbidden]
|
|
1332
|
-
F_forbidden = F[forbidden]
|
|
1333
|
-
|
|
1334
|
-
dif['forbidden'] = {}
|
|
1335
|
-
dif['forbidden']['Sg'] = s_g_forbidden
|
|
1336
|
-
dif['forbidden']['hkl'] = hkl_forbidden.copy()
|
|
1337
|
-
dif['forbidden']['g'] = g_forbidden
|
|
1338
|
-
|
|
1339
|
-
# Make pretty labels
|
|
1340
|
-
hkl_label = make_pretty_labels(hkl_allowed)
|
|
1341
|
-
dif['allowed']['label'] = hkl_label
|
|
1342
|
-
hkl_label = make_pretty_labels(hkl_forbidden)
|
|
1343
|
-
dif['forbidden']['label'] = hkl_label
|
|
1344
|
-
|
|
1345
|
-
# Dynamically Allowed Reflection
|
|
1346
|
-
"""
|
|
1347
|
-
indices = range(len(hkl_allowed))
|
|
1348
|
-
combinations = [list(x) for x in itertools.permutations(indices, 2)]
|
|
1349
|
-
hkl_forbidden = hkl_forbidden.tolist()
|
|
1350
|
-
dynamically_allowed = np.zeros(len(hkl_forbidden), dtype=bool)
|
|
1351
|
-
for [i, j] in combinations:
|
|
1352
|
-
possible = (hkl_allowed[i] + hkl_allowed[j]).tolist()
|
|
1353
|
-
if possible in hkl_forbidden:
|
|
1354
|
-
dynamically
|
|
1355
|
-
_allowed[hkl_forbidden.index(possible)] = True
|
|
1356
|
-
dif['forbidden']['dynamically_allowed'] = dynamically_allowed
|
|
1357
|
-
|
|
1358
|
-
if verbose:
|
|
1359
|
-
print(f"Of the {g_forbidden.shape[0]} forbidden reflection {dif['dynamically_allowed']['g'].shape[0]} "
|
|
1360
|
-
f"can be dynamically activated.")
|
|
1361
|
-
# print(dif['forbidden']['hkl'][dynamically_allowed])
|
|
1362
|
-
"""
|
|
1363
|
-
|
|
1364
|
-
# Center of Laue Circle
|
|
1365
|
-
laue_circle = np.dot(tags['nearest_zone_axis'], tags['reciprocal_unit_cell'])
|
|
1366
|
-
laue_circle = np.dot(laue_circle, rotation_matrix)
|
|
1367
|
-
laue_circle = laue_circle / np.linalg.norm(laue_circle) * k0
|
|
1368
|
-
laue_circle[2] = 0
|
|
1369
|
-
|
|
1370
|
-
dif['Laue_circle'] = laue_circle
|
|
1371
|
-
if verbose:
|
|
1372
|
-
print('Laue_circle', laue_circle)
|
|
1373
|
-
|
|
1374
|
-
# ###########################
|
|
1375
|
-
# Calculate Laue Zones (of allowed reflections)
|
|
1376
|
-
# ###########################
|
|
1377
|
-
# Below is the expression given in most books.
|
|
1378
|
-
# However, that would only work for orthogonal crystal systems
|
|
1379
|
-
# Laue_Zone = abs(np.dot(hkl_allowed,tags['zone_hkl'])) # works only for orthogonal systems
|
|
1380
|
-
|
|
1381
|
-
# This expression works for all crystal systems
|
|
1382
|
-
# Remember we have already tilted, and so the dot product is trivial and gives only the z-component.
|
|
1383
|
-
|
|
1384
|
-
Laue_Zone = abs(np.dot(hkl_allowed, tags['nearest_zone_axis']))
|
|
1385
|
-
dif['allowed']['Laue_Zone'] = Laue_Zone
|
|
1386
|
-
|
|
1387
|
-
ZOLZ = Laue_Zone == 0
|
|
1388
|
-
FOLZ = Laue_Zone == 1
|
|
1389
|
-
SOLZ = Laue_Zone == 2
|
|
1390
|
-
HOLZ = Laue_Zone > 2
|
|
1391
|
-
|
|
1392
|
-
dif['allowed']['ZOLZ'] = ZOLZ
|
|
1393
|
-
dif['allowed']['FOLZ'] = FOLZ
|
|
1394
|
-
dif['allowed']['SOLZ'] = SOLZ
|
|
1395
|
-
dif['allowed']['HOLZ'] = HOLZ
|
|
1396
|
-
|
|
1397
|
-
if verbose:
|
|
1398
|
-
print(' There are {0} allowed reflections in the zero order Laue Zone'.format(ZOLZ.sum()))
|
|
1399
|
-
print(' There are {0} allowed reflections in the first order Laue Zone'.format((Laue_Zone == 1).sum()))
|
|
1400
|
-
print(' There are {0} allowed reflections in the second order Laue Zone'.format((Laue_Zone == 2).sum()))
|
|
1401
|
-
print(' There are {0} allowed reflections in the higher order Laue Zone'.format((Laue_Zone > 2).sum()))
|
|
1402
|
-
|
|
1403
|
-
if verbose:
|
|
1404
|
-
print(' hkl \t Laue zone \t Intensity (*1 and \t log) \t length \n')
|
|
1405
|
-
for i in range(len(hkl_allowed)):
|
|
1406
|
-
print(' {0} \t {1} \t {2:.3f} \t {3:.3f} \t {4:.3f} '.format(hkl_allowed[i],
|
|
1407
|
-
g_allowed[i], intensities[i],
|
|
1408
|
-
np.log(intensities[i] + 1),
|
|
1409
|
-
g_norm_allowed[i]))
|
|
1410
|
-
|
|
1411
|
-
####################################
|
|
1412
|
-
# Calculate HOLZ and Kikuchi Lines #
|
|
1413
|
-
####################################
|
|
1414
|
-
|
|
1415
|
-
tags_new_zone = tags.copy()
|
|
1416
|
-
tags_new_zone['mistilt_alpha'] = 0
|
|
1417
|
-
tags_new_zone['mistilt_beta'] = 0
|
|
1418
|
-
|
|
1419
|
-
for i in range(1): # tags['nearest_zone_axes']['amount']):
|
|
1420
|
-
|
|
1421
|
-
zone_tags = tags['nearest_zone_axes'][str(i)]
|
|
1422
|
-
|
|
1423
|
-
if verbose:
|
|
1424
|
-
print('Calculating Kikuchi lines for zone: ', zone_tags['hkl'])
|
|
1425
|
-
|
|
1426
|
-
laue_circle = np.dot(zone_tags['hkl'], tags['reciprocal_unit_cell'])
|
|
1427
|
-
laue_circle = np.dot(laue_circle, rotation_matrix)
|
|
1428
|
-
laue_circle = laue_circle / np.linalg.norm(laue_circle) * k0
|
|
1429
|
-
laue_circle[2] = 0
|
|
1430
|
-
|
|
1431
|
-
zone_tags['Laue_circle'] = laue_circle
|
|
1432
|
-
# Rotate to nearest zone axis
|
|
1433
|
-
|
|
1434
|
-
tags_new_zone['zone_hkl']
|
|
1435
|
-
|
|
1436
|
-
theta = -(zone_tags['mistilt_alpha'])
|
|
1437
|
-
phi = -(zone_tags['mistilt_beta'])
|
|
1438
|
-
|
|
1439
|
-
# first we rotate phi about z-axis
|
|
1440
|
-
c, s = np.cos(phi), np.sin(phi)
|
|
1441
|
-
rot_z = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
1442
|
-
|
|
1443
|
-
# second we rotate theta about y-axis
|
|
1444
|
-
c, s = np.cos(theta), np.sin(theta)
|
|
1445
|
-
rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
|
1446
|
-
|
|
1447
|
-
# the rotation now makes z-axis coincide with plane normal
|
|
1448
|
-
|
|
1449
|
-
rotation_matrix2 = np.dot(rot_z, rot_y)
|
|
1450
|
-
|
|
1451
|
-
g_kikuchi_all = np.dot(g, rotation_matrix2)
|
|
1452
|
-
ZOLZ = abs(g_kikuchi_all[:, 2]) < 1
|
|
1453
|
-
|
|
1454
|
-
g_kikuchi = g_kikuchi_all[ZOLZ]
|
|
1455
|
-
S = (k0 ** 2 - vector_norm(g_kikuchi - np.array([0, 0, k0])) ** 2) / (2 * k0)
|
|
1456
|
-
reflections = abs(S) < 0.1 # This is now a boolean array with True for all possible reflections
|
|
1457
|
-
g_hkl_kikuchi2 = g_kikuchi[reflections]
|
|
1458
|
-
hkl_kikuchi2 = (hkl_all[ZOLZ])[reflections]
|
|
1459
|
-
|
|
1460
|
-
structure_factors = []
|
|
1461
|
-
for j in range(len(g_hkl_kikuchi2)):
|
|
1462
|
-
F = 0
|
|
1463
|
-
for b in range(len(atoms)):
|
|
1464
|
-
f = feq(atoms[b].symbol, np.linalg.norm(g_hkl_kikuchi2[j]))
|
|
1465
|
-
F += f * np.exp(-2 * np.pi * 1j * (g_hkl_kikuchi2[j] * atoms.positions[b]).sum())
|
|
1466
|
-
|
|
1467
|
-
structure_factors.append(F)
|
|
1468
|
-
|
|
1469
|
-
F = np.array(structure_factors)
|
|
1470
|
-
|
|
1471
|
-
allowed_kikuchi = np.absolute(F) > 0.000001
|
|
1472
|
-
|
|
1473
|
-
g_hkl_kikuchi = g_hkl_kikuchi2[allowed_kikuchi]
|
|
1474
|
-
hkl_kikuchi = hkl_kikuchi2[allowed_kikuchi]
|
|
1475
|
-
|
|
1476
|
-
gd2 = g_hkl_kikuchi / 2.
|
|
1477
|
-
gd2[:, 2] = 0.
|
|
1478
|
-
|
|
1479
|
-
# calculate and save line in Hough space coordinates (distance and theta)
|
|
1480
|
-
slope2 = gd2[:, 0] / (gd2[:, 1] + 1e-20)
|
|
1481
|
-
distance2 = np.sqrt(gd2[:, 0] * gd2[:, 0] + gd2[:, 1] * gd2[:, 1])
|
|
1482
|
-
theta2 = np.arctan(slope2)
|
|
1483
|
-
|
|
1484
|
-
dif['Kikuchi'] = {}
|
|
1485
|
-
dif['Kikuchi']['slope'] = slope2
|
|
1486
|
-
dif['Kikuchi']['distance'] = distance2
|
|
1487
|
-
dif['Kikuchi']['theta'] = theta2
|
|
1488
|
-
dif['Kikuchi']['hkl'] = hkl_kikuchi
|
|
1489
|
-
dif['Kikuchi']['g_hkl'] = g_hkl_kikuchi
|
|
1490
|
-
dif['Kikuchi']['g_deficient'] = gd2
|
|
1491
|
-
dif['Kikuchi']['min dist'] = gd2 + laue_circle
|
|
1492
|
-
|
|
1493
|
-
k_g = k0
|
|
1494
|
-
|
|
1495
|
-
# Dynamic Correction
|
|
1496
|
-
# Does not correct ZOLZ lines !!!!
|
|
1497
|
-
# Equation Spence+Zuo 3.86a
|
|
1498
|
-
if 'dynamic correction' in tags:
|
|
1499
|
-
if tags['dynamic correction']:
|
|
1500
|
-
gamma_1 = - 1. / (2. * k0) * (intensities / (2. * k0 * s_g_allowed)).sum()
|
|
1501
|
-
if verbose:
|
|
1502
|
-
print('Dynamic correction gamma_1: ', gamma_1)
|
|
1503
|
-
|
|
1504
|
-
# Equation Spence+Zuo 3.84
|
|
1505
|
-
k_g = k0 - k0 * gamma_1 / g_allowed[:, 2]
|
|
1506
|
-
|
|
1507
|
-
# k_g = np.dot( [0,0,k0], rotation_matrix)
|
|
1508
|
-
# Calculate angle between k0 and deficient cone vector
|
|
1509
|
-
# For dynamic calculations k0 is replaced by k_g
|
|
1510
|
-
d_theta = np.arcsin(g_norm_allowed / k_g / 2.) - np.arcsin(np.abs(g_allowed[:, 2]) / g_norm_allowed)
|
|
1511
|
-
|
|
1512
|
-
# calculate length of distance of deficient cone to k0 in ZOLZ plane
|
|
1513
|
-
gd_length = 2 * np.sin(d_theta / 2) * k0
|
|
1514
|
-
|
|
1515
|
-
# Calculate nearest point of HOLZ and Kikuchi lines
|
|
1516
|
-
gd = g_allowed.copy()
|
|
1517
|
-
gd[:, 0] = -gd[:, 0] * gd_length / g_norm_allowed
|
|
1518
|
-
gd[:, 1] = -gd[:, 1] * gd_length / g_norm_allowed
|
|
1519
|
-
gd[:, 2] = 0.
|
|
1520
|
-
|
|
1521
|
-
# calculate and save line in Hough space coordinates (distance and theta)
|
|
1522
|
-
slope = gd[:, 0] / (gd[:, 1] + 1e-20)
|
|
1523
|
-
distance = gd_length
|
|
1524
|
-
theta = np.arctan(slope)
|
|
1525
|
-
|
|
1526
|
-
dif['HOLZ'] = {}
|
|
1527
|
-
dif['HOLZ']['slope'] = slope
|
|
1528
|
-
# a line is now given by
|
|
1529
|
-
dif['HOLZ']['distance'] = distance
|
|
1530
|
-
dif['HOLZ']['theta'] = theta
|
|
1531
|
-
dif['HOLZ']['g_deficient'] = gd
|
|
1532
|
-
dif['HOLZ']['g_excess'] = gd + g_allowed
|
|
1533
|
-
dif['HOLZ']['g_allowed'] = g_allowed.copy()
|
|
1534
|
-
|
|
1535
|
-
dif['HOLZ']['ZOLZ'] = ZOLZ
|
|
1536
|
-
dif['HOLZ']['HOLZ'] = np.logical_not(ZOLZ)
|
|
1537
|
-
dif['HOLZ']['FOLZ'] = FOLZ
|
|
1538
|
-
dif['HOLZ']['SOLZ'] = SOLZ
|
|
1539
|
-
dif['HOLZ']['HHOLZ'] = HOLZ # even higher HOLZ
|
|
1540
|
-
|
|
1541
|
-
dif['HOLZ']['hkl'] = dif['allowed']['hkl']
|
|
1542
|
-
dif['HOLZ']['intensities'] = intensities
|
|
1543
|
-
|
|
1544
|
-
print('done')
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
1112
|
def make_pretty_labels(hkls, hex_label=False):
|
|
1548
1113
|
"""Make pretty labels
|
|
1549
1114
|
|