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,725 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # pylint: disable=no-name-in-module, import-error
4
+ import matplotlib
5
+ from PyQt5.QtWidgets import (
6
+ QWidget,
7
+ QGridLayout,
8
+ QFormLayout,
9
+ QLabel,
10
+ QLineEdit,
11
+ QPushButton,
12
+ QTabWidget,
13
+ QDoubleSpinBox,
14
+ QGroupBox,
15
+ QRadioButton,
16
+ QButtonGroup,
17
+ QFileDialog,
18
+ QMessageBox,
19
+ QComboBox,
20
+ QVBoxLayout,
21
+ QHBoxLayout,
22
+ QTableWidget,
23
+ QTableWidgetItem,
24
+ QHeaderView,
25
+ QFrame,
26
+ )
27
+ from PyQt5.QtCore import Qt, pyqtSlot, QMimeData
28
+ from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QFont, QColor, QBrush
29
+
30
+ # Tell matplotlib to render plots using the Qt5 framework with the Agg backend for drawing
31
+ matplotlib.use("Qt5Agg")
32
+
33
+ from advisor.features.scattering_geometry.domain import BrillouinCalculator
34
+ from advisor.ui.tab_interface import TabInterface
35
+ from advisor.ui.visualizers import (
36
+ ScatteringVisualizer,
37
+ UnitcellVisualizer,
38
+ HKLScan2DVisualizer,
39
+ )
40
+ from advisor.ui.tips import Tips, set_tip
41
+ from .components import (
42
+ HKLScanControls,
43
+ HKLScanResultsTable,
44
+ HKAnglesControls,
45
+ HKAnglesResultsWidget,
46
+ AnglesToHKLControls,
47
+ AnglesToHKLResults,
48
+ HKLToAnglesControls,
49
+ HKLToAnglesResultsWidget,
50
+ )
51
+ class DragDropLineEdit(QLineEdit):
52
+ """Custom QLineEdit that accepts drag and drop events."""
53
+
54
+ def __init__(self, parent=None):
55
+ super().__init__(parent)
56
+ self.setAcceptDrops(True)
57
+ self.setPlaceholderText("Drag and drop CIF file here or click Browse...")
58
+ self.setReadOnly(True)
59
+
60
+ def dragEnterEvent(self, event: QDragEnterEvent):
61
+ if event.mimeData().hasUrls():
62
+ urls = event.mimeData().urls()
63
+ if urls and urls[0].toLocalFile().endswith(".cif"):
64
+ event.acceptProposedAction()
65
+
66
+ def dropEvent(self, event: QDropEvent):
67
+ urls = event.mimeData().urls()
68
+ if urls:
69
+ file_path = urls[0].toLocalFile()
70
+ if file_path.endswith(".cif"):
71
+ self.setText(file_path)
72
+ # Emit the textChanged signal to notify parent
73
+ self.textChanged.emit(file_path)
74
+
75
+
76
+ class ScatteringGeometryTab(TabInterface):
77
+ """Tab for calculating Brillouin zone / scattering geometry parameters."""
78
+
79
+ def __init__(self, controller=None, calculator=None):
80
+ self.controller = controller
81
+ # Backend instance provided by controller
82
+ self.calculator = calculator or BrillouinCalculator()
83
+ self.angles_to_hkl_visualizer = ScatteringVisualizer() # first subtab
84
+ self.hkl_to_angles_visualizer = ScatteringVisualizer() # second subtab
85
+ self.hk_fixed_tth_visualizer = ScatteringVisualizer() # third subtab
86
+
87
+ # Unit cell visualizers for each subtab
88
+ self.angles_to_hkl_unitcell_viz = UnitcellVisualizer()
89
+ self.hkl_to_angles_unitcell_viz = UnitcellVisualizer()
90
+ self.hk_fixed_tth_unitcell_viz = UnitcellVisualizer()
91
+ self.funtional_objects = [
92
+ self.calculator,
93
+ self.angles_to_hkl_visualizer,
94
+ self.hkl_to_angles_visualizer,
95
+ self.hk_fixed_tth_visualizer,
96
+ ] # group up the funtional objects and initailize them later on
97
+ self.tips = Tips()
98
+
99
+ # Store parameters for display
100
+ self.parameters = None
101
+
102
+ # Initialize UI
103
+ main_window = controller.app_controller.main_window if controller else None
104
+ super().__init__(controller=controller, main_window=main_window)
105
+
106
+ params = controller.app_controller.get_parameters() if controller else None
107
+ if params:
108
+ self.set_parameters(params)
109
+ # Set window title
110
+ self.setWindowTitle("Scattering Geometry")
111
+
112
+ def init_ui(self):
113
+ """Initialize UI components."""
114
+ # Create tab widget for input methods
115
+ self.tab_widget = QTabWidget()
116
+ self.layout.addWidget(self.tab_widget, 0, 0) # Back to (0,0) position
117
+
118
+ # Create and add parameter header to the right
119
+ self._create_parameter_header()
120
+
121
+ # Create tabs for different functionalities
122
+ self._create_angles_to_hkl_tab()
123
+ self._create_hkl_to_angles_tab()
124
+ self._create_hk_to_angles_tth_fixed_tab()
125
+ self._create_hkl_scan_tab() # Add the new HKL scan tab
126
+
127
+ def _create_parameter_header(self):
128
+ """Create parameter display header."""
129
+ # Create main header frame
130
+ header_frame = QFrame()
131
+ header_frame.setObjectName("parameterPanel")
132
+ header_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
133
+ header_frame.setLineWidth(1)
134
+ header_frame.setFixedWidth(200) # Reduced from 280 to 220 for narrower panel
135
+
136
+ # Create header layout - vertical for sidebar
137
+ header_layout = QVBoxLayout(header_frame)
138
+ header_layout.setContentsMargins(15, 15, 15, 15)
139
+ header_layout.setSpacing(15) # Reduced from 25 to 15 for more compact layout
140
+
141
+ # Crystal Structure section
142
+ crystal_title = QLabel("Crystal Structure")
143
+ crystal_title.setObjectName("parameterSectionTitle")
144
+ header_layout.addWidget(crystal_title)
145
+
146
+ self.crystal_info_label = QLabel(
147
+ "a = -- Å\nb = -- Å\nc = -- Å\nα = --°\nβ = --°\nγ = --°"
148
+ )
149
+ self.crystal_info_label.setObjectName("parameterText")
150
+ self.crystal_info_label.setWordWrap(True)
151
+ header_layout.addWidget(self.crystal_info_label)
152
+
153
+ # X-ray section
154
+ xray_title = QLabel("X-ray Parameters")
155
+ xray_title.setObjectName("parameterSectionTitle")
156
+ header_layout.addWidget(xray_title)
157
+
158
+ self.xray_info_label = QLabel("Energy: -- eV\nλ: -- Å\n|k|: -- Å⁻¹")
159
+ self.xray_info_label.setObjectName("parameterText")
160
+ self.xray_info_label.setWordWrap(True)
161
+ header_layout.addWidget(self.xray_info_label)
162
+
163
+ # Add stretch to push edit button to the bottom
164
+ header_layout.addStretch()
165
+
166
+ # Edit button
167
+ edit_button = QPushButton("Reset Parameters")
168
+ edit_button.setObjectName("editParametersButton")
169
+ edit_button.setToolTip("Return to parameter initialization window")
170
+ edit_button.clicked.connect(self._edit_parameters)
171
+ edit_button.setFixedHeight(35) # Reduced from 45 to 35
172
+ header_layout.addWidget(edit_button)
173
+
174
+ # Add header to main layout - right side
175
+ self.layout.addWidget(header_frame, 0, 1)
176
+
177
+ # Set column stretch so tab widget takes most space
178
+ self.layout.setColumnStretch(0, 5) # Tab widget gets more space
179
+ self.layout.setColumnStretch(1, 1) # Header gets less space
180
+
181
+ def set_parameters(self, params: dict):
182
+ """Set parameters from global settings."""
183
+ if not params:
184
+ return
185
+ # Store parameters
186
+ self.parameters = params
187
+
188
+ # Update header display
189
+ self._update_parameter_display()
190
+
191
+ # Initialize functional objects
192
+ for obj in self.funtional_objects:
193
+ obj.initialize(params=params)
194
+
195
+ # Initialize unit cell visualizers if CIF file is provided
196
+ cif_file = params.get('cif_file')
197
+ if cif_file:
198
+ self._update_unitcell_visualizers(cif_file)
199
+
200
+ # HKL scan visualizer uses trajectory-only mode, no structure factor calculator needed
201
+
202
+
203
+ def _update_unitcell_visualizers(self, cif_file_path: str):
204
+ """Update all unit cell visualizers with the CIF file."""
205
+ try:
206
+ unit_cell_vizs = [
207
+ self.angles_to_hkl_unitcell_viz,
208
+ self.hkl_to_angles_unitcell_viz,
209
+ self.hk_fixed_tth_unitcell_viz
210
+ ]
211
+
212
+ for viz in unit_cell_vizs:
213
+ viz.set_parameters({"cif_file": cif_file_path})
214
+ viz.visualize_unitcell()
215
+
216
+ except Exception as e:
217
+ print(f"**Error** updating unit cell visualizers: {e}")
218
+
219
+ def _update_parameter_display(self):
220
+ """Update the parameter display in the header."""
221
+ if not self.parameters:
222
+ return
223
+
224
+ # Update crystal structure display - vertical format
225
+ crystal_text = (
226
+ f"a = {self.parameters.get('a', 0):.2f} Å\n"
227
+ f"b = {self.parameters.get('b', 0):.2f} Å\n"
228
+ f"c = {self.parameters.get('c', 0):.2f} Å\n"
229
+ f"α = {self.parameters.get('alpha', 0):.1f}°\n"
230
+ f"β = {self.parameters.get('beta', 0):.1f}°\n"
231
+ f"γ = {self.parameters.get('gamma', 0):.1f}°"
232
+ )
233
+ self.crystal_info_label.setText(crystal_text)
234
+
235
+ # Update X-ray display - vertical format
236
+ energy = self.parameters.get("energy", 0)
237
+ # Convert eV to Angstrom: λ = hc/E = 12398.4 / E(eV)
238
+ wavelength = 12398.4 / energy if energy > 0 else 0
239
+ wavevector = 2 * 3.1415926 / wavelength if wavelength else 0
240
+ xray_text = (
241
+ f"Energy: {energy:.2f} eV\n"
242
+ f"λ: {wavelength:.3f} Å\n"
243
+ f"|k|: {wavevector:.3f} Å⁻¹"
244
+ )
245
+ self.xray_info_label.setText(xray_text)
246
+
247
+ @pyqtSlot()
248
+ def _edit_parameters(self):
249
+ """Return to parameter initialization window."""
250
+ if self.controller:
251
+ self.controller.app_controller.reset_parameters()
252
+
253
+ def _set_tip(self, widget, name):
254
+ """Set the tooltip and status tip for a widget by the name"""
255
+ set_tip(widget, self.tips.tip(name))
256
+
257
+ def _create_angles_to_hkl_tab(self):
258
+ """Create tab for angles to HKL calculation."""
259
+ angles_tab = QWidget()
260
+ angles_layout = QHBoxLayout(angles_tab)
261
+
262
+ # Left column - Controls and Results
263
+ left_column = QWidget()
264
+ left_layout = QVBoxLayout(left_column)
265
+
266
+ # Create controls widget
267
+ self.angles_to_hkl_controls = AnglesToHKLControls(parent=self)
268
+ self.angles_to_hkl_controls.calculateClicked.connect(self.calculate_hkl)
269
+ self.angles_to_hkl_controls.anglesChanged.connect(self.calculate_hkl) # Auto-calculate on angle change
270
+ left_layout.addWidget(self.angles_to_hkl_controls)
271
+
272
+ # Create results widget
273
+ self.angles_to_hkl_results = AnglesToHKLResults(parent=self)
274
+ left_layout.addWidget(self.angles_to_hkl_results)
275
+
276
+ left_layout.addStretch() # Add stretch to push content to top
277
+
278
+ # Right column - Visualizers
279
+ right_column = QWidget()
280
+ right_layout = QVBoxLayout(right_column)
281
+
282
+ # Scattering visualizer
283
+ self.angles_to_hkl_visualizer.visualize_lab_system()
284
+ self.angles_to_hkl_visualizer.visualize_scattering_geometry()
285
+ right_layout.addWidget(self.angles_to_hkl_visualizer)
286
+
287
+ # Unit cell visualizer
288
+ right_layout.addWidget(self.angles_to_hkl_unitcell_viz)
289
+
290
+ # Add columns to main layout
291
+ angles_layout.addWidget(left_column, 1) # Left column takes 1 part
292
+ angles_layout.addWidget(right_column, 1.5) # Right column takes 1.5 parts
293
+
294
+ # Add to tab widget
295
+ self.tab_widget.addTab(angles_tab, "Angles → HKL")
296
+
297
+ def _create_hkl_to_angles_tab(self):
298
+ """Create tab for HKL to angles calculation."""
299
+ hkl_tab = QWidget()
300
+ hkl_layout = QHBoxLayout(hkl_tab)
301
+
302
+ # Left column - Controls and Results
303
+ left_column = QWidget()
304
+ left_layout = QVBoxLayout(left_column)
305
+
306
+ # Create controls widget
307
+ self.hkl_to_angles_controls = HKLToAnglesControls(parent=self)
308
+ self.hkl_to_angles_controls.calculateClicked.connect(self.calculate_angles)
309
+ left_layout.addWidget(self.hkl_to_angles_controls)
310
+
311
+ # Create results widget
312
+ self.hkl_to_angles_results = HKLToAnglesResultsWidget(parent=self)
313
+ # self.hkl_to_angles_results.solutionSelected.connect(self.on_angle_solution_selected)
314
+ left_layout.addWidget(self.hkl_to_angles_results)
315
+
316
+ left_layout.addStretch() # Add stretch to push content to top
317
+
318
+ # Right column - Visualizers
319
+ right_column = QWidget()
320
+ right_layout = QVBoxLayout(right_column)
321
+
322
+ # Scattering visualizer
323
+ self.hkl_to_angles_visualizer.visualize_lab_system()
324
+ self.hkl_to_angles_visualizer.visualize_scattering_geometry()
325
+ right_layout.addWidget(self.hkl_to_angles_visualizer)
326
+
327
+ # Unit cell visualizer
328
+ right_layout.addWidget(self.hkl_to_angles_unitcell_viz)
329
+
330
+ # Add columns to main layout
331
+ hkl_layout.addWidget(left_column, 1) # Left column takes 1 part
332
+ hkl_layout.addWidget(right_column, 1.5) # Right column takes 1.5 parts
333
+
334
+ # Add to tab widget
335
+ self.tab_widget.addTab(hkl_tab, "HKL → Angles")
336
+
337
+ def _create_hk_to_angles_tth_fixed_tab(self):
338
+ """Create tab for HK to angles calculation with fixed tth."""
339
+ hk_tab = QWidget()
340
+ hk_layout = QHBoxLayout(hk_tab)
341
+
342
+ # Left column - Controls and Results
343
+ left_column = QWidget()
344
+ left_layout = QVBoxLayout(left_column)
345
+
346
+ # Create controls widget
347
+ self.hk_angles_controls = HKAnglesControls(parent=self)
348
+ self.hk_angles_controls.calculateClicked.connect(self.calculate_angles_tth_fixed)
349
+ left_layout.addWidget(self.hk_angles_controls)
350
+
351
+ # Create results widget
352
+ self.hk_angles_results = HKAnglesResultsWidget(parent=self)
353
+ # self.hk_angles_results.solutionSelected.connect(self.on_angle_solution_selected_tth)
354
+ left_layout.addWidget(self.hk_angles_results)
355
+
356
+ left_layout.addStretch() # Add stretch to push content to top
357
+
358
+ # Right column - Visualizers
359
+ right_column = QWidget()
360
+ right_layout = QVBoxLayout(right_column)
361
+
362
+ # Scattering visualizer
363
+ self.hk_fixed_tth_visualizer.visualize_lab_system()
364
+ self.hk_fixed_tth_visualizer.visualize_scattering_geometry()
365
+ right_layout.addWidget(self.hk_fixed_tth_visualizer)
366
+
367
+ # Unit cell visualizer
368
+ right_layout.addWidget(self.hk_fixed_tth_unitcell_viz)
369
+
370
+ # Add columns to main layout
371
+ hk_layout.addWidget(left_column, 1) # Left column takes 1 part
372
+ hk_layout.addWidget(right_column, 1.5) # Right column takes 1.5 parts
373
+
374
+ # Add to tab widget
375
+ self.tab_widget.addTab(hk_tab, "HK to Angles | tth fixed")
376
+
377
+ def _create_hkl_scan_tab(self):
378
+ """Create tab for scanning a range of HKL values."""
379
+ scan_tab = QWidget()
380
+ scan_layout = QHBoxLayout(scan_tab)
381
+
382
+ # Create controls widget
383
+ self.hkl_scan_controls = HKLScanControls(parent=self)
384
+ self.hkl_scan_controls.calculateClicked.connect(self.calculate_hkl_scan)
385
+ scan_layout.addWidget(self.hkl_scan_controls, 1)
386
+
387
+ # Create results table & 2d visualization
388
+ results_layout = QVBoxLayout()
389
+
390
+ # Results table
391
+ self.hkl_scan_results_table = HKLScanResultsTable(parent=self)
392
+ results_layout.addWidget(self.hkl_scan_results_table, 1)
393
+
394
+ # 2D visualizer section
395
+ self.hkl_scan_visualizer = HKLScan2DVisualizer(parent=self)
396
+ results_layout.addWidget(self.hkl_scan_visualizer, 1)
397
+
398
+ # Add to layout
399
+ scan_layout.addLayout(results_layout, 2)
400
+ # Add to tab widget
401
+ self.tab_widget.addTab(scan_tab, "HKL Scan | tth fixed")
402
+
403
+ @pyqtSlot()
404
+ def calculate_hkl_scan(self):
405
+ """Calculate angles for a range of HKL values."""
406
+ try:
407
+ # Check if calculator is initialized
408
+ if not self.calculator.is_initialized():
409
+ QMessageBox.warning(
410
+ self, "Warning", "Please initialize the calculator first!"
411
+ )
412
+ self.tab_widget.setCurrentIndex(0)
413
+ return
414
+
415
+ # Get parameters for scan
416
+ params = self.hkl_scan_controls.get_scan_parameters()
417
+
418
+ # Calculate angles for the scan
419
+ result = self.calculator.calculate_angles_tth_fixed_scan(
420
+ tth=params["tth"],
421
+ start_points=params["start_points"],
422
+ end_points=params["end_points"],
423
+ num_points=params["num_points"],
424
+ deactivated_index=params["deactivated_index"],
425
+ fixed_angle_name=params["fixed_angle_name"],
426
+ fixed_angle=params["fixed_angle"],
427
+ )
428
+
429
+ # Check for success
430
+ success = result.get("success", False)
431
+ if not success:
432
+ QMessageBox.warning(
433
+ self, "Warning", result.get("error", "Unknown error")
434
+ )
435
+ return
436
+
437
+ # Display results in table
438
+ self.hkl_scan_results_table.display_results(result)
439
+
440
+ # Update visualization
441
+ self.update_hkl_visualization(result)
442
+
443
+ except Exception as e:
444
+ QMessageBox.critical(self, "Error", f"**Error** calculating HKL scan: {str(e)}")
445
+
446
+ @pyqtSlot()
447
+ def update_hkl_visualization(self, scan_results=None):
448
+ """Update the HKL scan visualization with auto-detected ranges and results."""
449
+ try:
450
+ # Structure factor calculator not needed for trajectory-only visualization
451
+
452
+ # Use the provided scan results or the last stored results
453
+ if scan_results is None:
454
+ scan_results = getattr(self.hkl_scan_visualizer, 'last_scan_results', None)
455
+
456
+ if scan_results:
457
+ # Determine plane type from deactivated index
458
+ deactivated_index = scan_results.get("deactivated_index", "L")
459
+ if deactivated_index == "L":
460
+ plane_type = "HK"
461
+ elif deactivated_index == "K":
462
+ plane_type = "HL"
463
+ elif deactivated_index == "H":
464
+ plane_type = "KL"
465
+ else:
466
+ plane_type = "HK" # Default
467
+
468
+ # Visualize results (ranges will be auto-detected)
469
+ self.hkl_scan_visualizer.visualize_results(scan_results, plane_type)
470
+ else:
471
+ # No scan results yet, just clear the plot
472
+ self.hkl_scan_visualizer.clear_plot()
473
+
474
+ except Exception as e:
475
+ print(f"**Error** updating HKL visualization: {e}")
476
+
477
+ @pyqtSlot()
478
+ def browse_cif_file(self):
479
+ """Browse for CIF file."""
480
+ file_path, _ = QFileDialog.getOpenFileName(
481
+ self, "Open CIF File", "", "CIF Files (*.cif);;All Files (*)"
482
+ )
483
+
484
+ if file_path:
485
+ self.file_path_input.setText(file_path)
486
+
487
+ @pyqtSlot()
488
+ def calculate_hkl(self):
489
+ """Calculate HKL from angles."""
490
+ try:
491
+ # Check if calculator is initialized
492
+ if not self.calculator.is_initialized():
493
+ QMessageBox.warning(
494
+ self, "Warning", "Please initialize the calculator first!"
495
+ )
496
+ self.tab_widget.setCurrentIndex(0)
497
+ return
498
+
499
+ # Get parameters from the controls component
500
+ params = self.angles_to_hkl_controls.get_calculation_parameters()
501
+
502
+ # Calculate HKL
503
+ result = self.calculator.calculate_hkl(
504
+ tth=params["tth"],
505
+ theta=params["theta"],
506
+ phi=params["phi"],
507
+ chi=params["chi"],
508
+ )
509
+ roll, pitch, yaw = self.parameters["roll"], self.parameters["pitch"], self.parameters["yaw"]
510
+ success = result.get("success", False)
511
+ if not success:
512
+ QMessageBox.warning(
513
+ self, "Warning", result.get("error", "Unknown error")
514
+ )
515
+ return
516
+
517
+ # Display results using the new results widget
518
+ self.angles_to_hkl_results.display_results(result)
519
+
520
+ # Update visualization
521
+ self.angles_to_hkl_visualizer.visualize_lab_system(
522
+ chi=params["chi"], phi=params["phi"], plot_k_basis=True, plot_basis=False
523
+ )
524
+ self.angles_to_hkl_visualizer.visualize_scattering_geometry(
525
+ scattering_angles=result
526
+ )
527
+
528
+ self.angles_to_hkl_unitcell_viz.visualize_unitcell()
529
+ self.angles_to_hkl_unitcell_viz.visualize_scattering_geometry(
530
+ scattering_angles=result
531
+ )
532
+
533
+ except Exception as e:
534
+ QMessageBox.critical(self, "Error", f"**Error** calculating HKL: {str(e)}")
535
+
536
+ @pyqtSlot()
537
+ def _update_fixed_angle_ui(self):
538
+ """Update UI based on which angle is fixed."""
539
+ is_chi_fixed = self.fix_chi_radio.isChecked()
540
+ self.chi_input.setEnabled(is_chi_fixed)
541
+ self.phi_input.setEnabled(not is_chi_fixed)
542
+
543
+ @pyqtSlot()
544
+ def calculate_angles(self):
545
+ """Calculate angles from HKL."""
546
+ if not self.calculator.is_initialized():
547
+ QMessageBox.warning(
548
+ self, "Warning", "Please initialize the calculator first!"
549
+ )
550
+ self.tab_widget.setCurrentIndex(0)
551
+ return
552
+
553
+ # Get parameters from the controls component
554
+ params = self.hkl_to_angles_controls.get_calculation_parameters()
555
+
556
+ # Calculate angles
557
+ result = self.calculator.calculate_angles(
558
+ H=params["H"],
559
+ K=params["K"],
560
+ L=params["L"],
561
+ fixed_angle=params["fixed_angle_value"],
562
+ fixed_angle_name=params["fixed_angle_name"],
563
+ )
564
+ if not result["success"]:
565
+ QMessageBox.warning(
566
+ self, "Warning", result.get("error", "No solution found")
567
+ )
568
+ return
569
+ self.hkl_to_angles_results.display_results(result)
570
+ # Update visualization with the first solution
571
+ self.hkl_to_angles_visualizer.visualize_lab_system(
572
+ is_clear=True, chi=result["chi"], phi=result["phi"], plot_basis=False, plot_k_basis=True
573
+ )
574
+ self.hkl_to_angles_visualizer.visualize_scattering_geometry(
575
+ scattering_angles=result, is_clear=False
576
+ )
577
+ self.hkl_to_angles_unitcell_viz.visualize_unitcell()
578
+ self.hkl_to_angles_unitcell_viz.visualize_scattering_geometry(
579
+ scattering_angles=result
580
+ )
581
+ @pyqtSlot()
582
+ def calculate_angles_tth_fixed(self):
583
+ """Calculate angles from HK with fixed tth."""
584
+ # Check if calculator is initialized
585
+ if not self.calculator.is_initialized():
586
+ QMessageBox.warning(
587
+ self, "Warning", "Please initialize the calculator first!"
588
+ )
589
+ self.tab_widget.setCurrentIndex(0)
590
+ return
591
+
592
+ # Get parameters from the controls component
593
+ params = self.hk_angles_controls.get_calculation_parameters()
594
+
595
+ # Extract values for calculation
596
+ tth = params["tth"]
597
+ H = params["H"]
598
+ K = params["K"]
599
+ L = params["L"]
600
+ fixed_index = params["fixed_index"]
601
+ fixed_angle_name = params["fixed_angle_name"]
602
+ fixed_angle_value = params["fixed_angle_value"]
603
+
604
+ # Calculate angles
605
+ result = self.calculator.calculate_angles_tth_fixed(
606
+ tth=tth,
607
+ H=H,
608
+ K=K,
609
+ L=L,
610
+ fixed_angle_name=fixed_angle_name,
611
+ fixed_angle=fixed_angle_value,
612
+ )
613
+ print("result", result)
614
+ if not result["success"]:
615
+ QMessageBox.warning(
616
+ self, "Warning", result.get("error", "No solution found")
617
+ )
618
+ return
619
+ self.hk_angles_results.display_results(result)
620
+
621
+ # Update visualization with the first solution
622
+ self.hk_fixed_tth_visualizer.visualize_lab_system(
623
+ is_clear=True, chi=result["chi"], phi=result["phi"], plot_basis=False, plot_k_basis=True
624
+ )
625
+ self.hk_fixed_tth_visualizer.visualize_scattering_geometry(
626
+ scattering_angles=result, is_clear=False
627
+ )
628
+ self.hk_fixed_tth_unitcell_viz.visualize_unitcell()
629
+ self.hk_fixed_tth_unitcell_viz.visualize_scattering_geometry(
630
+ scattering_angles=result
631
+ )
632
+ @pyqtSlot()
633
+ def on_angle_solution_selected(self, solution):
634
+ """Handle selection of a specific angle solution from the results widget."""
635
+ # Get HKL values from the controls component
636
+ params = self.hkl_to_angles_controls.get_calculation_parameters()
637
+
638
+ # Add HKL values to the solution for visualization
639
+ complete_solution = dict(solution)
640
+ complete_solution["H"] = params["H"]
641
+ complete_solution["K"] = params["K"]
642
+ complete_solution["L"] = params["L"]
643
+
644
+ # Update visualization with the selected solution
645
+ self.hkl_to_angles_visualizer.visualize_lab_system(
646
+ is_clear=True, chi=solution["chi"], phi=solution["phi"]
647
+ )
648
+ self.hkl_to_angles_visualizer.visualize_scattering_geometry(
649
+ scattering_angles=complete_solution, is_clear=False
650
+ )
651
+
652
+ @pyqtSlot()
653
+ def on_angle_solution_selected_tth(self, solution):
654
+ """Handle selection of a specific angle solution from the results widget."""
655
+ # Get HKL values from the controls component
656
+ params = self.hk_angles_controls.get_calculation_parameters()
657
+
658
+ # Add HKL values to the solution for visualization
659
+ complete_solution = dict(solution)
660
+ complete_solution["H"] = params["H"] if params["H"] is not None else 0.0
661
+ complete_solution["K"] = params["K"] if params["K"] is not None else 0.0
662
+ complete_solution["L"] = params["L"] if params["L"] is not None else 0.0
663
+
664
+ # Update visualization with the selected solution
665
+ self.hk_fixed_tth_visualizer.visualize_lab_system(
666
+ is_clear=True, chi=solution["chi"], phi=solution["phi"]
667
+ )
668
+ self.hk_fixed_tth_visualizer.visualize_scattering_geometry(
669
+ scattering_angles=complete_solution, is_clear=False
670
+ )
671
+
672
+ @pyqtSlot()
673
+ def clear_hkl_to_angles_results(self):
674
+ """Clear all results from the HKL to angles table."""
675
+ self.hkl_to_angles_results.clear_results()
676
+
677
+ @pyqtSlot()
678
+ def clear_hk_tth_fixed_results(self):
679
+ """Clear all results from the HK to angles (tth fixed) table."""
680
+ self.hk_angles_results.clear_results()
681
+
682
+ def get_module_instance(self):
683
+ """Get the backend module instance."""
684
+ return self.calculator
685
+
686
+ def get_state(self):
687
+ """Get the current state for session saving."""
688
+ return {
689
+ "lattice": {
690
+ "a": self.a_input.value(),
691
+ "b": self.b_input.value(),
692
+ "c": self.c_input.value(),
693
+ "alpha": self.alpha_input.value(),
694
+ "beta": self.beta_input.value(),
695
+ "gamma": self.gamma_input.value(),
696
+ },
697
+ "energy": self.energy_input.value(),
698
+ "file_path": self.file_path_input.text(),
699
+ "current_tab": self.tab_widget.currentIndex(),
700
+ }
701
+
702
+ def set_state(self, state):
703
+ """Restore tab state from saved session."""
704
+ try:
705
+ if "lattice" in state:
706
+ lattice = state["lattice"]
707
+ self.a_input.setValue(lattice.get("a", 5.0))
708
+ self.b_input.setValue(lattice.get("b", 5.0))
709
+ self.c_input.setValue(lattice.get("c", 5.0))
710
+ self.alpha_input.setValue(lattice.get("alpha", 90.0))
711
+ self.beta_input.setValue(lattice.get("beta", 90.0))
712
+ self.gamma_input.setValue(lattice.get("gamma", 90.0))
713
+
714
+ if "energy" in state:
715
+ self.energy_input.setValue(state["energy"])
716
+
717
+ if "file_path" in state and state["file_path"]:
718
+ self.file_path_input.setText(state["file_path"])
719
+
720
+ if "current_tab" in state:
721
+ self.tab_widget.setCurrentIndex(state["current_tab"])
722
+
723
+ return True
724
+ except Exception:
725
+ return False