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,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
|