advisor-scattering 0.5.0__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.
- advisor/__init__.py +3 -0
- advisor/__main__.py +7 -0
- advisor/app.py +40 -0
- advisor/controllers/__init__.py +6 -0
- advisor/controllers/app_controller.py +69 -0
- advisor/controllers/feature_controller.py +25 -0
- advisor/domain/__init__.py +23 -0
- advisor/domain/core/__init__.py +8 -0
- advisor/domain/core/lab.py +121 -0
- advisor/domain/core/lattice.py +79 -0
- advisor/domain/core/sample.py +101 -0
- advisor/domain/geometry.py +212 -0
- advisor/domain/unit_converter.py +82 -0
- advisor/features/__init__.py +6 -0
- advisor/features/scattering_geometry/controllers/__init__.py +5 -0
- advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
- advisor/features/scattering_geometry/domain/__init__.py +5 -0
- advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
- advisor/features/scattering_geometry/domain/core.py +516 -0
- advisor/features/scattering_geometry/ui/__init__.py +5 -0
- advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
- advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
- advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
- advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
- advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
- advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
- advisor/features/structure_factor/controllers/__init__.py +6 -0
- advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
- advisor/features/structure_factor/domain/__init__.py +6 -0
- advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
- advisor/features/structure_factor/ui/__init__.py +6 -0
- advisor/features/structure_factor/ui/components/__init__.py +12 -0
- advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
- advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
- advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
- advisor/resources/__init__.py +0 -0
- advisor/resources/config/app_config.json +14 -0
- advisor/resources/config/tips.json +4 -0
- advisor/resources/data/nacl.cif +111 -0
- advisor/resources/icons/bz_caculator.jpg +0 -0
- advisor/resources/icons/bz_calculator.png +0 -0
- advisor/resources/icons/minus.svg +3 -0
- advisor/resources/icons/placeholder.png +0 -0
- advisor/resources/icons/plus.svg +3 -0
- advisor/resources/icons/reset.png +0 -0
- advisor/resources/icons/sf_calculator.jpg +0 -0
- advisor/resources/icons/sf_calculator.png +0 -0
- advisor/resources/icons.qrc +6 -0
- advisor/resources/qss/styles.qss +348 -0
- advisor/resources/resources_rc.py +83 -0
- advisor/ui/__init__.py +7 -0
- advisor/ui/init_window.py +566 -0
- advisor/ui/main_window.py +174 -0
- advisor/ui/tab_interface.py +44 -0
- advisor/ui/tips.py +30 -0
- advisor/ui/utils/__init__.py +6 -0
- advisor/ui/utils/readcif.py +129 -0
- advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
- advisor/ui/visualizers/__init__.py +8 -0
- advisor/ui/visualizers/coordinate_visualizer.py +203 -0
- advisor/ui/visualizers/scattering_visualizer.py +301 -0
- advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
- advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
- advisor/ui/visualizers/unitcell_visualizer.py +518 -0
- advisor_scattering-0.5.0.dist-info/METADATA +122 -0
- advisor_scattering-0.5.0.dist-info/RECORD +69 -0
- advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
- advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
- advisor_scattering-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Core calculation functions for Brillouin calculator.
|
|
4
|
+
|
|
5
|
+
This module contains the pure computational functions for Brillouin zone calculations
|
|
6
|
+
that don't depend on the BrillouinCalculator class.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.optimize import fsolve
|
|
11
|
+
|
|
12
|
+
from advisor.domain import angle_to_matrix
|
|
13
|
+
from advisor.domain.core import Lab
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_real_space_vectors(a, b, c, alpha, beta, gamma):
|
|
17
|
+
"""Get the real space vectors a_vec, b_vec, c_vec from the lattice parameters.
|
|
18
|
+
- a_vec is by-default along x-axis (a, 0, 0)
|
|
19
|
+
- b_vec is by-default (b cos gamma, b sin gamma, 0) on the x-y plane,
|
|
20
|
+
- c_vec is then calculated
|
|
21
|
+
The above convention defines the crystal coordinate system.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
a, b, c (float): Lattice constants in Angstroms
|
|
25
|
+
alpha, beta, gamma (float): Lattice angles in degrees
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
a_vec, b_vec, c_vec (np.ndarray): Real space vectors
|
|
29
|
+
"""
|
|
30
|
+
alpha_rad, beta_rad, gamma_rad = (
|
|
31
|
+
np.radians(alpha),
|
|
32
|
+
np.radians(beta),
|
|
33
|
+
np.radians(gamma),
|
|
34
|
+
)
|
|
35
|
+
a_vec = np.array([a, 0, 0])
|
|
36
|
+
b_vec = np.array([b * np.cos(gamma_rad), b * np.sin(gamma_rad), 0])
|
|
37
|
+
c_vec_x = c * np.cos(beta_rad)
|
|
38
|
+
c_vec_y = (
|
|
39
|
+
c
|
|
40
|
+
* (np.cos(alpha_rad) - np.cos(beta_rad) * np.cos(gamma_rad))
|
|
41
|
+
/ np.sin(gamma_rad)
|
|
42
|
+
)
|
|
43
|
+
c_vec_z = np.sqrt(c**2 - c_vec_x**2 - c_vec_y**2)
|
|
44
|
+
c_vec = np.array([c_vec_x, c_vec_y, c_vec_z])
|
|
45
|
+
return a_vec, b_vec, c_vec
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_reciprocal_space_vectors(a, b, c, alpha, beta, gamma):
|
|
49
|
+
"""Get the reciprocal space vectors a_star_vec, b_star_vec, c_star_vec from the lattice
|
|
50
|
+
parameters, angles in degrees. These vectors are in the crystal coordinate system.
|
|
51
|
+
"""
|
|
52
|
+
a_vec, b_vec, c_vec = _get_real_space_vectors(a, b, c, alpha, beta, gamma)
|
|
53
|
+
volumn = abs(np.dot(a_vec, np.cross(b_vec, c_vec)))
|
|
54
|
+
a_star_vec = 2 * np.pi * np.cross(b_vec, c_vec) / volumn
|
|
55
|
+
b_star_vec = 2 * np.pi * np.cross(c_vec, a_vec) / volumn
|
|
56
|
+
c_star_vec = 2 * np.pi * np.cross(a_vec, b_vec) / volumn
|
|
57
|
+
return a_star_vec, b_star_vec, c_star_vec
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_norm_vector(h, k, l, a, b, c, alpha, beta, gamma):
|
|
61
|
+
"""Get the norm vector of the plane defined by the Miller indices (h, k, l)."""
|
|
62
|
+
a_star_vec, b_star_vec, c_star_vec = _get_reciprocal_space_vectors(
|
|
63
|
+
a, b, c, alpha, beta, gamma
|
|
64
|
+
)
|
|
65
|
+
norm_vec = (
|
|
66
|
+
h * a_star_vec / (2 * np.pi)
|
|
67
|
+
+ k * b_star_vec / (2 * np.pi)
|
|
68
|
+
+ l * c_star_vec / (2 * np.pi)
|
|
69
|
+
)
|
|
70
|
+
return norm_vec
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_d_spacing(h, k, l, a, b, c, alpha, beta, gamma):
|
|
74
|
+
"""Get the d-spacing of the plane defined by the Miller indices (h, k, l)."""
|
|
75
|
+
norm_vec = _get_norm_vector(h, k, l, a, b, c, alpha, beta, gamma)
|
|
76
|
+
d_spacing = 1 / np.linalg.norm(norm_vec)
|
|
77
|
+
return d_spacing
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_momentum_diffraction(h, k, l, a, b, c, alpha, beta, gamma):
|
|
81
|
+
"""Get the momentum transfer vector of the plane defined by the Miller indices (h, k, l)."""
|
|
82
|
+
norm_vec = _get_norm_vector(h, k, l, a, b, c, alpha, beta, gamma)
|
|
83
|
+
return 2 * np.pi * norm_vec
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_HKL_from_momentum_scattering(momentum, a_vec, b_vec, c_vec):
|
|
87
|
+
"""Get the HKL (r.l.u.) from the momentum transfer vector."""
|
|
88
|
+
H = np.dot(momentum, a_vec) / (2 * np.pi)
|
|
89
|
+
K = np.dot(momentum, b_vec) / (2 * np.pi)
|
|
90
|
+
L = np.dot(momentum, c_vec) / (2 * np.pi)
|
|
91
|
+
return H, K, L
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def calculate_k_magnitude(k_in, tth):
|
|
95
|
+
"""Calculate the momentum transfer magnitude from the scattering angle."""
|
|
96
|
+
return 2 * k_in * np.sin(np.radians(tth / 2.0))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def calculate_tth_from_k_magnitude(k_in, k_magnitude):
|
|
100
|
+
"""calculate the scattering angle tth from the momentum transfer magnitude"""
|
|
101
|
+
return 2 * np.degrees(np.arcsin(k_magnitude / (2 * k_in)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def calculate_k_vector_in_lab(k_in, tth):
|
|
105
|
+
"""get the momentum transfer k vector in lab frame from the scattering angle tth"""
|
|
106
|
+
eta = 90 - tth / 2
|
|
107
|
+
eta_rad = np.radians(eta)
|
|
108
|
+
k_magnitude = calculate_k_magnitude(k_in, tth)
|
|
109
|
+
#k_vector = k_magnitude * np.array([-np.cos(eta_rad), 0, -np.sin(eta_rad)])
|
|
110
|
+
k_vector = k_magnitude * np.array([-np.sin(eta_rad), -np.cos(eta_rad), 0])
|
|
111
|
+
return k_vector
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def derivative(fun, x, delta_x=1e-6):
|
|
115
|
+
"""calculate the derivative of the function fun at the point x"""
|
|
116
|
+
return (fun(x + delta_x) - fun(x - delta_x)) / (2 * delta_x)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def process_angle(angle):
|
|
120
|
+
"""process the angle to be in the range of (-180, 180]"""
|
|
121
|
+
angle = angle % 360
|
|
122
|
+
if angle > 180:
|
|
123
|
+
angle -= 360
|
|
124
|
+
return angle
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _calculate_angles_factory(fixed_angle_name):
|
|
128
|
+
if fixed_angle_name == "chi":
|
|
129
|
+
return _calculate_angles_chi_fixed
|
|
130
|
+
elif fixed_angle_name == "phi":
|
|
131
|
+
return _calculate_angles_phi_fixed
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _calculate_angles_tth_fixed(
|
|
135
|
+
k_in,
|
|
136
|
+
tth,
|
|
137
|
+
a,
|
|
138
|
+
b,
|
|
139
|
+
c,
|
|
140
|
+
alpha,
|
|
141
|
+
beta,
|
|
142
|
+
gamma,
|
|
143
|
+
roll,
|
|
144
|
+
pitch,
|
|
145
|
+
yaw,
|
|
146
|
+
H=0.15,
|
|
147
|
+
K=0.1,
|
|
148
|
+
L=None,
|
|
149
|
+
fixed_angle_name="chi",
|
|
150
|
+
fixed_angle=0.0,
|
|
151
|
+
):
|
|
152
|
+
"""Calculate scattering angles from two of the three HKL indices, with tth (in degrees) fixed.
|
|
153
|
+
|
|
154
|
+
Two steps involved:
|
|
155
|
+
|
|
156
|
+
1. Use fsolve to find the missing momentum transfer component (H, K, or L). IT IS POSSIBLE THAT
|
|
157
|
+
THERE ARE MULTIPLE SOLUTIONS, BUT HERE WE ONLY RETURN THE ONE CLOSE TO THE NEGATIVE VALUE.
|
|
158
|
+
2. Use optimization algorithm to find the theta and phi/chi angles that satisfy the condition
|
|
159
|
+
for the given HKL indices while keeping one angle fixed.
|
|
160
|
+
|
|
161
|
+
There could be more than one solution, so the function returns a list of solutions.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
165
|
+
tth (float): Scattering angle in degrees
|
|
166
|
+
a, b, c (float): Lattice constants in Angstroms
|
|
167
|
+
alpha, beta, gamma (float): sample rotation angles in degrees
|
|
168
|
+
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
169
|
+
H (float, optional): momentum transfer in reciprocal length unit (r.l.u.). Defaults to 0.15.
|
|
170
|
+
K (float, optional): momentum transfer in reciprocal length unit (r.l.u.). Defaults to 0.1.
|
|
171
|
+
L (float, optional): momentum transfer in reciprocal length unit (r.l.u.). Defaults to None.
|
|
172
|
+
fixed_angle_name (str, optional): Name of the angle to fix ("chi" or "phi"). Defaults to "chi".
|
|
173
|
+
fixed_angle (float, optional): Value of the fixed angle in degrees. Defaults to 0.0.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
tuple: Five values containing the calculated results:
|
|
177
|
+
- tth_result (float/list): Scattering angle value(s) in degrees
|
|
178
|
+
- theta_result (float/list): Sample theta rotation value(s) in degrees
|
|
179
|
+
- phi_result (float/list): Sample phi rotation value(s) in degrees
|
|
180
|
+
- chi_result (float/list): Sample chi rotation value(s) in degrees
|
|
181
|
+
- momentum (float): Solved momentum transfer component (H, K, or L depending on which was None)
|
|
182
|
+
"""
|
|
183
|
+
# initial k_vec_lab when sample has not rotated
|
|
184
|
+
k_magnitude_target = calculate_k_magnitude(k_in, tth)
|
|
185
|
+
lab = Lab()
|
|
186
|
+
lab.initialize(a, b, c, alpha, beta, gamma, roll, pitch, yaw, 0, 0, 0)
|
|
187
|
+
a_star_vec_lab, b_star_vec_lab, c_star_vec_lab = lab.get_reciprocal_space_vectors()
|
|
188
|
+
|
|
189
|
+
# Define which index is None and will be solved for
|
|
190
|
+
index_to_solve = None
|
|
191
|
+
if H is None:
|
|
192
|
+
index_to_solve = "H"
|
|
193
|
+
elif K is None:
|
|
194
|
+
index_to_solve = "K"
|
|
195
|
+
elif L is None:
|
|
196
|
+
index_to_solve = "L"
|
|
197
|
+
|
|
198
|
+
def fun_to_solve(momentum):
|
|
199
|
+
h_val = momentum if index_to_solve == "H" else H
|
|
200
|
+
k_val = momentum if index_to_solve == "K" else K
|
|
201
|
+
l_val = momentum if index_to_solve == "L" else L
|
|
202
|
+
k = h_val * a_star_vec_lab + k_val * b_star_vec_lab + l_val * c_star_vec_lab
|
|
203
|
+
k_magnitude = np.linalg.norm(k)
|
|
204
|
+
return k_magnitude - k_magnitude_target
|
|
205
|
+
|
|
206
|
+
momentum = fsolve(fun_to_solve, -1.0)
|
|
207
|
+
|
|
208
|
+
# Update the appropriate index
|
|
209
|
+
if index_to_solve == "H":
|
|
210
|
+
H = momentum[0]
|
|
211
|
+
elif index_to_solve == "K":
|
|
212
|
+
K = momentum[0]
|
|
213
|
+
elif index_to_solve == "L":
|
|
214
|
+
L = momentum[0]
|
|
215
|
+
|
|
216
|
+
calculate_angles = _calculate_angles_factory(fixed_angle_name)
|
|
217
|
+
|
|
218
|
+
tth_result, theta_result, phi_result, chi_result = calculate_angles(
|
|
219
|
+
k_in,
|
|
220
|
+
H,
|
|
221
|
+
K,
|
|
222
|
+
L,
|
|
223
|
+
a,
|
|
224
|
+
b,
|
|
225
|
+
c,
|
|
226
|
+
alpha,
|
|
227
|
+
beta,
|
|
228
|
+
gamma,
|
|
229
|
+
roll,
|
|
230
|
+
pitch,
|
|
231
|
+
yaw,
|
|
232
|
+
fixed_angle,
|
|
233
|
+
)
|
|
234
|
+
return tth_result, theta_result, phi_result, chi_result, momentum[0]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _calculate_angles_chi_fixed(
|
|
238
|
+
k_in,
|
|
239
|
+
H,
|
|
240
|
+
K,
|
|
241
|
+
L,
|
|
242
|
+
a,
|
|
243
|
+
b,
|
|
244
|
+
c,
|
|
245
|
+
alpha,
|
|
246
|
+
beta,
|
|
247
|
+
gamma,
|
|
248
|
+
roll,
|
|
249
|
+
pitch,
|
|
250
|
+
yaw,
|
|
251
|
+
chi_fixed,
|
|
252
|
+
target_objective=1e-7,
|
|
253
|
+
num_steps=3000,
|
|
254
|
+
learning_rate=100,
|
|
255
|
+
):
|
|
256
|
+
"""Calculate scattering angles with chi angle (in degrees) fixed.
|
|
257
|
+
|
|
258
|
+
Uses optimization algorithm to find the theta and phi angles that satisfy the condition
|
|
259
|
+
for the given HKL indices while keeping chi fixed at the specified value. There could be more
|
|
260
|
+
than one solution, so the function returns a list of solutions.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
264
|
+
H, K, L (float): momentum transfer in reciprocal length unit (r.l.u.),
|
|
265
|
+
a, b, c (float): Lattice constants in Angstroms
|
|
266
|
+
alpha, beta, gamma (float): sample rotation angles in degrees
|
|
267
|
+
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
268
|
+
chi_fixed (float): Fixed chi angle in degrees
|
|
269
|
+
target_objective (float, optional): Convergence criterion for optimization. Defaults to 1e-5.
|
|
270
|
+
num_steps (int, optional): Maximum number of optimization steps. Defaults to 1000.
|
|
271
|
+
learning_rate (float, optional): Learning rate for the gradient descent. Defaults to 100.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
tuple: Four lists containing the calculated values:
|
|
275
|
+
- tth_result (list): Scattering angle values in degrees
|
|
276
|
+
- theta_result (list): Sample theta rotation values in degrees
|
|
277
|
+
- phi_result (list): Sample phi rotation values in degrees
|
|
278
|
+
- chi_result (list): Fixed chi values in degrees (all equal to chi_fixed)
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def objective_function(k_cal, k_target):
|
|
282
|
+
"""objective function for gradient decent"""
|
|
283
|
+
return np.linalg.norm(k_cal - k_target)/np.linalg.norm(k_target)
|
|
284
|
+
|
|
285
|
+
def get_k_cal(lab, theta_, phi_, chi_):
|
|
286
|
+
lab.rotate(theta_, phi_, chi_)
|
|
287
|
+
a_star_vec, b_star_vec, c_star_vec = lab.get_reciprocal_space_vectors()
|
|
288
|
+
k_cal = H * a_star_vec + K * b_star_vec + L * c_star_vec
|
|
289
|
+
return k_cal
|
|
290
|
+
|
|
291
|
+
def is_valid_solution(phi):
|
|
292
|
+
if phi is None:
|
|
293
|
+
return False
|
|
294
|
+
if (phi > 90) or (phi < -90):
|
|
295
|
+
return False
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
theta_best = None
|
|
299
|
+
phi_best = None
|
|
300
|
+
_is_valid_solution = False
|
|
301
|
+
|
|
302
|
+
while not _is_valid_solution:
|
|
303
|
+
lab = Lab()
|
|
304
|
+
theta = np.random.uniform(0, 180)
|
|
305
|
+
phi = np.random.uniform(-90, 90)
|
|
306
|
+
|
|
307
|
+
lab.initialize(
|
|
308
|
+
a, b, c, alpha, beta, gamma, roll, pitch, yaw, theta, phi, chi_fixed
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
k_cal = get_k_cal(lab, theta, phi, chi_fixed)
|
|
312
|
+
k_magnitude = np.linalg.norm(k_cal)
|
|
313
|
+
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
314
|
+
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
315
|
+
objective = objective_function(k_cal, k_target)
|
|
316
|
+
for i in range(num_steps):
|
|
317
|
+
step_size = objective * learning_rate
|
|
318
|
+
theta_new = theta + np.random.uniform(-step_size, step_size)
|
|
319
|
+
phi_new = phi + np.random.uniform(-step_size, step_size)
|
|
320
|
+
k_cal = get_k_cal(lab, theta_new, phi_new, chi_fixed)
|
|
321
|
+
objective_new = objective_function(k_cal, k_target)
|
|
322
|
+
if objective_new < objective:
|
|
323
|
+
theta = theta_new
|
|
324
|
+
phi = phi_new
|
|
325
|
+
objective = objective_new
|
|
326
|
+
if objective < target_objective:
|
|
327
|
+
break
|
|
328
|
+
# Normalize angles to (-180, 180] range
|
|
329
|
+
theta = process_angle(theta)
|
|
330
|
+
phi = process_angle(phi)
|
|
331
|
+
|
|
332
|
+
theta_best = theta
|
|
333
|
+
phi_best = phi
|
|
334
|
+
_is_valid_solution = is_valid_solution(phi_best)
|
|
335
|
+
|
|
336
|
+
theta_result = np.round(theta_best, 1)
|
|
337
|
+
phi_result = np.round(phi_best, 1)
|
|
338
|
+
tth_result = np.round(process_angle(tth), 1)
|
|
339
|
+
chi_result = np.round(chi_fixed, 1)
|
|
340
|
+
|
|
341
|
+
return tth_result, theta_result, phi_result, chi_result
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _calculate_angles_phi_fixed(
|
|
345
|
+
k_in,
|
|
346
|
+
H,
|
|
347
|
+
K,
|
|
348
|
+
L,
|
|
349
|
+
a,
|
|
350
|
+
b,
|
|
351
|
+
c,
|
|
352
|
+
alpha,
|
|
353
|
+
beta,
|
|
354
|
+
gamma,
|
|
355
|
+
roll,
|
|
356
|
+
pitch,
|
|
357
|
+
yaw,
|
|
358
|
+
phi_fixed,
|
|
359
|
+
target_objective=1e-7,
|
|
360
|
+
num_steps=3000,
|
|
361
|
+
learning_rate=100,
|
|
362
|
+
):
|
|
363
|
+
"""Calculate scattering angles with phi angle fixed.
|
|
364
|
+
|
|
365
|
+
Uses optimization algorithm to find the theta and chi angles that satisfy the condition
|
|
366
|
+
for the given HKL indices while keeping phi fixed at the specified value. There could be more
|
|
367
|
+
than one solution, so the function returns a list of solutions.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
371
|
+
H, K, L (float): momentum transfer in reciprocal length unit (r.l.u.),
|
|
372
|
+
a, b, c (float): Lattice constants in Angstroms
|
|
373
|
+
alpha, beta, gamma (float): sample rotation angles in degrees
|
|
374
|
+
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
375
|
+
phi_fixed (float): Fixed phi angle in degrees
|
|
376
|
+
target_objective (float, optional): Convergence criterion for optimization. Defaults to 1e-5.
|
|
377
|
+
num_steps (int, optional): Maximum number of optimization steps. Defaults to 1000.
|
|
378
|
+
learning_rate (float, optional): Learning rate for the gradient descent. Defaults to 100.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
tuple: Four lists containing the calculated values:
|
|
382
|
+
- tth_result (list): Scattering angle values in degrees
|
|
383
|
+
- theta_result (list): Sample theta rotation values in degrees
|
|
384
|
+
- phi_result (list): Fixed phi values in degrees (all equal to phi_fixed)
|
|
385
|
+
- chi_result (list): Sample chi rotation values in degrees
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def objective_function(k_cal, k_target):
|
|
389
|
+
"""objective function for gradient decent"""
|
|
390
|
+
return np.linalg.norm(k_cal - k_target)
|
|
391
|
+
|
|
392
|
+
def get_k_cal(lab, theta_, phi_, chi_):
|
|
393
|
+
lab.rotate(theta_, phi_, chi_)
|
|
394
|
+
a_star_vec, b_star_vec, c_star_vec = lab.get_reciprocal_space_vectors()
|
|
395
|
+
k_cal = H * a_star_vec + K * b_star_vec + L * c_star_vec
|
|
396
|
+
return k_cal
|
|
397
|
+
|
|
398
|
+
def is_valid_solution(chi):
|
|
399
|
+
if chi is None:
|
|
400
|
+
return False
|
|
401
|
+
if (chi > 90) or (chi < -90):
|
|
402
|
+
return False
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
theta_best = None
|
|
406
|
+
chi_best = None
|
|
407
|
+
_is_valid_solution = False
|
|
408
|
+
while not _is_valid_solution:
|
|
409
|
+
lab = Lab()
|
|
410
|
+
theta = np.random.uniform(0, 180)
|
|
411
|
+
chi = np.random.uniform(-90, 90)
|
|
412
|
+
|
|
413
|
+
lab.initialize(
|
|
414
|
+
a, b, c, alpha, beta, gamma, roll, pitch, yaw, theta, phi_fixed, chi
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
k_cal = get_k_cal(lab, theta, phi_fixed, chi)
|
|
418
|
+
k_magnitude = np.linalg.norm(k_cal)
|
|
419
|
+
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
420
|
+
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
421
|
+
objective = objective_function(k_cal, k_target)
|
|
422
|
+
for i in range(num_steps):
|
|
423
|
+
step_size = objective * learning_rate
|
|
424
|
+
theta_new = theta + np.random.uniform(-step_size, step_size)
|
|
425
|
+
chi_new = chi + np.random.uniform(-step_size, step_size)
|
|
426
|
+
k_cal = get_k_cal(lab, theta_new, phi_fixed, chi_new)
|
|
427
|
+
objective_new = objective_function(k_cal, k_target)
|
|
428
|
+
if objective_new < objective:
|
|
429
|
+
theta = theta_new
|
|
430
|
+
chi = chi_new
|
|
431
|
+
objective = objective_new
|
|
432
|
+
if objective < target_objective:
|
|
433
|
+
break
|
|
434
|
+
# Normalize angles to (0, 360) range
|
|
435
|
+
theta = process_angle(theta)
|
|
436
|
+
chi = process_angle(chi)
|
|
437
|
+
theta_best = theta
|
|
438
|
+
chi_best = chi
|
|
439
|
+
_is_valid_solution = is_valid_solution(chi_best)
|
|
440
|
+
|
|
441
|
+
# round up to 0.1, discard duplicates, theta and chi should match the order of the list
|
|
442
|
+
theta_result = np.round(theta_best, 1)
|
|
443
|
+
chi_result = np.round(chi_best, 1)
|
|
444
|
+
tth_result = np.round(process_angle(tth), 1)
|
|
445
|
+
phi_result = np.round(phi_fixed, 1)
|
|
446
|
+
return tth_result, theta_result, phi_result, chi_result
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _calculate_hkl(k_in, tth, theta, phi, chi, a_vec_lab, b_vec_lab, c_vec_lab):
|
|
450
|
+
"""Calculate HKL values from scattering angles.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
454
|
+
tth (float): Scattering angle in degrees
|
|
455
|
+
theta (float): Sample theta rotation in degrees
|
|
456
|
+
phi (float): Sample phi rotation in degrees
|
|
457
|
+
chi (float): Sample chi rotation in degrees
|
|
458
|
+
a_vec_lab (np.ndarray): Real space a vector in lab frame
|
|
459
|
+
b_vec_lab (np.ndarray): Real space b vector in lab frame
|
|
460
|
+
c_vec_lab (np.ndarray): Real space c vector in lab frame
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
dict: Dictionary containing calculated values:
|
|
464
|
+
- H, K, L (float): momentum transfer in reciprocal length unit (r.l.u.)
|
|
465
|
+
- tth, theta, phi, chi (float): Input angles in degrees
|
|
466
|
+
- success (bool): Whether calculation was successful
|
|
467
|
+
- error (str or None): Error message if any
|
|
468
|
+
"""
|
|
469
|
+
try:
|
|
470
|
+
# Calculate momentum transfer magnitude
|
|
471
|
+
k_magnitude = 2.0 * k_in * np.sin(np.radians(tth / 2.0))
|
|
472
|
+
|
|
473
|
+
# Calculate delta = theta + 90 - (tth/2)
|
|
474
|
+
delta = 90 -(tth / 2.0)
|
|
475
|
+
sin_delta = np.sin(np.radians(delta))
|
|
476
|
+
cos_delta = np.cos(np.radians(delta))
|
|
477
|
+
|
|
478
|
+
# momentum transfer at theta, phi, chi = 0
|
|
479
|
+
k_vec_initial = np.array(
|
|
480
|
+
[-k_magnitude * sin_delta, -k_magnitude * cos_delta, 0.0]
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# rotation of the beam is the reverse rotation of the sample, thus the transpose
|
|
484
|
+
rotation_matrix = angle_to_matrix(theta, phi, chi).T
|
|
485
|
+
|
|
486
|
+
# momentum transfer at non-zero theta, phi, chi
|
|
487
|
+
k_vec_lab = rotation_matrix @ k_vec_initial
|
|
488
|
+
|
|
489
|
+
# calculate HKL
|
|
490
|
+
H = np.dot(k_vec_lab, a_vec_lab) / (2 * np.pi)
|
|
491
|
+
K = np.dot(k_vec_lab, b_vec_lab) / (2 * np.pi)
|
|
492
|
+
L = np.dot(k_vec_lab, c_vec_lab) / (2 * np.pi)
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
"H": H,
|
|
496
|
+
"K": K,
|
|
497
|
+
"L": L,
|
|
498
|
+
"tth": tth,
|
|
499
|
+
"theta": theta,
|
|
500
|
+
"phi": phi,
|
|
501
|
+
"chi": chi,
|
|
502
|
+
"success": True,
|
|
503
|
+
"error": None,
|
|
504
|
+
}
|
|
505
|
+
except Exception as e:
|
|
506
|
+
return {
|
|
507
|
+
"H": None,
|
|
508
|
+
"K": None,
|
|
509
|
+
"L": None,
|
|
510
|
+
"tth": tth,
|
|
511
|
+
"theta": theta,
|
|
512
|
+
"phi": phi,
|
|
513
|
+
"chi": chi,
|
|
514
|
+
"success": False,
|
|
515
|
+
"error": str(e),
|
|
516
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .hkl_scan_components import (
|
|
2
|
+
HKLScanControls,
|
|
3
|
+
HKLScanResultsTable,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from .hk_angles_components import (
|
|
7
|
+
HKAnglesControls,
|
|
8
|
+
HKAnglesResultsWidget,
|
|
9
|
+
)
|
|
10
|
+
from .angles_to_hkl_components import (
|
|
11
|
+
AnglesToHKLControls,
|
|
12
|
+
AnglesToHKLResults,
|
|
13
|
+
)
|
|
14
|
+
from .hkl_to_angles_components import (
|
|
15
|
+
HKLToAnglesControls,
|
|
16
|
+
HKLToAnglesResultsWidget,
|
|
17
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# pylint: disable=no-name-in-module, import-error
|
|
4
|
+
from PyQt5.QtWidgets import (
|
|
5
|
+
QWidget,
|
|
6
|
+
QVBoxLayout,
|
|
7
|
+
QFormLayout,
|
|
8
|
+
QGroupBox,
|
|
9
|
+
QLabel,
|
|
10
|
+
QPushButton,
|
|
11
|
+
QDoubleSpinBox,
|
|
12
|
+
QLineEdit,
|
|
13
|
+
)
|
|
14
|
+
from PyQt5.QtCore import pyqtSignal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnglesToHKLControls(QWidget):
|
|
18
|
+
"""Widget for Angles to HKL calculation controls."""
|
|
19
|
+
|
|
20
|
+
# Signal emitted when calculate button is clicked
|
|
21
|
+
calculateClicked = pyqtSignal()
|
|
22
|
+
# Signal emitted when any angle value changes
|
|
23
|
+
anglesChanged = pyqtSignal()
|
|
24
|
+
|
|
25
|
+
def __init__(self, parent=None):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
|
|
28
|
+
# Main layout
|
|
29
|
+
main_layout = QVBoxLayout(self)
|
|
30
|
+
|
|
31
|
+
# Input form
|
|
32
|
+
form_group = QGroupBox("Scattering Angles")
|
|
33
|
+
form_layout = QFormLayout(form_group)
|
|
34
|
+
|
|
35
|
+
# tth input
|
|
36
|
+
self.tth_input = QDoubleSpinBox()
|
|
37
|
+
self.tth_input.setRange(0.0, 180.0)
|
|
38
|
+
self.tth_input.setValue(150.0)
|
|
39
|
+
self.tth_input.setSuffix(" °")
|
|
40
|
+
self.tth_input.valueChanged.connect(self.anglesChanged.emit)
|
|
41
|
+
form_layout.addRow("tth:", self.tth_input)
|
|
42
|
+
|
|
43
|
+
# theta input
|
|
44
|
+
self.theta_input = QDoubleSpinBox()
|
|
45
|
+
self.theta_input.setRange(-180.0, 180.0)
|
|
46
|
+
self.theta_input.setValue(50.0)
|
|
47
|
+
self.theta_input.setSuffix(" °")
|
|
48
|
+
self.theta_input.valueChanged.connect(self.anglesChanged.emit)
|
|
49
|
+
form_layout.addRow("θ:", self.theta_input)
|
|
50
|
+
|
|
51
|
+
# phi input
|
|
52
|
+
self.phi_input = QDoubleSpinBox()
|
|
53
|
+
self.phi_input.setRange(-180.0, 180.0)
|
|
54
|
+
self.phi_input.setValue(0.0)
|
|
55
|
+
self.phi_input.setSuffix(" °")
|
|
56
|
+
self.phi_input.valueChanged.connect(self.anglesChanged.emit)
|
|
57
|
+
form_layout.addRow("φ:", self.phi_input)
|
|
58
|
+
|
|
59
|
+
# chi input
|
|
60
|
+
self.chi_input = QDoubleSpinBox()
|
|
61
|
+
self.chi_input.setRange(-180.0, 180.0)
|
|
62
|
+
self.chi_input.setValue(0.0)
|
|
63
|
+
self.chi_input.setSuffix(" °")
|
|
64
|
+
self.chi_input.valueChanged.connect(self.anglesChanged.emit)
|
|
65
|
+
form_layout.addRow("χ:", self.chi_input)
|
|
66
|
+
|
|
67
|
+
main_layout.addWidget(form_group)
|
|
68
|
+
|
|
69
|
+
# Calculate button
|
|
70
|
+
self.calculate_button = QPushButton("Calculate HKL")
|
|
71
|
+
self.calculate_button.clicked.connect(self.calculateClicked.emit)
|
|
72
|
+
self.calculate_button.setObjectName("calculateHKLButton")
|
|
73
|
+
main_layout.addWidget(self.calculate_button)
|
|
74
|
+
|
|
75
|
+
def get_calculation_parameters(self):
|
|
76
|
+
"""Get parameters for HKL calculation."""
|
|
77
|
+
return {
|
|
78
|
+
"tth": self.tth_input.value(),
|
|
79
|
+
"theta": self.theta_input.value(),
|
|
80
|
+
"phi": self.phi_input.value(),
|
|
81
|
+
"chi": self.chi_input.value(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def set_values(self, tth=None, theta=None, phi=None, chi=None):
|
|
85
|
+
"""Set input values programmatically."""
|
|
86
|
+
if tth is not None:
|
|
87
|
+
self.tth_input.setValue(tth)
|
|
88
|
+
if theta is not None:
|
|
89
|
+
self.theta_input.setValue(theta)
|
|
90
|
+
if phi is not None:
|
|
91
|
+
self.phi_input.setValue(phi)
|
|
92
|
+
if chi is not None:
|
|
93
|
+
self.chi_input.setValue(chi)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AnglesToHKLResults(QWidget):
|
|
97
|
+
"""Widget for displaying Angles to HKL calculation results."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, parent=None):
|
|
100
|
+
super().__init__(parent)
|
|
101
|
+
|
|
102
|
+
# Main layout
|
|
103
|
+
main_layout = QVBoxLayout(self)
|
|
104
|
+
|
|
105
|
+
# Results group
|
|
106
|
+
results_group = QGroupBox("Results")
|
|
107
|
+
results_layout = QFormLayout(results_group)
|
|
108
|
+
|
|
109
|
+
# H result
|
|
110
|
+
self.H_result = QLineEdit()
|
|
111
|
+
self.H_result.setReadOnly(True)
|
|
112
|
+
results_layout.addRow("H:", self.H_result)
|
|
113
|
+
|
|
114
|
+
# K result
|
|
115
|
+
self.K_result = QLineEdit()
|
|
116
|
+
self.K_result.setReadOnly(True)
|
|
117
|
+
results_layout.addRow("K:", self.K_result)
|
|
118
|
+
|
|
119
|
+
# L result
|
|
120
|
+
self.L_result = QLineEdit()
|
|
121
|
+
self.L_result.setReadOnly(True)
|
|
122
|
+
results_layout.addRow("L:", self.L_result)
|
|
123
|
+
|
|
124
|
+
main_layout.addWidget(results_group)
|
|
125
|
+
|
|
126
|
+
def display_results(self, results):
|
|
127
|
+
"""Display calculation results."""
|
|
128
|
+
if results and results.get("success", False):
|
|
129
|
+
self.H_result.setText(f"{results['H']:.4f}")
|
|
130
|
+
self.K_result.setText(f"{results['K']:.4f}")
|
|
131
|
+
self.L_result.setText(f"{results['L']:.4f}")
|
|
132
|
+
else:
|
|
133
|
+
self.clear_results()
|
|
134
|
+
|
|
135
|
+
def clear_results(self):
|
|
136
|
+
"""Clear all results."""
|
|
137
|
+
self.H_result.clear()
|
|
138
|
+
self.K_result.clear()
|
|
139
|
+
self.L_result.clear()
|
|
140
|
+
|
|
141
|
+
def get_results(self):
|
|
142
|
+
"""Get current results as a dictionary."""
|
|
143
|
+
try:
|
|
144
|
+
return {
|
|
145
|
+
"H": float(self.H_result.text()) if self.H_result.text() else None,
|
|
146
|
+
"K": float(self.K_result.text()) if self.K_result.text() else None,
|
|
147
|
+
"L": float(self.L_result.text()) if self.L_result.text() else None,
|
|
148
|
+
}
|
|
149
|
+
except ValueError:
|
|
150
|
+
return {"H": None, "K": None, "L": None}
|