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.
- advisor/__init__.py +3 -0
- advisor/__main__.py +7 -0
- advisor/app.py +40 -0
- advisor/controllers/__init__.py +6 -0
- advisor/controllers/app_controller.py +69 -0
- advisor/controllers/feature_controller.py +25 -0
- advisor/domain/__init__.py +23 -0
- advisor/domain/core/__init__.py +8 -0
- advisor/domain/core/lab.py +121 -0
- advisor/domain/core/lattice.py +79 -0
- advisor/domain/core/sample.py +101 -0
- advisor/domain/geometry.py +212 -0
- advisor/domain/unit_converter.py +82 -0
- advisor/features/__init__.py +6 -0
- advisor/features/scattering_geometry/controllers/__init__.py +5 -0
- advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
- advisor/features/scattering_geometry/domain/__init__.py +5 -0
- advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
- advisor/features/scattering_geometry/domain/core.py +516 -0
- advisor/features/scattering_geometry/ui/__init__.py +5 -0
- advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
- advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
- advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
- advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
- advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
- advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
- advisor/features/structure_factor/controllers/__init__.py +6 -0
- advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
- advisor/features/structure_factor/domain/__init__.py +6 -0
- advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
- advisor/features/structure_factor/ui/__init__.py +6 -0
- advisor/features/structure_factor/ui/components/__init__.py +12 -0
- advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
- advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
- advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
- advisor/resources/__init__.py +0 -0
- advisor/resources/config/app_config.json +14 -0
- advisor/resources/config/tips.json +4 -0
- advisor/resources/data/nacl.cif +111 -0
- advisor/resources/icons/bz_caculator.jpg +0 -0
- advisor/resources/icons/bz_calculator.png +0 -0
- advisor/resources/icons/minus.svg +3 -0
- advisor/resources/icons/placeholder.png +0 -0
- advisor/resources/icons/plus.svg +3 -0
- advisor/resources/icons/reset.png +0 -0
- advisor/resources/icons/sf_calculator.jpg +0 -0
- advisor/resources/icons/sf_calculator.png +0 -0
- advisor/resources/icons.qrc +6 -0
- advisor/resources/qss/styles.qss +348 -0
- advisor/resources/resources_rc.py +83 -0
- advisor/ui/__init__.py +7 -0
- advisor/ui/init_window.py +566 -0
- advisor/ui/main_window.py +174 -0
- advisor/ui/tab_interface.py +44 -0
- advisor/ui/tips.py +30 -0
- advisor/ui/utils/__init__.py +6 -0
- advisor/ui/utils/readcif.py +129 -0
- advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
- advisor/ui/visualizers/__init__.py +8 -0
- advisor/ui/visualizers/coordinate_visualizer.py +203 -0
- advisor/ui/visualizers/scattering_visualizer.py +301 -0
- advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
- advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
- advisor/ui/visualizers/unitcell_visualizer.py +518 -0
- advisor_scattering-0.5.0.dist-info/METADATA +122 -0
- advisor_scattering-0.5.0.dist-info/RECORD +69 -0
- advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
- advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
- 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
|
+
|