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,6 @@
1
+ """Controllers for Structure Factor feature."""
2
+
3
+ from .structure_factor_controller import StructureFactorController
4
+
5
+ __all__ = ["StructureFactorController"]
6
+
@@ -0,0 +1,25 @@
1
+ """Controller for the Structure Factor feature."""
2
+
3
+ from advisor.controllers.feature_controller import FeatureController
4
+ from advisor.features.structure_factor.domain import StructureFactorCalculator
5
+ from advisor.features.structure_factor.ui.structure_factor_tab import StructureFactorTab
6
+
7
+
8
+ class StructureFactorController(FeatureController):
9
+ """Manages structure factor calculations."""
10
+
11
+ title = "Structure Factor"
12
+ description = "Calculate structure factors from CIF files using Dans_Diffraction."
13
+ icon = "sf_calculator.png"
14
+
15
+ def __init__(self, app_controller):
16
+ super().__init__(app_controller)
17
+ self.calculator = StructureFactorCalculator()
18
+ self.view = self.build_view()
19
+
20
+ def build_view(self):
21
+ return StructureFactorTab(controller=self, calculator=self.calculator)
22
+
23
+ def set_parameters(self, params: dict):
24
+ if self.view and hasattr(self.view, "set_parameters"):
25
+ self.view.set_parameters(params)
@@ -0,0 +1,6 @@
1
+ """Domain logic for structure factor feature."""
2
+
3
+ from .structure_factor_calculator import StructureFactorCalculator
4
+
5
+ __all__ = ["StructureFactorCalculator"]
6
+
@@ -0,0 +1,107 @@
1
+ """This is a module responsible for calculating the structure factor of a given crystal
2
+ structure.
3
+
4
+ CONVENTION:
5
+ - energy input is in eV
6
+
7
+ """
8
+
9
+ import Dans_Diffraction as dif
10
+ import numpy as np
11
+ import os
12
+
13
+ DEFAULT_HKL_LIST = [
14
+ [1, 0, 0],
15
+ [0, 1, 0],
16
+ [0, 0, 1],
17
+ [1, 1, 0],
18
+ [1, 0, 1],
19
+ [0, 1, 1],
20
+ [1, 1, 1],
21
+ [2, 0, 0],
22
+ [0, 2, 0],
23
+ [0, 0, 2],
24
+ [2, 1, 0],
25
+ [1, 2, 0],
26
+ [2, 2, 0],
27
+ ]
28
+ SCATTERING_TYPE = "xray dispersion"
29
+ METHOD = "x-ray"
30
+
31
+
32
+ class StructureFactorCalculator:
33
+ def __init__(self):
34
+ self.calculator = None
35
+ self._cif_file_path = None
36
+ self._energy = None # in eV
37
+ self._is_initialized = False
38
+
39
+ @property
40
+ def cif_file_path(self):
41
+ return self._cif_file_path
42
+
43
+ @cif_file_path.setter
44
+ def cif_file_path(self, cif_file_path: str):
45
+ # check if the file exists
46
+ if not os.path.exists(cif_file_path):
47
+ raise FileNotFoundError(f"File {cif_file_path} does not exist.")
48
+ self._cif_file_path = cif_file_path
49
+
50
+ @property
51
+ def is_initialized(self):
52
+ return self._is_initialized
53
+
54
+ @is_initialized.setter
55
+ def is_initialized(self, is_initialized: bool):
56
+ self._is_initialized = is_initialized
57
+
58
+ @property
59
+ def energy(self):
60
+ """Energy in eV"""
61
+ return self._energy
62
+
63
+ @energy.setter
64
+ def energy(self, energy: float):
65
+ """Energy in eV"""
66
+ self._energy = energy
67
+ if self.is_initialized:
68
+ self.calculator.Scatter.setup_scatter(
69
+ scattering_type=METHOD, energy_kev=self.energy / 1000
70
+ )
71
+
72
+ def initialize(self, cif_file_path: str, energy: float):
73
+ self.cif_file_path = cif_file_path
74
+ self.energy = energy
75
+ self.calculator = dif.Crystal(self.cif_file_path)
76
+ self.calculator.Scatter.setup_scatter(
77
+ scattering_type=METHOD, energy_kev=self.energy / 1000
78
+ )
79
+ self.is_initialized = True
80
+
81
+ def calculate_structure_factors(
82
+ self,
83
+ hkl_input_list: list[list[int]] = None,
84
+ energy: float = None,
85
+ ):
86
+ if energy is not None:
87
+ # reset the energy
88
+ self.energy = energy
89
+ if hkl_input_list is None:
90
+ hkl_input_list = DEFAULT_HKL_LIST
91
+ result = self.calculator.Scatter.structure_factor(
92
+ hkl=hkl_input_list, scattering_type=SCATTERING_TYPE
93
+ )
94
+ else:
95
+ result = self.calculator.Scatter.structure_factor(
96
+ hkl=hkl_input_list, scattering_type=SCATTERING_TYPE
97
+ )
98
+
99
+ return result
100
+
101
+
102
+ if __name__ == "__main__":
103
+ calculator = StructureFactorCalculator()
104
+ calculator.initialize(cif_file_path="data/nacl.cif", energy=10000)
105
+ result = calculator.calculate_structure_factors()
106
+ for hkl, F_hkl in zip(DEFAULT_HKL_LIST, result):
107
+ print(f"hkl = {hkl}, |F| = {np.abs(F_hkl)}")
@@ -0,0 +1,6 @@
1
+ """UI components for the Structure Factor feature."""
2
+
3
+ from .structure_factor_tab import StructureFactorTab
4
+
5
+ __all__ = ["StructureFactorTab"]
6
+
@@ -0,0 +1,12 @@
1
+ from .hkl_plane_components import (
2
+ HKLPlaneControls,
3
+ HKLPlane3DWidget,
4
+ HKLPlane2DWidget,
5
+ )
6
+
7
+ from .customized_plane_components import (
8
+ CustomizedPlaneControls,
9
+ CustomizedPlane3DWidget,
10
+ CustomizedPlane2DWidget,
11
+ CustomizedPlaneWidget,
12
+ )
@@ -0,0 +1,358 @@
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
+ QGridLayout,
7
+ QFormLayout,
8
+ QLabel,
9
+ QPushButton,
10
+ QGroupBox,
11
+ QHBoxLayout,
12
+ QVBoxLayout,
13
+ QSpinBox,
14
+ QLineEdit,
15
+ )
16
+ from PyQt5.QtCore import pyqtSignal, pyqtSlot
17
+ import numpy as np
18
+ import sys
19
+ import os
20
+
21
+ # Add parent directory to path for imports
22
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
23
+ from advisor.ui.visualizers import StructureFactorVisualizer3D, StructureFactorVisualizer2D
24
+ from .hkl_plane_components import EnergySpinBox
25
+
26
+
27
+ class CustomizedPlaneControls(QWidget):
28
+ """Control panel for customized plane visualization with vector inputs."""
29
+
30
+ initializeClicked = pyqtSignal()
31
+ updatePlotsClicked = pyqtSignal()
32
+ parametersChanged = pyqtSignal() # Emitted when any parameter changes
33
+
34
+ def __init__(self, parent=None):
35
+ super().__init__(parent)
36
+ self.init_ui()
37
+
38
+ def init_ui(self):
39
+ """Initialize the control panel UI."""
40
+ layout = QVBoxLayout(self)
41
+
42
+ # Configuration group
43
+ config_group = QGroupBox("Configuration")
44
+ # set the width of the group box to 300px
45
+ config_layout = QFormLayout(config_group)
46
+
47
+ # Energy input (in keV, converted to eV internally)
48
+ self.energy_input = EnergySpinBox()
49
+ config_layout.addRow("X-ray Energy:", self.energy_input)
50
+
51
+ # Compact U, V, Center in one row using text inputs like 110, 010, 000
52
+ uvc_row = QWidget()
53
+ uvc_layout = QHBoxLayout(uvc_row)
54
+ uvc_layout.setContentsMargins(0, 0, 0, 0)
55
+
56
+ self.u_line = QLineEdit()
57
+ self.u_line.setPlaceholderText("1,1,0")
58
+ self.u_line.setText("1,1,0")
59
+
60
+ self.u_line.setFixedWidth(80)
61
+ self.v_line = QLineEdit()
62
+ self.v_line.setPlaceholderText("0,0,1")
63
+ self.v_line.setText("0,0,1")
64
+ self.v_line.setFixedWidth(80)
65
+
66
+ self.c_line = QLineEdit()
67
+ self.c_line.setPlaceholderText("0,0,0")
68
+ self.c_line.setText("0,0,0")
69
+ self.c_line.setFixedWidth(80)
70
+
71
+ uvc_layout.addWidget(QLabel("U"))
72
+ uvc_layout.addWidget(self.u_line)
73
+ uvc_layout.addWidget(QLabel("V"))
74
+ uvc_layout.addWidget(self.v_line)
75
+ uvc_layout.addWidget(QLabel("Center"))
76
+ uvc_layout.addWidget(self.c_line)
77
+ config_layout.addRow("", uvc_row)
78
+
79
+ # u,v range controls on the same row
80
+ ranges_row = QWidget()
81
+ ranges_layout = QHBoxLayout(ranges_row)
82
+ ranges_layout.setContentsMargins(0, 0, 0, 0)
83
+
84
+ self.u_range_spin = QSpinBox()
85
+ self.u_range_spin.setRange(0, 35)
86
+ self.u_range_spin.setValue(3)
87
+
88
+ self.v_range_spin = QSpinBox()
89
+ self.v_range_spin.setRange(0, 35)
90
+ self.v_range_spin.setValue(3)
91
+
92
+ ranges_layout.addWidget(QLabel("U range"))
93
+ ranges_layout.addWidget(self.u_range_spin)
94
+ ranges_layout.addWidget(QLabel("V range"))
95
+ ranges_layout.addWidget(self.v_range_spin)
96
+ config_layout.addRow("", ranges_row)
97
+
98
+ # Initialize button and status
99
+ self.init_btn = QPushButton("Initialize Calculator")
100
+ self.init_btn.clicked.connect(self.initializeClicked.emit)
101
+
102
+ self.status_label = QLabel("Status: Provide CIF in initialization window, then initialize")
103
+ self.status_label.setStyleSheet("color: orange; font-weight: bold;")
104
+ config_layout.addRow("", self.status_label)
105
+
106
+ # Update button (kept for manual refresh if needed)
107
+ self.update_plane_btn = QPushButton("Update Plane & Plots")
108
+ self.update_plane_btn.clicked.connect(self.updatePlotsClicked.emit)
109
+
110
+ # Put init and update button on the same row
111
+ buttons_row = QWidget()
112
+ buttons_layout = QHBoxLayout(buttons_row)
113
+ buttons_layout.setContentsMargins(0, 0, 0, 0)
114
+ buttons_layout.addWidget(self.init_btn)
115
+ buttons_layout.addWidget(self.update_plane_btn)
116
+ config_layout.addRow("", buttons_row)
117
+
118
+ layout.addWidget(config_group)
119
+
120
+ # Connect parameter change signals
121
+ self._connect_signals()
122
+
123
+ def _connect_signals(self):
124
+ """Connect signals for automatic updates."""
125
+ self.u_range_spin.valueChanged.connect(self.parametersChanged.emit)
126
+ self.v_range_spin.valueChanged.connect(self.parametersChanged.emit)
127
+ self.u_line.textChanged.connect(self.parametersChanged.emit)
128
+ self.v_line.textChanged.connect(self.parametersChanged.emit)
129
+ self.c_line.textChanged.connect(self.parametersChanged.emit)
130
+
131
+ def set_status(self, message: str, color: str = "orange"):
132
+ """Update status label."""
133
+ self.status_label.setText(f"Status: {message}")
134
+ self.status_label.setStyleSheet(f"color: {color}; font-weight: bold;")
135
+
136
+ def get_energy_ev(self):
137
+ """Get current energy in eV."""
138
+ return self.energy_input.energy_ev
139
+
140
+ def get_custom_vectors(self):
141
+ """Return U, V, and Center vectors parsed from text inputs.
142
+
143
+ Expected format: comma-separated values like '1,3,11' for h=1, k=3, l=11.
144
+ Negative values are supported, e.g. '-1,2,-3'.
145
+ """
146
+ def parse_hkl(text: str, default: tuple) -> tuple:
147
+ try:
148
+ # Remove all spaces and split by comma
149
+ parts = text.strip().replace(" ", "").split(",")
150
+
151
+ # Must have exactly 3 values
152
+ if len(parts) != 3:
153
+ return default
154
+
155
+ # Parse each part as an integer (supports negative values)
156
+ vals = []
157
+ for part in parts:
158
+ if not part: # Empty string after split
159
+ return default
160
+ vals.append(int(part))
161
+
162
+ return tuple(vals)
163
+ except (ValueError, AttributeError):
164
+ return default
165
+
166
+ U = parse_hkl(self.u_line.text(), (1, 1, 0))
167
+ V = parse_hkl(self.v_line.text(), (0, 0, 1))
168
+ C = parse_hkl(self.c_line.text(), (0, 0, 0))
169
+ return U, V, C
170
+
171
+ def get_ranges(self):
172
+ """Get u and v ranges."""
173
+ return self.u_range_spin.value(), self.v_range_spin.value()
174
+
175
+
176
+ class CustomizedPlane3DWidget(QWidget):
177
+ """3D visualization widget for customized plane with overlay."""
178
+
179
+ def __init__(self, parent=None):
180
+ super().__init__(parent)
181
+ self.init_ui()
182
+
183
+ def init_ui(self):
184
+ """Initialize the 3D widget."""
185
+ layout = QVBoxLayout(self)
186
+ layout.setContentsMargins(0, 0, 0, 0)
187
+
188
+ # Create group box
189
+ self.visualizer3d = StructureFactorVisualizer3D()
190
+
191
+ layout.addWidget(self.visualizer3d)
192
+
193
+ def visualize_structure_factors(self, hkl_list, sf_values):
194
+ """Visualize structure factors in 3D."""
195
+ self.visualizer3d.visualize_structure_factors(hkl_list, np.abs(sf_values))
196
+
197
+ def set_custom_plane(self, U, V, u_min, u_max, v_min, v_max, steps=2, center=(0, 0, 0)):
198
+ """Set custom plane overlay."""
199
+ try:
200
+ self.visualizer3d.set_custom_plane(
201
+ U, V, u_min, u_max, v_min, v_max, steps, center
202
+ )
203
+ except Exception as e:
204
+ print(f"Error setting custom plane: {e}")
205
+
206
+ def clear_plot(self):
207
+ """Clear the 3D plot."""
208
+ self.visualizer3d.clear_plot()
209
+
210
+
211
+ class CustomizedPlane2DWidget(QWidget):
212
+ """2D visualization widget for customized UV plane."""
213
+
214
+ def __init__(self, parent=None):
215
+ super().__init__(parent)
216
+ self.init_ui()
217
+
218
+ def init_ui(self):
219
+ """Initialize the 2D widget."""
220
+ layout = QVBoxLayout(self)
221
+ layout.setContentsMargins(0, 0, 0, 0)
222
+
223
+ self.visualizer2d = StructureFactorVisualizer2D()
224
+ layout.addWidget(self.visualizer2d)
225
+
226
+ def visualize_uv_plane_points(self, uv_points, sf_values, u_label, v_label,
227
+ vector_center=(0, 0, 0), value_max=None):
228
+ """Visualize UV plane points."""
229
+ self.visualizer2d.visualize_uv_plane_points(
230
+ uv_points, sf_values, u_label, v_label, vector_center, value_max
231
+ )
232
+
233
+ def clear_plot(self):
234
+ """Clear the 2D plot."""
235
+ self.visualizer2d.clear_plot()
236
+
237
+
238
+ class CustomizedPlaneWidget(QWidget):
239
+ """Complete customized plane widget combining controls and visualizations."""
240
+
241
+ def __init__(self, parent=None):
242
+ super().__init__(parent)
243
+ self.calculator = None # Will be set by parent
244
+ self.init_ui()
245
+
246
+ def init_ui(self):
247
+ """Initialize the complete widget."""
248
+ main_layout = QGridLayout(self)
249
+
250
+ # Left panel: configuration
251
+ self.controls = CustomizedPlaneControls()
252
+ main_layout.addWidget(self.controls, 1, 0)
253
+
254
+ # 2D plane visualizer below configuration for more space
255
+ self.plane_2d = CustomizedPlane2DWidget()
256
+ main_layout.addWidget(self.plane_2d, 0, 1, 1, 1)
257
+
258
+ # Right panel: 3D spanning both rows
259
+ self.plane_3d = CustomizedPlane3DWidget()
260
+ main_layout.addWidget(self.plane_3d, 0, 0)
261
+
262
+ # Set layout proportions
263
+ main_layout.setColumnStretch(0, 1) # Left column
264
+ main_layout.setColumnStretch(1, 1) # Right column
265
+ main_layout.setRowStretch(0, 3) # More space for visualizers
266
+ main_layout.setRowStretch(1, 1) # Less space for controls
267
+
268
+
269
+ # Connect signals
270
+ self.controls.parametersChanged.connect(self.update_plots)
271
+ self.controls.updatePlotsClicked.connect(self.update_plots)
272
+
273
+ def set_calculator(self, calculator):
274
+ """Set the calculator instance."""
275
+ self.calculator = calculator
276
+
277
+ def get_controls(self):
278
+ """Get the controls widget."""
279
+ return self.controls
280
+
281
+ def _generate_hkl_cube(self, max_index: int = 5):
282
+ """Generate a full integer HKL grid from 0..max_index for 3D visualization."""
283
+ cube = []
284
+ for h in range(0, max_index + 1):
285
+ for k in range(0, max_index + 1):
286
+ for l in range(0, max_index + 1):
287
+ cube.append([h, k, l])
288
+ return cube
289
+
290
+ @pyqtSlot()
291
+ def update_plots(self):
292
+ """Update 3D scatter (all HKL) with a custom plane overlay and 2D uv plot."""
293
+ try:
294
+ # Always update the plane overlay for immediate feedback
295
+ U, V, C = self.controls.get_custom_vectors()
296
+ u_max, v_max = self.controls.get_ranges()
297
+
298
+ # Symmetric parameter ranges around 0; apply center offset in HKL
299
+ u_min_param = -(u_max // 2)
300
+ u_max_param = u_max - (u_max // 2)
301
+ v_min_param = -(v_max // 2)
302
+ v_max_param = v_max - (v_max // 2)
303
+
304
+ # Update plane overlay
305
+ self.plane_3d.set_custom_plane(
306
+ U, V, u_min_param, u_max_param, v_min_param, v_max_param, steps=2, center=C
307
+ )
308
+
309
+ if not self.calculator or not self.calculator.is_initialized:
310
+ return
311
+
312
+ # 3D: plot all HKL points 0..5
313
+ hkl_list = self._generate_hkl_cube(5)
314
+ sf_values = self.calculator.calculate_structure_factors(hkl_list)
315
+ self.plane_3d.visualize_structure_factors(hkl_list, sf_values)
316
+
317
+ # Re-apply plane overlay after replot
318
+ self.plane_3d.set_custom_plane(
319
+ U, V, u_min_param, u_max_param, v_min_param, v_max_param, steps=2, center=C
320
+ )
321
+
322
+ # 2D: points on the plane using integer combinations of U and V in ranges
323
+ uv_points = []
324
+ hkl_points = []
325
+ # symmetric parameter ranges around 0 with given max steps, shifted by center
326
+ for u in range(u_min_param, u_max_param + 1):
327
+ for v in range(v_min_param, v_max_param + 1):
328
+ H = C[0] + u * U[0] + v * V[0]
329
+ K = C[1] + u * U[1] + v * V[1]
330
+ L = C[2] + u * U[2] + v * V[2]
331
+ uv_points.append({"u": u, "v": v, "H": H, "K": K, "L": L})
332
+ hkl_points.append([H, K, L])
333
+
334
+ if len(hkl_points) > 0:
335
+ sf_plane = self.calculator.calculate_structure_factors(hkl_points)
336
+ # Reference value for sizing: use |F(0,0,0)| for consistency
337
+ ref = self.calculator.calculate_structure_factors([[0, 0, 0]])
338
+ value_max = (
339
+ float(np.abs(ref[0]))
340
+ if len(ref) > 0
341
+ else (
342
+ float(np.max(np.abs(sf_plane))) if len(sf_plane) > 0 else None
343
+ )
344
+ )
345
+ u_label = f"[{U[0]} {U[1]} {U[2]}]"
346
+ v_label = f"[{V[0]} {V[1]} {V[2]}]"
347
+ self.plane_2d.visualize_uv_plane_points(
348
+ uv_points, np.abs(sf_plane), u_label, v_label, vector_center=C, value_max=value_max
349
+ )
350
+
351
+ except Exception as e:
352
+ # Keep UI responsive
353
+ print(f"Error updating customized plots: {e}")
354
+
355
+ def clear_plots(self):
356
+ """Clear all plots."""
357
+ self.plane_2d.clear_plot()
358
+ self.plane_3d.clear_plot()