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,203 @@
1
+ """This module provides a class for visualizing the coordinate system in a 3D space. More
2
+ specifically, it visualizes the relative position of of crystal coordinates with respect to the lab
3
+ coordinate system.
4
+ """
5
+ import numpy as np
6
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
7
+ from matplotlib.figure import Figure
8
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection
9
+ from advisor.domain.core import Lab
10
+
11
+ class CoordinateVisualizer(FigureCanvas):
12
+ """Visualizer for coordinate system with 3D interactive canvas."""
13
+
14
+ def __init__(self, width=4, height=4, dpi=100):
15
+ """Initialize the visualizer with a 3D canvas."""
16
+ self.fig = Figure(figsize=(width, height), dpi=dpi)
17
+ self.axes = self.fig.add_subplot(111, projection="3d")
18
+ self.axes.set_axis_off()
19
+ super().__init__(self.fig)
20
+
21
+
22
+ # Set background color to white
23
+ self.fig.patch.set_facecolor("white")
24
+ self.axes.set_facecolor("white")
25
+
26
+ # Set initial view (adjusted for x-y scattering plane, beam from -y)
27
+ self.axes.view_init(elev=20, azim=30, roll=0)
28
+
29
+
30
+ # Set initial limits
31
+ self.axes.set_xlim(-0.75, 0.75)
32
+ self.axes.set_ylim(-0.75, 0.75)
33
+ self.axes.set_zlim(-0.75, 0.75)
34
+
35
+ # Initialize reciprocal lattice vectors in lab frame
36
+ self.a_star_lab = np.array([1, 0, 0])
37
+ self.b_star_lab = np.array([0, 1, 0])
38
+ self.c_star_lab = np.array([0, 0, 1])
39
+ self.roll = 0
40
+ self.pitch = 0
41
+ self.yaw = 0
42
+
43
+ def initialize(self, params: dict):
44
+ """Initialize the visualizer with the given parameters."""
45
+ self.roll = params["roll"]
46
+ self.pitch = params["pitch"]
47
+ self.yaw = params["yaw"]
48
+ a, b, c = params["a"], params["b"], params["c"]
49
+ alpha, beta, gamma = params["alpha"], params["beta"], params["gamma"]
50
+
51
+ lab = Lab()
52
+ lab.initialize(
53
+ a, b, c, alpha, beta, gamma, self.roll, self.pitch, self.yaw, 0, 0, 0
54
+ )
55
+ # calculate the corresponding a_star_lab, b_star_lab, c_star_lab
56
+ self.a_star_lab, self.b_star_lab, self.c_star_lab = (
57
+ lab.get_reciprocal_space_vectors()
58
+ )
59
+ return True
60
+
61
+ def visualize_lab_system(self):
62
+ """Update the visualization with new crystal coordinates system."""
63
+ # Clear previous plot
64
+ self.axes.clear()
65
+
66
+ # Plot the scattering plane (x-y plane, z=0), adjusted for beam from -y
67
+ scatter_plane_vertices = np.array(
68
+ [
69
+ [1.25, -1.25, 0], # bottom right
70
+ [-0.25, -1.25, 0], # bottom left
71
+ [1.25, 1.25, 0], # top right
72
+ [-0.25, 1.25, 0], # top left
73
+ ]
74
+ )
75
+
76
+ scatter_plane_faces = np.array([[0, 1, 3, 2]]) # single face
77
+ self.axes.add_collection3d(
78
+ Poly3DCollection(
79
+ scatter_plane_vertices[scatter_plane_faces],
80
+ facecolors=[0.3010, 0.7450, 0.9330], # light blue
81
+ edgecolors=[0.7, 0.7, 0.7],
82
+ alpha=0.15,
83
+ )
84
+ )
85
+
86
+ # Define vertices of the cube (standing perpendicular to scattering plane)
87
+ # Rotated 90° so thin dimension (0.25) is along x-axis, facing beam from -y
88
+ ver = np.array(
89
+ [
90
+ [0.125, 0.25, 0.5], # top front right
91
+ [0.125, -0.25, 0.5], # top front left
92
+ [-0.125, -0.25, 0.5], # top back left
93
+ [-0.125, 0.25, 0.5], # top back right
94
+ [0.125, 0.25, -0.5], # bottom front right
95
+ [0.125, -0.25, -0.5], # bottom front left
96
+ [-0.125, -0.25, -0.5], # bottom back left
97
+ [-0.125, 0.25, -0.5], # bottom back right
98
+ ]
99
+ )
100
+
101
+ # Define faces of the cube
102
+ fac = np.array(
103
+ [
104
+ [0, 1, 2, 3], # top face
105
+ [4, 5, 6, 7], # bottom face
106
+ [0, 1, 5, 4], # front face
107
+ [2, 3, 7, 6], # back face
108
+ [0, 3, 7, 4], # right face
109
+ [1, 2, 6, 5], # left face
110
+ ]
111
+ )
112
+
113
+ # Plot the cube
114
+ self.axes.add_collection3d(
115
+ Poly3DCollection(
116
+ ver[fac],
117
+ facecolors=[0.3, 0.3, 0.3],
118
+ edgecolors=[0.55, 0.55, 0.55],
119
+ alpha=0.2,
120
+ )
121
+ )
122
+
123
+ # Normalize the vectors
124
+ a_star_norm = self.a_star_lab / np.linalg.norm(self.a_star_lab)
125
+ b_star_norm = self.b_star_lab / np.linalg.norm(self.b_star_lab)
126
+ c_star_norm = self.c_star_lab / np.linalg.norm(self.c_star_lab)
127
+
128
+ # Plot the normalized vectors
129
+ vectors = [a_star_norm, b_star_norm, c_star_norm]
130
+ colors = ["r", "r", "r"]
131
+ labels = ["$a^*$", "$b^*$", "$c^*$"]
132
+
133
+ for vec, color, label in zip(vectors, colors, labels):
134
+ # Plot the vector
135
+ self.axes.quiver(
136
+ 0,
137
+ 0,
138
+ 0, # origin
139
+ vec[0],
140
+ vec[1],
141
+ vec[2], # vector components
142
+ color=color,
143
+ alpha=1,
144
+ linewidth=2,
145
+ arrow_length_ratio=0.2,
146
+ )
147
+
148
+ # Add text label at the tip of the vector
149
+ # Add a small offset to prevent text from overlapping with the arrow
150
+ offset = 0.2
151
+ self.axes.text(
152
+ vec[0] + offset,
153
+ vec[1] + offset,
154
+ vec[2] + offset,
155
+ label,
156
+ color=color,
157
+ fontsize=14,
158
+ ha="center",
159
+ )
160
+
161
+ # plot the vector of the lab coordinate system, by default it is the unit vectors
162
+ e_X = np.array([1, 0, 0]) / 0.65
163
+ e_Y = np.array([0, 1, 0]) / 0.65
164
+ e_Z = np.array([0, 0, 1]) / 0.65
165
+ vectors = [e_X, e_Y, e_Z]
166
+ colors = ["r", "g", "b"]
167
+ labels = ["$X$", "$Y$", "$Z$"]
168
+
169
+ for vec, color, label in zip(vectors, colors, labels):
170
+ # Plot the vector
171
+ self.axes.quiver(
172
+ 0,
173
+ 0,
174
+ 0, # origin
175
+ vec[0],
176
+ vec[1],
177
+ vec[2], # vector components
178
+ color=(64 / 255, 148 / 255, 184 / 255),
179
+ alpha=1,
180
+ linewidth=0.8,
181
+ arrow_length_ratio=0.1,
182
+ )
183
+
184
+ # Add text label at the tip of the vector
185
+ # Add a small offset to prevent text from overlapping with the arrow
186
+ offset = 0.2
187
+ self.axes.text(
188
+ vec[0] + offset,
189
+ vec[1] + offset,
190
+ vec[2] + offset,
191
+ label,
192
+ color=(64 / 255, 148 / 255, 184 / 255),
193
+ )
194
+
195
+ # Set axis limits
196
+ self.axes.set_xlim(-1, 1)
197
+ self.axes.set_ylim(-1, 1)
198
+ self.axes.set_zlim(-1, 1)
199
+
200
+ self.axes.set_axis_off()
201
+ self.fig.tight_layout()
202
+ # Update the canvas
203
+ self.draw()
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """This is a class to visualize the X-ray scattering geometry."""
5
+ import numpy as np
6
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
7
+ from matplotlib.figure import Figure
8
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection
9
+ from advisor.domain import (
10
+ get_reciprocal_space_vectors,
11
+ get_rotation,
12
+ )
13
+ from advisor.domain.core import Lab
14
+
15
+ class ScatteringVisualizer(FigureCanvas):
16
+ """Visualizer for scattering geometry with 3D interactive canvas."""
17
+
18
+ def __init__(self, width=4, height=4, dpi=100):
19
+ """Initialize the visualizer with a 3D canvas."""
20
+ self.fig = Figure(figsize=(width, height), dpi=dpi)
21
+ self.axes = self.fig.add_subplot(111, projection="3d")
22
+ super().__init__(self.fig)
23
+
24
+ # Set initial view (adjusted for x-y scattering plane, beam from -y)
25
+ self.axes.view_init(elev=20, azim=30, roll=0)
26
+
27
+ # Initialize reciprocal lattice vectors in lab frame
28
+ self.a_star_lab = np.array([1, 0, 0])
29
+ self.b_star_lab = np.array([0, 1, 0])
30
+ self.c_star_lab = np.array([0, 0, 1])
31
+ self.roll = 0
32
+ self.pitch = 0
33
+ self.yaw = 0
34
+
35
+ def initialize(self, params: dict):
36
+ """Initialize the visualizer with the given crystal coordinates system."""
37
+ self.roll = params["roll"]
38
+ self.pitch = params["pitch"]
39
+ self.yaw = params["yaw"]
40
+ a, b, c = params["a"], params["b"], params["c"]
41
+ alpha, beta, gamma = params["alpha"], params["beta"], params["gamma"]
42
+ lab = Lab()
43
+ lab.initialize(
44
+ a, b, c, alpha, beta, gamma, self.roll, self.pitch, self.yaw, 0, 0, 0
45
+ )
46
+ # calculate the corresponding a_star_lab, b_star_lab, c_star_lab
47
+ self.a_star_lab, self.b_star_lab, self.c_star_lab = (
48
+ lab.get_reciprocal_space_vectors(is_normalized=True)
49
+ )
50
+ self.a_lab, self.b_lab, self.c_lab = lab.get_real_space_vectors(is_normalized=True)
51
+ self.visualize_lab_system()
52
+ return True
53
+
54
+ def visualize_lab_system(self, chi=0, phi=0, plot_k_basis=True, plot_basis = False, is_clear=True):
55
+ """Update the visualization with new crystal coordinates system."""
56
+ if is_clear:
57
+ # Clear previous plot
58
+ self.axes.clear()
59
+
60
+ # Define vertices of the sample (standing perpendicular to scattering plane)
61
+ # Rotated 90° so thin dimension (0.25) is along x-axis, facing beam from -y
62
+ vertices_sample = np.array(
63
+ [
64
+ [0.125, 0.25, 0.5], # top front right
65
+ [0.125, -0.25, 0.5], # top front left
66
+ [-0.125, -0.25, 0.5], # top back left
67
+ [-0.125, 0.25, 0.5], # top back right
68
+ [0.125, 0.25, -0.5], # bottom front right
69
+ [0.125, -0.25, -0.5], # bottom front left
70
+ [-0.125, -0.25, -0.5], # bottom back left
71
+ [-0.125, 0.25, -0.5], # bottom back right
72
+ ]
73
+ )
74
+ vertices_sample = _rotate_vertices(vertices_sample, phi, chi)
75
+ # Define faces of the cube
76
+ faces_sample = np.array(
77
+ [
78
+ [0, 1, 2, 3], # top face
79
+ [4, 5, 6, 7], # bottom face
80
+ [0, 1, 5, 4], # front face
81
+ [2, 3, 7, 6], # back face
82
+ [0, 3, 7, 4], # right face
83
+ [1, 2, 6, 5], # left face
84
+ ]
85
+ )
86
+
87
+ # Plot the cube
88
+ self.axes.add_collection3d(
89
+ Poly3DCollection(
90
+ vertices_sample[faces_sample],
91
+ facecolors=[0.3, 0.3, 0.3],
92
+ edgecolors=[0.55, 0.55, 0.55, 0.2],
93
+ alpha=0.05,
94
+ )
95
+ )
96
+
97
+ # add extra color to the top face
98
+
99
+ # add extra color to the top face
100
+ self.axes.add_collection3d(
101
+ Poly3DCollection(
102
+ [vertices_sample[faces_sample[0]]],
103
+ facecolors=[0.3, 0.3, 0.3],
104
+ edgecolors=[0.55, 0.55, 0.55, 0.1],
105
+ alpha=0.25,
106
+ )
107
+ )
108
+
109
+ a_star_norm = self.a_star_lab if plot_k_basis else [0, 0, 0]
110
+ a_star_label = "$a^*$" if plot_k_basis else None
111
+ b_star_norm = self.b_star_lab if plot_k_basis else [0, 0, 0]
112
+ b_star_label = "$b^*$" if plot_k_basis else None
113
+ c_star_norm = self.c_star_lab if plot_k_basis else [0, 0, 0]
114
+ c_star_label = "$c^*$" if plot_k_basis else None
115
+ a_norm = self.a_lab if plot_basis else [0, 0, 0]
116
+ a_label = "$a$" if plot_basis else None
117
+ b_norm = self.b_lab if plot_basis else [0, 0, 0]
118
+ b_label = "$b$" if plot_basis else None
119
+ c_norm = self.c_lab if plot_basis else [0, 0, 0]
120
+ c_label = "$c$" if plot_basis else None
121
+
122
+ # Plot the normalized vectors
123
+ vectors = [a_star_norm, b_star_norm, c_star_norm, a_norm, b_norm, c_norm]
124
+ vectors = _rotate_vertices(vectors, phi, chi)
125
+ colors = ["tomato", "tomato", "tomato", "dodgerblue", "dodgerblue", "dodgerblue"]
126
+ labels = [a_star_label, b_star_label, c_star_label, a_label, b_label, c_label]
127
+
128
+ for vec, color, label in zip(vectors, colors, labels):
129
+ # Plot the vector
130
+ self.axes.quiver(
131
+ 0,
132
+ 0,
133
+ 0, # origin
134
+ vec[0],
135
+ vec[1],
136
+ vec[2], # vector components
137
+ color=color,
138
+ alpha=0.25,
139
+ linewidth=2,
140
+ arrow_length_ratio=0.2,
141
+ )
142
+
143
+ # Add text label at the tip of the vector
144
+ # Add a small offset to prevent text from overlapping with the arrow
145
+ offset = 0.1
146
+ self.axes.text(
147
+ vec[0] + offset+0.1,
148
+ vec[1] + offset,
149
+ vec[2] + offset,
150
+ label,
151
+ color=color,
152
+ fontsize=14,
153
+ ha="center",
154
+ alpha=0.4,
155
+ )
156
+
157
+ # plot the vector of the lab coordinate system, by default it is the unit vectors
158
+ e_X = np.array([1, 0, 0]) / 0.65
159
+ e_Y = np.array([0, 1, 0]) / 0.65
160
+ e_Z = np.array([0, 0, 1]) / 0.65
161
+ vectors = [e_X, e_Y, e_Z]
162
+ #vectors = _rotate_vertices(vectors, phi, chi)
163
+ colors = ["r", "g", "b"]
164
+ labels = ["$X$", "$Y$", "$Z$"]
165
+
166
+ for vec, color, label in zip(vectors, colors, labels):
167
+ # Plot the vector
168
+ self.axes.quiver(
169
+ 0,
170
+ 0,
171
+ 0, # origin
172
+ vec[0],
173
+ vec[1],
174
+ vec[2], # vector components
175
+ color=(64 / 255, 148 / 255, 184 / 255),
176
+ alpha=0.4,
177
+ linewidth=0.8,
178
+ arrow_length_ratio=0.1,
179
+ )
180
+
181
+ # Add text label at the tip of the vector
182
+ # Add a small offset to prevent text from overlapping with the arrow
183
+ offset = 0.2
184
+ self.axes.text(
185
+ vec[0] + offset,
186
+ vec[1] + offset,
187
+ vec[2] + offset,
188
+ label,
189
+ color=(64 / 255, 148 / 255, 184 / 255),
190
+ alpha=0.4,
191
+ )
192
+
193
+ # Update the canvas
194
+ # Set axis limits
195
+ self.axes.set_xlim(-1, 1)
196
+ self.axes.set_ylim(-1, 1)
197
+ self.axes.set_zlim(-1, 1)
198
+
199
+ self.axes.set_axis_off()
200
+ self.fig.tight_layout()
201
+ self.draw()
202
+
203
+ def visualize_scattering_geometry(self, scattering_angles=None, is_clear=False):
204
+ """Update the visualization with new scattering angles."""
205
+ if is_clear:
206
+ # Clear previous plot
207
+ self.axes.clear()
208
+
209
+ # Plot the scattering plane (x-y plane, z=0), adjusted for beam from -y
210
+ scatter_plane_vertices = np.array(
211
+ [
212
+ [1.25, -1.25, 0], # bottom right
213
+ [-0.25, -1.25, 0], # bottom left
214
+ [1.25, 1.25, 0], # top right
215
+ [-0.25, 1.25, 0], # top left
216
+ ]
217
+ )
218
+
219
+ scatter_plane_faces = np.array([[0, 1, 3, 2]]) # single face
220
+ self.axes.add_collection3d(
221
+ Poly3DCollection(
222
+ scatter_plane_vertices[scatter_plane_faces],
223
+ facecolors=[0.3510, 0.7850, 0.9330], # light blue
224
+ edgecolors=[0.7, 0.7, 0.7],
225
+ alpha=0.3,
226
+ )
227
+ )
228
+
229
+ # Plot the x-ray beam
230
+ if scattering_angles is None:
231
+ scattering_angles = {
232
+ "theta": 50,
233
+ "tth": 150,
234
+ }
235
+
236
+ # Extract angles from data
237
+ theta = scattering_angles.get("theta", 50) # theta angle
238
+ tth = scattering_angles.get("tth", 150) # two theta angle
239
+
240
+ # Plot incident beam (k_in) - coming from -y direction (x-y plane)
241
+ offset = 0
242
+ k_in_length = 1.3
243
+ # Rotated 90° so theta=0 means beam comes from -y direction
244
+ k_in_x = -k_in_length * np.sin(np.radians(theta))
245
+ k_in_y = -k_in_length * np.cos(np.radians(theta))
246
+ k_in_z = 0
247
+ # Draw colored arrow on top
248
+ self.axes.quiver(
249
+ -k_in_x,
250
+ -k_in_y + offset,
251
+ -k_in_z,
252
+ k_in_x,
253
+ k_in_y,
254
+ k_in_z,
255
+ color=(191 / 255, 44 / 255, 0),
256
+ alpha=1,
257
+ linewidth=5,
258
+ arrow_length_ratio=0.2,
259
+ zorder=10,
260
+ )
261
+
262
+ # Plot scattered beam (k_out) - in x-y plane, rotated 90° from original
263
+ k_out_length = 1.3
264
+ # Rotated 90° to match incident beam coming from -y
265
+ k_out_x = k_out_length * np.sin(np.radians(tth - theta))
266
+ k_out_y = -k_out_length * np.cos(np.radians(tth - theta))
267
+ k_out_z = 0
268
+
269
+ # Draw colored arrow on top
270
+ self.axes.quiver(
271
+ 0,
272
+ 0 + offset,
273
+ 0,
274
+ k_out_x,
275
+ k_out_y,
276
+ k_out_z,
277
+ color=(2 / 255, 78 / 255, 191 / 255),
278
+ linewidth=5,
279
+ arrow_length_ratio=0.2,
280
+ zorder=10,
281
+ )
282
+
283
+ # Set axis limits
284
+ self.axes.set_xlim(-1, 1)
285
+ self.axes.set_ylim(-1, 1)
286
+ self.axes.set_zlim(-1, 1)
287
+
288
+ self.axes.set_axis_off()
289
+ self.fig.tight_layout()
290
+ # Update the canvas
291
+ self.draw()
292
+
293
+
294
+
295
+ def _rotate_vertices(vertices, phi, chi):
296
+ """Rotate the vertices of the sample with respect to the scattering plane by the given phi and chi angles."""
297
+ rotation_matrix = get_rotation(phi, chi)
298
+ vertices = np.array(vertices)
299
+ for i, vertex in enumerate(vertices):
300
+ vertices[i] = rotation_matrix @ vertex
301
+ return vertices