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.
Files changed (69) hide show
  1. advisor/__init__.py +3 -0
  2. advisor/__main__.py +7 -0
  3. advisor/app.py +40 -0
  4. advisor/controllers/__init__.py +6 -0
  5. advisor/controllers/app_controller.py +69 -0
  6. advisor/controllers/feature_controller.py +25 -0
  7. advisor/domain/__init__.py +23 -0
  8. advisor/domain/core/__init__.py +8 -0
  9. advisor/domain/core/lab.py +121 -0
  10. advisor/domain/core/lattice.py +79 -0
  11. advisor/domain/core/sample.py +101 -0
  12. advisor/domain/geometry.py +212 -0
  13. advisor/domain/unit_converter.py +82 -0
  14. advisor/features/__init__.py +6 -0
  15. advisor/features/scattering_geometry/controllers/__init__.py +5 -0
  16. advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
  17. advisor/features/scattering_geometry/domain/__init__.py +5 -0
  18. advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
  19. advisor/features/scattering_geometry/domain/core.py +516 -0
  20. advisor/features/scattering_geometry/ui/__init__.py +5 -0
  21. advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
  22. advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
  23. advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
  24. advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
  25. advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
  26. advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
  27. advisor/features/structure_factor/controllers/__init__.py +6 -0
  28. advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
  29. advisor/features/structure_factor/domain/__init__.py +6 -0
  30. advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
  31. advisor/features/structure_factor/ui/__init__.py +6 -0
  32. advisor/features/structure_factor/ui/components/__init__.py +12 -0
  33. advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
  34. advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
  35. advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
  36. advisor/resources/__init__.py +0 -0
  37. advisor/resources/config/app_config.json +14 -0
  38. advisor/resources/config/tips.json +4 -0
  39. advisor/resources/data/nacl.cif +111 -0
  40. advisor/resources/icons/bz_caculator.jpg +0 -0
  41. advisor/resources/icons/bz_calculator.png +0 -0
  42. advisor/resources/icons/minus.svg +3 -0
  43. advisor/resources/icons/placeholder.png +0 -0
  44. advisor/resources/icons/plus.svg +3 -0
  45. advisor/resources/icons/reset.png +0 -0
  46. advisor/resources/icons/sf_calculator.jpg +0 -0
  47. advisor/resources/icons/sf_calculator.png +0 -0
  48. advisor/resources/icons.qrc +6 -0
  49. advisor/resources/qss/styles.qss +348 -0
  50. advisor/resources/resources_rc.py +83 -0
  51. advisor/ui/__init__.py +7 -0
  52. advisor/ui/init_window.py +566 -0
  53. advisor/ui/main_window.py +174 -0
  54. advisor/ui/tab_interface.py +44 -0
  55. advisor/ui/tips.py +30 -0
  56. advisor/ui/utils/__init__.py +6 -0
  57. advisor/ui/utils/readcif.py +129 -0
  58. advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
  59. advisor/ui/visualizers/__init__.py +8 -0
  60. advisor/ui/visualizers/coordinate_visualizer.py +203 -0
  61. advisor/ui/visualizers/scattering_visualizer.py +301 -0
  62. advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
  63. advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
  64. advisor/ui/visualizers/unitcell_visualizer.py +518 -0
  65. advisor_scattering-0.5.0.dist-info/METADATA +122 -0
  66. advisor_scattering-0.5.0.dist-info/RECORD +69 -0
  67. advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
  68. advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
  69. 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,5 @@
1
+ """UI components for the scattering geometry feature."""
2
+
3
+ from .scattering_geometry_tab import ScatteringGeometryTab
4
+
5
+ __all__ = ["ScatteringGeometryTab"]
@@ -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}