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,526 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # pylint: disable=no-name-in-module, import-error
4
+ from PyQt5.QtWidgets import (
5
+ QWidget,
6
+ QVBoxLayout,
7
+ QHBoxLayout,
8
+ QFormLayout,
9
+ QGroupBox,
10
+ QLabel,
11
+ QPushButton,
12
+ QDoubleSpinBox,
13
+ QSpinBox,
14
+ QTableWidget,
15
+ QTableWidgetItem,
16
+ QHeaderView,
17
+ QRadioButton,
18
+ QButtonGroup,
19
+ QFileDialog,
20
+ QMessageBox,
21
+ )
22
+ from PyQt5.QtCore import Qt, pyqtSignal
23
+ from PyQt5.QtGui import QColor, QBrush
24
+ import csv
25
+ import os
26
+ import numpy as np
27
+
28
+ class RangeInputWidget(QWidget):
29
+ """Widget for input range (start, end, num_points)."""
30
+
31
+ def __init__(self, label, parent=None):
32
+ super().__init__(parent)
33
+
34
+ # Main layout
35
+ layout = QFormLayout(self)
36
+ layout.setContentsMargins(0, 0, 0, 0)
37
+
38
+ # Create group with label but remove border styling
39
+ group = QGroupBox(label)
40
+ group.setStyleSheet("QGroupBox { border: none; font-weight: bold; }")
41
+ group_layout = QHBoxLayout(group)
42
+ group_layout.setContentsMargins(10, 10, 10, 10)
43
+
44
+ # Start value input with label
45
+ start_widget = QWidget()
46
+ start_layout = QFormLayout(start_widget)
47
+ start_layout.setContentsMargins(0, 0, 0, 0)
48
+
49
+ self.start_input = QDoubleSpinBox()
50
+ self.start_input.setRange(-100.0, 100)
51
+ self.start_input.setDecimals(3)
52
+ self.start_input.setValue(0.0)
53
+ start_layout.addRow("Start:", self.start_input)
54
+
55
+ group_layout.addWidget(start_widget)
56
+
57
+ # End value input with label
58
+ end_widget = QWidget()
59
+ end_layout = QFormLayout(end_widget)
60
+ end_layout.setContentsMargins(0, 0, 0, 0)
61
+
62
+ self.end_input = QDoubleSpinBox()
63
+ self.end_input.setRange(-100.0, 100)
64
+ self.end_input.setDecimals(3)
65
+ self.end_input.setValue(-0.3)
66
+ end_layout.addRow("End:", self.end_input)
67
+
68
+ group_layout.addWidget(end_widget)
69
+
70
+ layout.addRow(group)
71
+
72
+ def get_range(self):
73
+ """Get start and end values."""
74
+ return (
75
+ self.start_input.value(),
76
+ self.end_input.value(),
77
+ )
78
+
79
+ def set_range(self, start, end):
80
+ """Set start and end values."""
81
+ self.start_input.setValue(start)
82
+ self.end_input.setValue(end)
83
+
84
+ def set_enabled(self, enabled):
85
+ """Enable or disable widget."""
86
+ self.start_input.setEnabled(enabled)
87
+ self.end_input.setEnabled(enabled)
88
+
89
+ def set_visible(self, visible):
90
+ """Show or hide widget."""
91
+ self.setVisible(visible)
92
+
93
+
94
+ class HKLScanControls(QWidget):
95
+ """Widget for HKL scan controls."""
96
+
97
+ # Signal emitted when calculate button is clicked
98
+ calculateClicked = pyqtSignal()
99
+
100
+ def __init__(self, parent=None):
101
+ super().__init__(parent)
102
+
103
+ # Main layout
104
+ main_layout = QVBoxLayout(self)
105
+
106
+ # Unified Fixed Angles panel
107
+ fixed_angles_group = QGroupBox("Fixed Angles")
108
+ fixed_angles_layout = QVBoxLayout(fixed_angles_group)
109
+
110
+ # Top row: Fix χ/Fix φ buttons
111
+ angle_selection = QWidget()
112
+ angle_selection_layout = QHBoxLayout(angle_selection)
113
+ angle_selection_layout.setContentsMargins(0, 0, 0, 0)
114
+
115
+ self.fix_chi_btn = QPushButton("Fix χ")
116
+ self.fix_phi_btn = QPushButton("Fix φ")
117
+
118
+ # Make buttons checkable for toggle behavior
119
+ for btn in (self.fix_chi_btn, self.fix_phi_btn):
120
+ btn.setCheckable(True)
121
+
122
+ self.fix_chi_btn.setChecked(True) # Default to fixed chi
123
+
124
+ # Create a button group for mutual exclusion
125
+ self.angle_button_group = QButtonGroup(self)
126
+ self.angle_button_group.addButton(self.fix_chi_btn)
127
+ self.angle_button_group.addButton(self.fix_phi_btn)
128
+
129
+ angle_selection_layout.addWidget(self.fix_chi_btn)
130
+ angle_selection_layout.addWidget(self.fix_phi_btn)
131
+
132
+ fixed_angles_layout.addWidget(angle_selection)
133
+
134
+ # Bottom row: tth on left, chi/phi angles on right
135
+ angle_values = QWidget()
136
+ angle_values_layout = QHBoxLayout(angle_values)
137
+ angle_values_layout.setContentsMargins(0, 0, 0, 0)
138
+
139
+ # tth input (left side)
140
+ self.tth_widget = QWidget()
141
+ tth_layout = QFormLayout(self.tth_widget)
142
+ tth_layout.setContentsMargins(0, 0, 0, 0)
143
+ self.tth_input = QDoubleSpinBox()
144
+ self.tth_input.setRange(0.0, 180.0)
145
+ self.tth_input.setValue(150.0)
146
+ self.tth_input.setSuffix(" °")
147
+ tth_layout.addRow("tth:", self.tth_input)
148
+ angle_values_layout.addWidget(self.tth_widget)
149
+
150
+ # Chi input
151
+ self.chi_widget = QWidget()
152
+ chi_layout = QFormLayout(self.chi_widget)
153
+ chi_layout.setContentsMargins(0, 0, 0, 0)
154
+ self.chi_input = QDoubleSpinBox()
155
+ self.chi_input.setRange(-180.0, 180.0)
156
+ self.chi_input.setValue(0.0)
157
+ self.chi_input.setSuffix(" °")
158
+ chi_layout.addRow("χ:", self.chi_input)
159
+ angle_values_layout.addWidget(self.chi_widget)
160
+
161
+ # Phi input
162
+ self.phi_widget = QWidget()
163
+ phi_layout = QFormLayout(self.phi_widget)
164
+ phi_layout.setContentsMargins(0, 0, 0, 0)
165
+ self.phi_input = QDoubleSpinBox()
166
+ self.phi_input.setRange(-180.0, 180.0)
167
+ self.phi_input.setValue(0.0)
168
+ self.phi_input.setSuffix(" °")
169
+ phi_layout.addRow("φ:", self.phi_input)
170
+ angle_values_layout.addWidget(self.phi_widget)
171
+
172
+ fixed_angles_layout.addWidget(angle_values)
173
+ main_layout.addWidget(fixed_angles_group)
174
+
175
+ # HKL index selection
176
+ hkl_group = QGroupBox("Choose a plane")
177
+ hkl_layout = QVBoxLayout(hkl_group)
178
+
179
+ # Plane selection - using plane-based naming like Structure Factor Calculator
180
+ index_selection = QWidget()
181
+ index_layout = QHBoxLayout(index_selection)
182
+ index_layout.setContentsMargins(0, 0, 0, 0)
183
+
184
+ self.hk_plane_toggle = QPushButton("HK plane")
185
+ self.hl_plane_toggle = QPushButton("HL plane")
186
+ self.kl_plane_toggle = QPushButton("KL plane")
187
+
188
+ # Make buttons checkable for toggle behavior
189
+ for btn in (self.hk_plane_toggle, self.hl_plane_toggle, self.kl_plane_toggle):
190
+ btn.setCheckable(True)
191
+
192
+ self.hk_plane_toggle.setChecked(True) # Default to HK plane (L deactivated)
193
+
194
+ # Create a button group for mutual exclusion
195
+ index_button_group = QButtonGroup(self)
196
+ index_button_group.addButton(self.hk_plane_toggle)
197
+ index_button_group.addButton(self.hl_plane_toggle)
198
+ index_button_group.addButton(self.kl_plane_toggle)
199
+
200
+ index_layout.addWidget(self.hk_plane_toggle)
201
+ index_layout.addWidget(self.hl_plane_toggle)
202
+ index_layout.addWidget(self.kl_plane_toggle)
203
+
204
+ hkl_layout.addWidget(index_selection)
205
+
206
+ # Create range widgets for H, K, L
207
+ ranges_widget = QWidget()
208
+ ranges_layout = QVBoxLayout(ranges_widget)
209
+ ranges_layout.setContentsMargins(0, 0, 0, 0)
210
+
211
+ self.h_range = RangeInputWidget("H Range")
212
+ self.k_range = RangeInputWidget("K Range")
213
+ self.l_range = RangeInputWidget("L Range")
214
+
215
+ ranges_layout.addWidget(self.h_range)
216
+ ranges_layout.addWidget(self.k_range)
217
+ ranges_layout.addWidget(self.l_range)
218
+
219
+ hkl_layout.addWidget(ranges_widget)
220
+
221
+ # Number of points
222
+ points_widget = QWidget()
223
+ points_layout = QFormLayout(points_widget)
224
+ points_layout.setContentsMargins(0, 0, 0, 0)
225
+
226
+ self.num_points = QSpinBox()
227
+ self.num_points.setRange(2, 100)
228
+ self.num_points.setValue(10)
229
+ points_layout.addRow("Number of points:", self.num_points)
230
+
231
+ hkl_layout.addWidget(points_widget)
232
+
233
+ main_layout.addWidget(hkl_group)
234
+
235
+ # Calculate button
236
+ self.calculate_button = QPushButton("Calculate")
237
+ self.calculate_button.clicked.connect(self.calculateClicked.emit)
238
+ self.calculate_button.setObjectName("calculateButton")
239
+ main_layout.addWidget(self.calculate_button)
240
+
241
+ # Connect signals
242
+ self.hk_plane_toggle.clicked.connect(lambda: self._set_active_plane("HK"))
243
+ self.hl_plane_toggle.clicked.connect(lambda: self._set_active_plane("HL"))
244
+ self.kl_plane_toggle.clicked.connect(lambda: self._set_active_plane("KL"))
245
+ self.fix_chi_btn.clicked.connect(lambda: self._set_active_fixed_angle("chi"))
246
+ self.fix_phi_btn.clicked.connect(lambda: self._set_active_fixed_angle("phi"))
247
+
248
+ # Initialize widget states
249
+ self._set_active_plane("HK") # Set initial plane and apply styling
250
+ self._set_active_fixed_angle("chi") # Set initial fixed angle and apply styling
251
+
252
+ def _update_widget_states(self):
253
+ """Update visibility of widgets based on current plane selection.
254
+
255
+ HK plane: H and K ranges visible, L range hidden
256
+ HL plane: H and L ranges visible, K range hidden
257
+ KL plane: K and L ranges visible, H range hidden
258
+ """
259
+ if self.hk_plane_toggle.isChecked():
260
+ # HK plane: H and K ranges visible, L range hidden
261
+ self.h_range.set_visible(True)
262
+ self.k_range.set_visible(True)
263
+ self.l_range.set_visible(False)
264
+ elif self.hl_plane_toggle.isChecked():
265
+ # HL plane: H and L ranges visible, K range hidden
266
+ self.h_range.set_visible(True)
267
+ self.k_range.set_visible(False)
268
+ self.l_range.set_visible(True)
269
+ elif self.kl_plane_toggle.isChecked():
270
+ # KL plane: K and L ranges visible, H range hidden
271
+ self.h_range.set_visible(False)
272
+ self.k_range.set_visible(True)
273
+ self.l_range.set_visible(True)
274
+
275
+ def _update_fixed_angle_ui(self):
276
+ """Update UI based on which angle is fixed.
277
+
278
+ If chi is fixed: Show chi input, hide phi input
279
+ If phi is fixed: Show phi input, hide chi input
280
+ """
281
+ is_chi_fixed = self.fix_chi_btn.isChecked()
282
+ # set both invisible first
283
+ self.chi_widget.setVisible(False)
284
+ self.phi_widget.setVisible(False)
285
+ self.chi_widget.setVisible(is_chi_fixed)
286
+ self.phi_widget.setVisible(not is_chi_fixed)
287
+ self.phi_input.setEnabled(not is_chi_fixed)
288
+ self.chi_input.setEnabled(is_chi_fixed)
289
+
290
+ def _update_fixed_angle_styles(self, active: str):
291
+ """Update fixed angle button colors based on active selection."""
292
+ mapping = {
293
+ "chi": self.fix_chi_btn,
294
+ "phi": self.fix_phi_btn,
295
+ }
296
+ for name, btn in mapping.items():
297
+ if name == active:
298
+ btn.setChecked(True)
299
+ btn.setProperty("class", "activeToggle")
300
+ else:
301
+ btn.setChecked(False)
302
+ btn.setProperty("class", "inactiveToggle")
303
+ # Force style refresh
304
+ btn.style().unpolish(btn)
305
+ btn.style().polish(btn)
306
+
307
+ def _set_active_fixed_angle(self, angle: str):
308
+ """Set the active fixed angle and update widget states and styling."""
309
+ angle = angle.lower()
310
+ self._update_fixed_angle_styles(angle)
311
+ self._update_fixed_angle_ui()
312
+
313
+ def _update_toggle_styles(self, active: str):
314
+ """Update toggle button colors based on active plane."""
315
+ mapping = {
316
+ "HK": self.hk_plane_toggle,
317
+ "HL": self.hl_plane_toggle,
318
+ "KL": self.kl_plane_toggle,
319
+ }
320
+ for name, btn in mapping.items():
321
+ if name == active:
322
+ btn.setChecked(True)
323
+ btn.setProperty("class", "activeToggle")
324
+ else:
325
+ btn.setChecked(False)
326
+ btn.setProperty("class", "inactiveToggle")
327
+ # Force style refresh
328
+ btn.style().unpolish(btn)
329
+ btn.style().polish(btn)
330
+
331
+ def _set_active_plane(self, plane: str):
332
+ """Set the active plane and update widget states and styling."""
333
+ plane = plane.upper()
334
+ self._update_toggle_styles(plane)
335
+ self._update_widget_states()
336
+
337
+ def get_scan_parameters(self):
338
+ """Get parameters for scan."""
339
+ # Get deactivated index based on plane selection
340
+ # HK plane means L is fixed (deactivated)
341
+ # HL plane means K is fixed (deactivated)
342
+ # KL plane means H is fixed (deactivated)
343
+ deactivated_index = None
344
+ if self.hk_plane_toggle.isChecked():
345
+ deactivated_index = "L" # HK plane: L is fixed
346
+ elif self.hl_plane_toggle.isChecked():
347
+ deactivated_index = "K" # HL plane: K is fixed
348
+ elif self.kl_plane_toggle.isChecked():
349
+ deactivated_index = "H" # KL plane: H is fixed
350
+
351
+ # Get fixed angle
352
+ fixed_angle_name = "chi" if self.fix_chi_btn.isChecked() else "phi"
353
+ fixed_angle_value = (
354
+ self.chi_input.value()
355
+ if self.fix_chi_btn.isChecked()
356
+ else self.phi_input.value()
357
+ )
358
+
359
+ # Get ranges
360
+ h_start, h_end = self.h_range.get_range()
361
+ k_start, k_end = self.k_range.get_range()
362
+ l_start, l_end = self.l_range.get_range()
363
+
364
+ return {
365
+ "tth": self.tth_input.value(),
366
+ "deactivated_index": deactivated_index,
367
+ "fixed_angle_name": fixed_angle_name,
368
+ "fixed_angle": fixed_angle_value,
369
+ "start_points": (h_start, k_start, l_start),
370
+ "end_points": (h_end, k_end, l_end),
371
+ "num_points": self.num_points.value(),
372
+ }
373
+
374
+
375
+ class HKLScanResultsTable(QTableWidget):
376
+ """Table to display HKL scan results with multiple solutions."""
377
+
378
+ def __init__(self, parent=None):
379
+ super().__init__(parent)
380
+
381
+ # Set up table
382
+ self.setColumnCount(7)
383
+ self.setHorizontalHeaderLabels(["H", "K", "L", "θ (°)", "φ (°)", "χ (°)", "β (°)"])
384
+ self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
385
+
386
+ # Hide vertical header (row numbers)
387
+ self.verticalHeader().setVisible(False)
388
+
389
+ # Define colors for alternating groups white and gray
390
+ self.group_colors = [
391
+ QColor(255, 255, 255), # White
392
+ QColor(230, 230, 240), # Gray
393
+ QColor(166, 45, 45), # Red
394
+ ]
395
+
396
+ # Enable sorting
397
+ self.setSortingEnabled(True)
398
+
399
+ # Add export button
400
+ self.layout_wrapper = QVBoxLayout()
401
+ self.layout_wrapper.setContentsMargins(0, 0, 0, 0)
402
+
403
+ self.export_button = QPushButton("Export to CSV")
404
+ self.export_button.clicked.connect(self.export_to_csv)
405
+ self.export_button.setEnabled(False) # Initially disabled until we have results
406
+
407
+ self.layout_wrapper.addWidget(self)
408
+ self.layout_wrapper.addWidget(self.export_button)
409
+
410
+ # Store the last results for export
411
+ self.last_results = None
412
+
413
+ def display_results(self, results):
414
+ """Display results in the table."""
415
+ self.setSortingEnabled(False) # Temporarily disable sorting
416
+ self.setRowCount(0) # Clear table
417
+
418
+ # Check if we have results
419
+ if not results or not results.get("success", False):
420
+ self.export_button.setEnabled(False)
421
+ self.last_results = None
422
+ return
423
+
424
+ # Store results for later export
425
+ self.last_results = results
426
+ # Get data from results
427
+ h_values = results["H"]
428
+ k_values = results["K"]
429
+ l_values = results["L"]
430
+ tth_values = results["tth"]
431
+ theta_values = results["theta"]
432
+ phi_values = results["phi"]
433
+ chi_values = results["chi"]
434
+
435
+ # Add a row for each result with alternating colors
436
+ for i in range(len(h_values)):
437
+ row_position = self.rowCount()
438
+ self.insertRow(row_position)
439
+
440
+ # Add HKL values
441
+ self.setItem(row_position, 0, QTableWidgetItem(f"{h_values[i]:.4f}"))
442
+ self.setItem(row_position, 1, QTableWidgetItem(f"{k_values[i]:.4f}"))
443
+ self.setItem(row_position, 2, QTableWidgetItem(f"{l_values[i]:.4f}"))
444
+
445
+ # Add angle values
446
+ self.setItem(row_position, 3, QTableWidgetItem(f"{theta_values[i]:.1f}"))
447
+ self.setItem(row_position, 4, QTableWidgetItem(f"{phi_values[i]:.1f}"))
448
+ self.setItem(row_position, 5, QTableWidgetItem(f"{chi_values[i]:.1f}"))
449
+ self.setItem(row_position, 6, QTableWidgetItem(f"{tth_values[0]-theta_values[i]:.1f}"))
450
+
451
+ # Apply alternating row colors
452
+ row_color = self.group_colors[i % 2] if results["feasible"][i] else self.group_colors[2]
453
+ for col in range(self.columnCount()):
454
+ item = self.item(row_position, col)
455
+ if item:
456
+ item.setBackground(QBrush(row_color))
457
+
458
+ # Re-enable sorting and export button
459
+ self.setSortingEnabled(True)
460
+ self.export_button.setEnabled(True)
461
+
462
+
463
+
464
+ def export_to_csv(self):
465
+ """Export results to a CSV file."""
466
+ if not self.last_results:
467
+ QMessageBox.warning(self, "Export Error", "No results to export.")
468
+ return
469
+
470
+ # Open file dialog to get save location
471
+ file_path, _ = QFileDialog.getSaveFileName(
472
+ self, "Save Results", "", "CSV Files (*.csv);;All Files (*)"
473
+ )
474
+
475
+ if not file_path:
476
+ return # User cancelled
477
+
478
+ # Add .csv extension if not present
479
+ if not file_path.endswith(".csv"):
480
+ file_path += ".csv"
481
+
482
+ try:
483
+ with open(file_path, "w", newline="") as csvfile:
484
+ writer = csv.writer(csvfile)
485
+
486
+ # Write header
487
+ writer.writerow(
488
+ [
489
+ "H",
490
+ "K",
491
+ "L",
492
+ "tth (deg)",
493
+ "theta (deg)",
494
+ "phi (deg)",
495
+ "chi (deg)",
496
+ ]
497
+ )
498
+
499
+ # Write data
500
+ for i in range(len(self.last_results["tth"])):
501
+ writer.writerow(
502
+ [
503
+ f"{self.last_results['H'][i]:.6f}",
504
+ f"{self.last_results['K'][i]:.6f}",
505
+ f"{self.last_results['L'][i]:.6f}",
506
+ f"{self.last_results['tth'][i]:.6f}",
507
+ f"{self.last_results['theta'][i]:.6f}",
508
+ f"{self.last_results['phi'][i]:.6f}",
509
+ f"{self.last_results['chi'][i]:.6f}",
510
+ ]
511
+ )
512
+
513
+ QMessageBox.information(
514
+ self, "Export Success", f"Results exported to {file_path}"
515
+ )
516
+ except Exception as e:
517
+ QMessageBox.critical(
518
+ self, "Export Error", f"Error exporting results: {str(e)}"
519
+ )
520
+
521
+ def get_widget(self):
522
+ """Return the widget containing the table and export button."""
523
+ container = QWidget()
524
+ container.setLayout(self.layout_wrapper)
525
+ return container
526
+