pyTEMlib 0.2025.4.2__py3-none-any.whl → 0.2025.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyTEMlib might be problematic. Click here for more details.

Files changed (94) hide show
  1. build/lib/pyTEMlib/__init__.py +33 -0
  2. build/lib/pyTEMlib/animation.py +640 -0
  3. build/lib/pyTEMlib/atom_tools.py +238 -0
  4. build/lib/pyTEMlib/config_dir.py +31 -0
  5. build/lib/pyTEMlib/crystal_tools.py +1219 -0
  6. build/lib/pyTEMlib/diffraction_plot.py +756 -0
  7. build/lib/pyTEMlib/dynamic_scattering.py +293 -0
  8. build/lib/pyTEMlib/eds_tools.py +826 -0
  9. build/lib/pyTEMlib/eds_xsections.py +432 -0
  10. build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
  11. build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  12. build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
  13. build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  14. build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  15. build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  16. build/lib/pyTEMlib/file_reader.py +274 -0
  17. build/lib/pyTEMlib/file_tools.py +811 -0
  18. build/lib/pyTEMlib/get_bote_salvat.py +69 -0
  19. build/lib/pyTEMlib/graph_tools.py +1153 -0
  20. build/lib/pyTEMlib/graph_viz.py +599 -0
  21. build/lib/pyTEMlib/image/__init__.py +37 -0
  22. build/lib/pyTEMlib/image/image_atoms.py +270 -0
  23. build/lib/pyTEMlib/image/image_clean.py +197 -0
  24. build/lib/pyTEMlib/image/image_distortion.py +299 -0
  25. build/lib/pyTEMlib/image/image_fft.py +277 -0
  26. build/lib/pyTEMlib/image/image_graph.py +926 -0
  27. build/lib/pyTEMlib/image/image_registration.py +316 -0
  28. build/lib/pyTEMlib/image/image_utilities.py +309 -0
  29. build/lib/pyTEMlib/image/image_window.py +421 -0
  30. build/lib/pyTEMlib/image_tools.py +699 -0
  31. build/lib/pyTEMlib/interactive_image.py +1 -0
  32. build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
  33. build/lib/pyTEMlib/microscope.py +61 -0
  34. build/lib/pyTEMlib/probe_tools.py +906 -0
  35. build/lib/pyTEMlib/sidpy_tools.py +153 -0
  36. build/lib/pyTEMlib/simulation_tools.py +104 -0
  37. build/lib/pyTEMlib/test.py +437 -0
  38. build/lib/pyTEMlib/utilities.py +314 -0
  39. build/lib/pyTEMlib/version.py +5 -0
  40. build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
  41. pyTEMlib/__init__.py +25 -3
  42. pyTEMlib/animation.py +31 -22
  43. pyTEMlib/atom_tools.py +29 -34
  44. pyTEMlib/config_dir.py +2 -28
  45. pyTEMlib/crystal_tools.py +129 -165
  46. pyTEMlib/eds_tools.py +559 -342
  47. pyTEMlib/eds_xsections.py +432 -0
  48. pyTEMlib/eels_tools/__init__.py +44 -0
  49. pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  50. pyTEMlib/eels_tools/eels_database.py +134 -0
  51. pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  52. pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  53. pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  54. pyTEMlib/file_reader.py +274 -0
  55. pyTEMlib/file_tools.py +260 -1130
  56. pyTEMlib/get_bote_salvat.py +69 -0
  57. pyTEMlib/graph_tools.py +101 -174
  58. pyTEMlib/graph_viz.py +150 -0
  59. pyTEMlib/image/__init__.py +37 -0
  60. pyTEMlib/image/image_atoms.py +270 -0
  61. pyTEMlib/image/image_clean.py +197 -0
  62. pyTEMlib/image/image_distortion.py +299 -0
  63. pyTEMlib/image/image_fft.py +277 -0
  64. pyTEMlib/image/image_graph.py +926 -0
  65. pyTEMlib/image/image_registration.py +316 -0
  66. pyTEMlib/image/image_utilities.py +309 -0
  67. pyTEMlib/image/image_window.py +421 -0
  68. pyTEMlib/image_tools.py +154 -928
  69. pyTEMlib/kinematic_scattering.py +1 -1
  70. pyTEMlib/probe_tools.py +1 -1
  71. pyTEMlib/test.py +437 -0
  72. pyTEMlib/utilities.py +314 -0
  73. pyTEMlib/version.py +2 -3
  74. pyTEMlib/xrpa_x_sections.py +14 -10
  75. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
  76. pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
  77. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
  78. pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
  79. pyTEMlib/core_loss_widget.py +0 -721
  80. pyTEMlib/eels_dialog.py +0 -754
  81. pyTEMlib/eels_dialog_utilities.py +0 -1199
  82. pyTEMlib/eels_tools.py +0 -2359
  83. pyTEMlib/file_tools_qt.py +0 -193
  84. pyTEMlib/image_dialog.py +0 -158
  85. pyTEMlib/image_dlg.py +0 -146
  86. pyTEMlib/info_widget.py +0 -1086
  87. pyTEMlib/info_widget3.py +0 -1120
  88. pyTEMlib/low_loss_widget.py +0 -479
  89. pyTEMlib/peak_dialog.py +0 -1129
  90. pyTEMlib/peak_dlg.py +0 -286
  91. pytemlib-0.2025.4.2.dist-info/RECORD +0 -38
  92. pytemlib-0.2025.4.2.dist-info/top_level.txt +0 -1
  93. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
  94. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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