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,391 @@
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
+ QSlider,
15
+ QStackedLayout,
16
+ )
17
+ from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
18
+ import numpy as np
19
+ import sys
20
+ import os
21
+
22
+ # Add parent directory to path for imports
23
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
24
+ from advisor.ui.visualizers import StructureFactorVisualizer3D, StructureFactorVisualizer2D
25
+
26
+
27
+ class EnergySpinBox(QWidget):
28
+ """Custom spinbox that displays keV but stores/returns eV internally."""
29
+
30
+ valueChanged = pyqtSignal(float) # Emits energy in eV
31
+
32
+ def __init__(self, parent=None):
33
+ super().__init__(parent)
34
+ from PyQt5.QtWidgets import QDoubleSpinBox
35
+
36
+ self.spinbox = QDoubleSpinBox()
37
+ self.spinbox.setRange(0.001, 1000.0) # keV range: 1 eV to 100 keV
38
+ self.spinbox.setDecimals(3) # More precision for keV
39
+ self.spinbox.setSuffix(" keV")
40
+ self.spinbox.setValue(100.0) # 10 keV default
41
+
42
+ layout = QHBoxLayout(self)
43
+ layout.setContentsMargins(0, 0, 0, 0)
44
+ layout.addWidget(self.spinbox)
45
+
46
+ # Connect signal
47
+ self.spinbox.valueChanged.connect(lambda v: self.valueChanged.emit(v * 1000.0))
48
+
49
+ @property
50
+ def energy_ev(self):
51
+ """Get energy value in eV (internal storage unit)."""
52
+ return self.spinbox.value() * 1000.0
53
+
54
+ @energy_ev.setter
55
+ def energy_ev(self, value_ev):
56
+ """Set energy value from eV (converts to keV for display)."""
57
+ self.spinbox.setValue(value_ev / 1000.0)
58
+
59
+
60
+ class HKLPlaneControls(QWidget):
61
+ """Control panel for HKL plane visualization with energy input and plane toggles."""
62
+
63
+ initializeClicked = pyqtSignal()
64
+ planeChanged = pyqtSignal(str) # Emits "HK", "HL", or "KL"
65
+ energyChanged = pyqtSignal(float) # Emits energy in eV
66
+
67
+ def __init__(self, parent=None):
68
+ super().__init__(parent)
69
+ self.init_ui()
70
+
71
+ def init_ui(self):
72
+ """Initialize the control panel UI."""
73
+ layout = QVBoxLayout(self)
74
+
75
+ # Configuration group
76
+ config_group = QGroupBox("Configuration")
77
+ config_layout = QFormLayout(config_group)
78
+
79
+ # Energy input
80
+ self.energy_input = EnergySpinBox()
81
+ self.energy_input.valueChanged.connect(self.energyChanged.emit)
82
+ config_layout.addRow("X-ray Energy:", self.energy_input)
83
+
84
+ # Plane toggle buttons
85
+ self.hk_toggle_btn = QPushButton("HK plane")
86
+ self.hl_toggle_btn = QPushButton("HL plane")
87
+ self.kl_toggle_btn = QPushButton("KL plane")
88
+
89
+ for btn in (self.hk_toggle_btn, self.hl_toggle_btn, self.kl_toggle_btn):
90
+ btn.setCheckable(True)
91
+
92
+ self.hk_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("HK"))
93
+ self.hl_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("HL"))
94
+ self.kl_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("KL"))
95
+
96
+ # Add plane toggle row
97
+ toggle_row = QWidget()
98
+ toggle_layout = QHBoxLayout(toggle_row)
99
+ toggle_layout.setContentsMargins(0, 0, 0, 0)
100
+ toggle_layout.addWidget(self.hk_toggle_btn)
101
+ toggle_layout.addWidget(self.hl_toggle_btn)
102
+ toggle_layout.addWidget(self.kl_toggle_btn)
103
+ config_layout.addRow("Plane:", toggle_row)
104
+
105
+ # Initialize button
106
+ self.init_btn = QPushButton("Initialize Calculator")
107
+ self.init_btn.clicked.connect(self.initializeClicked.emit)
108
+ config_layout.addRow("", self.init_btn)
109
+
110
+ # Status label
111
+ self.status_label = QLabel("Status: Provide CIF in initialization window, then initialize")
112
+ self.status_label.setStyleSheet("color: orange; font-weight: bold;")
113
+ config_layout.addRow("", self.status_label)
114
+
115
+ layout.addWidget(config_group)
116
+
117
+ # Default to HK plane
118
+ self._on_plane_clicked("HK")
119
+
120
+ def _on_plane_clicked(self, plane: str):
121
+ """Handle plane toggle button clicks."""
122
+ self._update_toggle_styles(plane)
123
+ self.planeChanged.emit(plane)
124
+
125
+ def _update_toggle_styles(self, active: str):
126
+ """Update toggle button colors based on active plane."""
127
+ active_css = "background-color: #2ecc71; color: white; font-weight: bold;"
128
+ inactive_css = "background-color: #bdc3c7; color: #333333;"
129
+ mapping = {
130
+ "HK": self.hk_toggle_btn,
131
+ "HL": self.hl_toggle_btn,
132
+ "KL": self.kl_toggle_btn,
133
+ }
134
+ for name, btn in mapping.items():
135
+ if name == active:
136
+ btn.setChecked(True)
137
+ btn.setStyleSheet(active_css)
138
+ else:
139
+ btn.setChecked(False)
140
+ btn.setStyleSheet(inactive_css)
141
+
142
+ def set_status(self, message: str, color: str = "orange"):
143
+ """Update status label."""
144
+ self.status_label.setText(f"Status: {message}")
145
+ self.status_label.setStyleSheet(f"color: {color}; font-weight: bold;")
146
+
147
+ def get_energy_ev(self):
148
+ """Get current energy in eV."""
149
+ return self.energy_input.energy_ev
150
+
151
+
152
+ class HKLPlane3DWidget(QWidget):
153
+ """3D visualization widget for HKL structure factors with plane overlays."""
154
+
155
+ def __init__(self, parent=None):
156
+ super().__init__(parent)
157
+ self.init_ui()
158
+
159
+ def init_ui(self):
160
+ """Initialize the 3D widget."""
161
+ layout = QVBoxLayout(self)
162
+ layout.setContentsMargins(0, 0, 0, 0)
163
+
164
+ self.visualizer3d = StructureFactorVisualizer3D()
165
+ layout.addWidget(self.visualizer3d)
166
+
167
+ def initialize(self, params):
168
+ """Initialize the 3D visualizer."""
169
+ self.visualizer3d.initialize(params)
170
+
171
+ def visualize_structure_factors(self, hkl_list, sf_values):
172
+ """Visualize structure factors in 3D."""
173
+ self.visualizer3d.visualize_structure_factors(hkl_list, np.abs(sf_values))
174
+
175
+ def set_plane_values(self, **kwargs):
176
+ """Set plane overlay values."""
177
+ self.visualizer3d.set_plane_values(**kwargs)
178
+
179
+ def set_active_plane(self, plane_axis: str):
180
+ """Set which plane is highlighted."""
181
+ self.visualizer3d.set_active_plane(plane_axis)
182
+
183
+
184
+ class FixedIndexControls(QWidget):
185
+ """Controls for a single fixed index (spin box + slider)."""
186
+
187
+ valueChanged = pyqtSignal(int)
188
+
189
+ def __init__(self, label_prefix: str, fixed_name: str, default_value: int = 0, parent=None):
190
+ super().__init__(parent)
191
+ self.fixed_name = fixed_name
192
+ self.init_ui(label_prefix, fixed_name, default_value)
193
+
194
+ def init_ui(self, label_prefix: str, fixed_name: str, default_value: int):
195
+ """Initialize the controls."""
196
+ layout = QVBoxLayout(self)
197
+
198
+ # Group box
199
+ self.group = QGroupBox(f"{label_prefix}")
200
+ group_layout = QFormLayout(self.group)
201
+
202
+ # Controls row
203
+ row = QWidget()
204
+ row_layout = QHBoxLayout(row)
205
+ row_layout.setContentsMargins(0, 0, 0, 0)
206
+
207
+ # Spin box
208
+ self.spin = QSpinBox()
209
+ self.spin.setRange(0, 35)
210
+ self.spin.setValue(default_value)
211
+
212
+ # Slider
213
+ self.slider = QSlider()
214
+ self.slider.setOrientation(Qt.Horizontal)
215
+ self.slider.setRange(0, 35)
216
+ self.slider.setValue(default_value)
217
+
218
+ row_layout.addWidget(self.spin)
219
+ row_layout.addWidget(self.slider)
220
+ group_layout.addRow(f"{fixed_name}:", row)
221
+
222
+ layout.addWidget(self.group)
223
+
224
+ # Connect signals
225
+ self.spin.valueChanged.connect(self._on_spin_changed)
226
+ self.slider.valueChanged.connect(self._on_slider_changed)
227
+
228
+ def _on_spin_changed(self, value):
229
+ """Handle spin box value change."""
230
+ self.slider.blockSignals(True)
231
+ self.slider.setValue(value)
232
+ self.slider.blockSignals(False)
233
+ self.valueChanged.emit(value)
234
+
235
+ def _on_slider_changed(self, value):
236
+ """Handle slider value change."""
237
+ self.spin.blockSignals(True)
238
+ self.spin.setValue(value)
239
+ self.spin.blockSignals(False)
240
+ self.valueChanged.emit(value)
241
+
242
+ def get_value(self):
243
+ """Get current value."""
244
+ return self.spin.value()
245
+
246
+ def set_value(self, value):
247
+ """Set value programmatically."""
248
+ self.spin.setValue(value)
249
+
250
+
251
+ class HKLPlane2DWidget(QWidget):
252
+ """Widget containing stacked 2D plane visualizers with individual controls."""
253
+
254
+ def __init__(self, parent=None):
255
+ super().__init__(parent)
256
+ self.grid_max = 5 # inclusive max for varying integer indices
257
+ self.init_ui()
258
+
259
+ def init_ui(self):
260
+ """Initialize the 2D widget with stacked layout."""
261
+ main_layout = QGridLayout(self)
262
+
263
+ # Create 2D visualizers
264
+ self.hk_visualizer = StructureFactorVisualizer2D()
265
+ self.hl_visualizer = StructureFactorVisualizer2D()
266
+ self.kl_visualizer = StructureFactorVisualizer2D()
267
+
268
+ # Create control widgets
269
+ self.hk_controls = FixedIndexControls("HK plane", "L", 0)
270
+ self.hl_controls = FixedIndexControls("HL plane", "K", 0)
271
+ self.kl_controls = FixedIndexControls("KL plane", "H", 0)
272
+
273
+ # Plane stack (top)
274
+ self.plane_stack_container = QWidget()
275
+ self.plane_stack = QStackedLayout(self.plane_stack_container)
276
+ self.plane_stack.addWidget(self.hk_visualizer)
277
+ self.plane_stack.addWidget(self.hl_visualizer)
278
+ self.plane_stack.addWidget(self.kl_visualizer)
279
+ main_layout.addWidget(self.plane_stack_container, 0, 0)
280
+
281
+ # Control stack (bottom)
282
+ self.ctrl_stack_container = QWidget()
283
+ self.ctrl_stack = QStackedLayout(self.ctrl_stack_container)
284
+ self.ctrl_stack.addWidget(self.hk_controls)
285
+ self.ctrl_stack.addWidget(self.hl_controls)
286
+ self.ctrl_stack.addWidget(self.kl_controls)
287
+ main_layout.addWidget(self.ctrl_stack_container, 1, 0)
288
+
289
+ # Set layout proportions
290
+ main_layout.setRowStretch(0, 3) # More space for visualizer
291
+ main_layout.setRowStretch(1, 1) # Less space for controls
292
+
293
+ # Default to HK plane
294
+ self.set_active_plane("HK")
295
+
296
+ def set_active_plane(self, plane: str):
297
+ """Set which plane is visible."""
298
+ plane = plane.upper()
299
+ index_map = {"HK": 0, "HL": 1, "KL": 2}
300
+ idx = index_map.get(plane, 0)
301
+ self.plane_stack.setCurrentIndex(idx)
302
+ self.ctrl_stack.setCurrentIndex(idx)
303
+
304
+ def connect_value_changed_signals(self, hk_callback, hl_callback, kl_callback):
305
+ """Connect value changed signals to callbacks."""
306
+ self.hk_controls.valueChanged.connect(hk_callback)
307
+ self.hl_controls.valueChanged.connect(hl_callback)
308
+ self.kl_controls.valueChanged.connect(kl_callback)
309
+
310
+ def connect_3d_plane_signals(self, plane_3d_widget):
311
+ """Connect control changes to 3D plane updates."""
312
+ self.hk_controls.valueChanged.connect(
313
+ lambda v: plane_3d_widget.set_plane_values(L=int(v))
314
+ )
315
+ self.hl_controls.valueChanged.connect(
316
+ lambda v: plane_3d_widget.set_plane_values(K=int(v))
317
+ )
318
+ self.kl_controls.valueChanged.connect(
319
+ lambda v: plane_3d_widget.set_plane_values(H=int(v))
320
+ )
321
+
322
+ def get_plane_values(self):
323
+ """Get current plane control values."""
324
+ return {
325
+ "L": self.hk_controls.get_value(),
326
+ "K": self.hl_controls.get_value(),
327
+ "H": self.kl_controls.get_value(),
328
+ }
329
+
330
+ def _generate_plane_points(self, varying_a: str, varying_b: str, fixed_name: str, fixed_value: int):
331
+ """Create integer HKL points for a plane with two varying indices in [0, grid_max]."""
332
+ points = []
333
+ for a in range(0, self.grid_max + 1):
334
+ for b in range(0, self.grid_max + 1):
335
+ values = {"H": 0, "K": 0, "L": 0}
336
+ values[varying_a] = a
337
+ values[varying_b] = b
338
+ values[fixed_name] = fixed_value
339
+ points.append([values["H"], values["K"], values["L"]])
340
+ return points
341
+
342
+ def update_hk_plane(self, calculator):
343
+ """Update HK plane visualization."""
344
+ if not calculator.is_initialized:
345
+ return
346
+ L_val = self.hk_controls.get_value()
347
+ hkl_list = self._generate_plane_points("H", "K", "L", L_val)
348
+ results = calculator.calculate_structure_factors(hkl_list)
349
+ # Reference value for color scale
350
+ ref = calculator.calculate_structure_factors([[0, 0, 0]])
351
+ value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
352
+ arr = np.array(hkl_list)
353
+ self.hk_visualizer.visualize_plane(
354
+ arr[:, 0], arr[:, 1], np.abs(results), "H", "K", "L", L_val, value_max
355
+ )
356
+
357
+ def update_hl_plane(self, calculator):
358
+ """Update HL plane visualization."""
359
+ if not calculator.is_initialized:
360
+ return
361
+ K_val = self.hl_controls.get_value()
362
+ hkl_list = self._generate_plane_points("H", "L", "K", K_val)
363
+ results = calculator.calculate_structure_factors(hkl_list)
364
+ # Reference value for color scale
365
+ ref = calculator.calculate_structure_factors([[0, 0, 0]])
366
+ value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
367
+ arr = np.array(hkl_list)
368
+ self.hl_visualizer.visualize_plane(
369
+ arr[:, 0], arr[:, 2], np.abs(results), "H", "L", "K", K_val, value_max
370
+ )
371
+
372
+ def update_kl_plane(self, calculator):
373
+ """Update KL plane visualization."""
374
+ if not calculator.is_initialized:
375
+ return
376
+ H_val = self.kl_controls.get_value()
377
+ hkl_list = self._generate_plane_points("K", "L", "H", H_val)
378
+ results = calculator.calculate_structure_factors(hkl_list)
379
+ # Reference value for color scale
380
+ ref = calculator.calculate_structure_factors([[0, 0, 0]])
381
+ value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
382
+ arr = np.array(hkl_list)
383
+ self.kl_visualizer.visualize_plane(
384
+ arr[:, 1], arr[:, 2], np.abs(results), "K", "L", "H", H_val, value_max
385
+ )
386
+
387
+ def clear_plots(self):
388
+ """Clear all plane visualizations."""
389
+ self.hk_visualizer.clear_plot()
390
+ self.hl_visualizer.clear_plot()
391
+ self.kl_visualizer.clear_plot()
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # pylint: disable=no-name-in-module, import-error
4
+ import os
5
+ import numpy as np
6
+ from PyQt5.QtWidgets import (
7
+ QGridLayout,
8
+ QTabWidget,
9
+ QWidget,
10
+ QMessageBox,
11
+ )
12
+ from PyQt5.QtCore import pyqtSlot
13
+
14
+ from advisor.features.structure_factor.domain import StructureFactorCalculator
15
+ from advisor.ui.tab_interface import TabInterface
16
+ from advisor.ui.tips import Tips, set_tip
17
+ from .components import (
18
+ HKLPlaneControls,
19
+ HKLPlane3DWidget,
20
+ HKLPlane2DWidget,
21
+ CustomizedPlaneWidget,
22
+ )
23
+
24
+
25
+ class StructureFactorTab(TabInterface):
26
+ """Tab for calculating structure factors using X-ray scattering."""
27
+
28
+ def __init__(self, controller=None, calculator=None):
29
+ self.controller = controller
30
+ self.calculator = calculator or StructureFactorCalculator()
31
+ self.tips = Tips()
32
+
33
+ # Initialize UI first
34
+ main_window = controller.app_controller.main_window if controller else None
35
+ super().__init__(controller=controller, main_window=main_window)
36
+ self.setWindowTitle("Structure Factor Calculator")
37
+
38
+ def init_ui(self):
39
+ """Initialize UI components with subtabs."""
40
+ self.tab_widget = QTabWidget()
41
+ self.layout.addWidget(self.tab_widget, 0, 0)
42
+
43
+ self._create_hkl_plane_tab()
44
+ self._create_customized_tab()
45
+
46
+ def set_parameters(self, params: dict):
47
+ """Set parameters from global lattice configuration.
48
+
49
+ Note: Structure factor calculator requires CIF file and energy,
50
+ which are not part of the global lattice parameters.
51
+ """
52
+ if not params:
53
+ return
54
+ # Initialize visualizers through components
55
+ if hasattr(self, 'hkl_plane_3d'):
56
+ self.hkl_plane_3d.initialize(params)
57
+
58
+ # Clear all plots
59
+ if hasattr(self, 'hkl_plane_2d'):
60
+ self.hkl_plane_2d.clear_plots()
61
+ if hasattr(self, 'customized_plane_widget'):
62
+ self.customized_plane_widget.clear_plots()
63
+
64
+ def _set_tip(self, widget, name):
65
+ """Set the tooltip and status tip for a widget by the name"""
66
+ set_tip(widget, self.tips.tip(name))
67
+
68
+ def _create_hkl_plane_tab(self):
69
+ """Create the HKL plane subtab using components."""
70
+ hkl_tab = QWidget()
71
+ main_layout = QGridLayout(hkl_tab)
72
+
73
+ # Create components
74
+ self.hkl_controls = HKLPlaneControls()
75
+ self.hkl_plane_3d = HKLPlane3DWidget()
76
+ self.hkl_plane_2d = HKLPlane2DWidget()
77
+
78
+ # Layout components
79
+ main_layout.addWidget(self.hkl_plane_3d, 0, 0) # 3D top-left
80
+ main_layout.addWidget(self.hkl_controls, 1, 0) # Controls bottom-left
81
+ main_layout.addWidget(self.hkl_plane_2d, 0, 1, 2, 1) # 2D right side spanning both rows
82
+
83
+ # Set layout proportions
84
+ main_layout.setColumnStretch(0, 1) # Left column
85
+ main_layout.setColumnStretch(1, 1) # Right column
86
+ main_layout.setRowStretch(0, 3) # More space for visualizers
87
+ main_layout.setRowStretch(1, 1) # Less space for controls
88
+
89
+ # Connect signals
90
+ self._connect_hkl_signals()
91
+
92
+ # Add to tab widget
93
+ self.tab_widget.addTab(hkl_tab, "HKL plane")
94
+
95
+ def _connect_hkl_signals(self):
96
+ """Connect signals for HKL plane tab."""
97
+ # Initialize button
98
+ self.hkl_controls.initializeClicked.connect(self.initialize_calculator_hkl)
99
+
100
+ # Plane change
101
+ self.hkl_controls.planeChanged.connect(self._on_plane_changed)
102
+
103
+ # Connect 2D plane updates to calculator
104
+ self.hkl_plane_2d.connect_value_changed_signals(
105
+ lambda: self.hkl_plane_2d.update_hk_plane(self.calculator),
106
+ lambda: self.hkl_plane_2d.update_hl_plane(self.calculator),
107
+ lambda: self.hkl_plane_2d.update_kl_plane(self.calculator)
108
+ )
109
+
110
+ # Connect 2D controls to 3D plane updates
111
+ self.hkl_plane_2d.connect_3d_plane_signals(self.hkl_plane_3d)
112
+
113
+ def _create_customized_tab(self):
114
+ """Create the customized plane subtab using components."""
115
+ # Create the complete customized widget
116
+ self.customized_plane_widget = CustomizedPlaneWidget()
117
+ self.customized_plane_widget.set_calculator(self.calculator)
118
+
119
+ # Connect initialize signal
120
+ controls = self.customized_plane_widget.get_controls()
121
+ controls.initializeClicked.connect(self.initialize_calculator_customized)
122
+
123
+ # Add to tab widget
124
+ self.tab_widget.addTab(self.customized_plane_widget, "Customized plane")
125
+
126
+ @pyqtSlot()
127
+ def initialize_calculator_hkl(self):
128
+ """Initialize the structure factor calculator for HKL plane tab."""
129
+ try:
130
+ params = self.controller.app_controller.get_parameters() if self.controller else None
131
+ cif_path = params.get("cif_file") if params else None
132
+ if not cif_path:
133
+ QMessageBox.warning(
134
+ self,
135
+ "Missing CIF",
136
+ "Please load a valid CIF file in the initialization window first.",
137
+ )
138
+ return
139
+
140
+ energy_ev = self.hkl_controls.get_energy_ev()
141
+ self.calculator.initialize(cif_path, energy_ev)
142
+
143
+ self.hkl_controls.set_status("Calculator initialized successfully", "green")
144
+
145
+ # Populate 3D using full HKL cube ranging 0..5
146
+ hkl_list = self._generate_hkl_cube(5)
147
+ results = self.calculator.calculate_structure_factors(hkl_list)
148
+ self.hkl_plane_3d.visualize_structure_factors(hkl_list, results)
149
+
150
+ # Initialize 2D slices
151
+ self.hkl_plane_2d.update_hk_plane(self.calculator)
152
+ self.hkl_plane_2d.update_hl_plane(self.calculator)
153
+ self.hkl_plane_2d.update_kl_plane(self.calculator)
154
+
155
+ # Initialize 3D planes based on current control values
156
+ plane_values = self.hkl_plane_2d.get_plane_values()
157
+ self.hkl_plane_3d.set_plane_values(**plane_values)
158
+
159
+ except Exception as e:
160
+ QMessageBox.critical(
161
+ self, "Error", f"Failed to initialize calculator: {str(e)}"
162
+ )
163
+ self.hkl_controls.set_status(f"Initialization failed - {str(e)}", "red")
164
+
165
+ @pyqtSlot()
166
+ def initialize_calculator_customized(self):
167
+ """Initialize the structure factor calculator for customized plane tab."""
168
+ try:
169
+ params = self.controller.app_controller.get_parameters() if self.controller else None
170
+ cif_path = params.get("cif_file") if params else None
171
+ if not cif_path:
172
+ QMessageBox.warning(
173
+ self,
174
+ "Missing CIF",
175
+ "Please load a valid CIF file in the initialization window first.",
176
+ )
177
+ return
178
+
179
+ controls = self.customized_plane_widget.get_controls()
180
+ energy_ev = controls.get_energy_ev()
181
+ self.calculator.initialize(cif_path, energy_ev)
182
+
183
+ controls.set_status("Calculator initialized successfully", "green")
184
+
185
+ # Update plots
186
+ self.customized_plane_widget.update_plots()
187
+
188
+ except Exception as e:
189
+ QMessageBox.critical(
190
+ self, "Error", f"Failed to initialize calculator: {str(e)}"
191
+ )
192
+ controls = self.customized_plane_widget.get_controls()
193
+ controls.set_status(f"Initialization failed - {str(e)}", "red")
194
+
195
+ @pyqtSlot(str)
196
+ def _on_plane_changed(self, plane: str):
197
+ """Handle plane change from controls."""
198
+ # Update 2D widget to show the selected plane
199
+ self.hkl_plane_2d.set_active_plane(plane)
200
+
201
+ # Update 3D plane highlighting
202
+ if plane == "HK":
203
+ self.hkl_plane_3d.set_active_plane("L") # HK plane means L constant
204
+ elif plane == "HL":
205
+ self.hkl_plane_3d.set_active_plane("K")
206
+ elif plane == "KL":
207
+ self.hkl_plane_3d.set_active_plane("H")
208
+
209
+ # Update corresponding plot if initialized
210
+ if self.calculator.is_initialized:
211
+ if plane == "HK":
212
+ self.hkl_plane_2d.update_hk_plane(self.calculator)
213
+ elif plane == "HL":
214
+ self.hkl_plane_2d.update_hl_plane(self.calculator)
215
+ elif plane == "KL":
216
+ self.hkl_plane_2d.update_kl_plane(self.calculator)
217
+
218
+ def _generate_hkl_cube(self, max_index: int = 5):
219
+ """Generate a full integer HKL grid from 0..max_index for 3D visualization."""
220
+ cube = []
221
+ for h in range(0, max_index + 1):
222
+ for k in range(0, max_index + 1):
223
+ for l in range(0, max_index + 1):
224
+ cube.append([h, k, l])
225
+ return cube
226
+
227
+ def get_module_instance(self):
228
+ """Get the backend module instance."""
229
+ return self.calculator
230
+
231
+ def clear(self):
232
+ """Clear all inputs and results."""
233
+ # Clear visualizations
234
+ if hasattr(self, 'hkl_plane_2d'):
235
+ self.hkl_plane_2d.clear_plots()
236
+ if hasattr(self, 'customized_plane_widget'):
237
+ self.customized_plane_widget.clear_plots()
238
+
239
+ def get_state(self):
240
+ """Get the current state for session saving."""
241
+ state = {}
242
+ if hasattr(self, 'hkl_controls'):
243
+ state["hkl_energy"] = self.hkl_controls.get_energy_ev()
244
+ if hasattr(self, 'customized_plane_widget'):
245
+ controls = self.customized_plane_widget.get_controls()
246
+ state["custom_energy"] = controls.get_energy_ev()
247
+ return state
248
+
249
+ def set_state(self, state):
250
+ """Restore tab state from saved session."""
251
+ try:
252
+ if "hkl_energy" in state and hasattr(self, 'hkl_controls'):
253
+ self.hkl_controls.energy_input.energy_ev = state["hkl_energy"]
254
+
255
+ if "custom_energy" in state and hasattr(self, 'customized_plane_widget'):
256
+ controls = self.customized_plane_widget.get_controls()
257
+ controls.energy_input.energy_ev = state["custom_energy"]
258
+
259
+ # Try to reinitialize if we have the required data globally
260
+ params = self.controller.app_controller.get_parameters() if self.controller else None
261
+ if (
262
+ params
263
+ and params.get("cif_file")
264
+ and os.path.exists(params.get("cif_file"))
265
+ ):
266
+ if hasattr(self, 'hkl_controls'):
267
+ self.initialize_calculator_hkl()
268
+ if hasattr(self, 'customized_plane_widget'):
269
+ self.initialize_calculator_customized()
270
+
271
+ return True
272
+ except Exception:
273
+ return False
File without changes
@@ -0,0 +1,14 @@
1
+ {
2
+ "app_name": "Advisor-Scattering",
3
+ "app_version": "0.1.0",
4
+ "window_size": {
5
+ "width": 1200,
6
+ "height": 800
7
+ },
8
+ "theme": "default",
9
+ "data_directory": "data",
10
+ "figures_directory": "figures",
11
+ "save_session_on_exit": true,
12
+ "show_welcome_screen": true,
13
+ "debug_mode": false
14
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "TTH": "Scattering angle: the angle between the incident and scattered beams",
3
+ "THETA": "Incident angle: the angle between the incident beam and the sample surface"
4
+ }