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,430 @@
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
+ QTableWidget,
14
+ QTableWidgetItem,
15
+ QHeaderView,
16
+ QButtonGroup,
17
+ QMessageBox,
18
+ )
19
+ from PyQt5.QtCore import Qt, pyqtSignal
20
+ from PyQt5.QtGui import QColor, QBrush
21
+
22
+
23
+ class HKAnglesControls(QWidget):
24
+ """Widget for HK to Angles calculation controls with fixed tth."""
25
+
26
+ # Signal emitted when calculate button is clicked
27
+ calculateClicked = pyqtSignal()
28
+
29
+ def __init__(self, parent=None):
30
+ super().__init__(parent)
31
+
32
+ # Main layout
33
+ main_layout = QVBoxLayout(self)
34
+
35
+
36
+
37
+ # HKL indices group
38
+ hkl_group = QGroupBox("HKL Indices")
39
+ hkl_layout = QFormLayout(hkl_group)
40
+
41
+ # Plane selection buttons
42
+ plane_selection = QWidget()
43
+ plane_layout = QHBoxLayout(plane_selection)
44
+ plane_layout.setContentsMargins(0, 0, 0, 0)
45
+
46
+ self.hk_plane_btn = QPushButton("HK plane")
47
+ self.hl_plane_btn = QPushButton("HL plane")
48
+ self.kl_plane_btn = QPushButton("KL plane")
49
+
50
+ # Make buttons checkable for toggle behavior
51
+ for btn in (self.hk_plane_btn, self.hl_plane_btn, self.kl_plane_btn):
52
+ btn.setCheckable(True)
53
+
54
+ self.hk_plane_btn.setChecked(True) # Default to HK plane (L fixed)
55
+
56
+ # Create a button group for mutual exclusion
57
+ self.hkl_plane_button_group = QButtonGroup(self)
58
+ self.hkl_plane_button_group.addButton(self.hk_plane_btn)
59
+ self.hkl_plane_button_group.addButton(self.hl_plane_btn)
60
+ self.hkl_plane_button_group.addButton(self.kl_plane_btn)
61
+
62
+ plane_layout.addWidget(self.hk_plane_btn)
63
+ plane_layout.addWidget(self.hl_plane_btn)
64
+ plane_layout.addWidget(self.kl_plane_btn)
65
+ # stretch buttons to be evenly distributed
66
+ plane_layout.addStretch()
67
+ hkl_layout.addRow("Plane:", plane_selection)
68
+ # Create HKL inputs
69
+ hkl_inputs_widget = QWidget()
70
+ hkl_inputs_layout = QHBoxLayout(hkl_inputs_widget)
71
+ hkl_inputs_layout.setContentsMargins(0, 0, 0, 0)
72
+
73
+ # H input row
74
+ self.h_row = QWidget()
75
+ h_layout = QHBoxLayout(self.h_row)
76
+ h_layout.setContentsMargins(0, 0, 0, 0)
77
+ h_form = QWidget()
78
+ h_form_layout = QFormLayout(h_form)
79
+ h_form_layout.setContentsMargins(0, 0, 0, 0)
80
+ self.H_input = QDoubleSpinBox()
81
+ self.H_input.setRange(-100.0, 100)
82
+ self.H_input.setDecimals(4)
83
+ self.H_input.setValue(0.15)
84
+ h_form_layout.addRow("H:", self.H_input)
85
+ h_layout.addWidget(h_form)
86
+ hkl_inputs_layout.addWidget(self.h_row)
87
+
88
+ # K input row
89
+ self.k_row = QWidget()
90
+ k_layout = QHBoxLayout(self.k_row)
91
+ k_layout.setContentsMargins(0, 0, 0, 0)
92
+ k_form = QWidget()
93
+ k_form_layout = QFormLayout(k_form)
94
+ k_form_layout.setContentsMargins(0, 0, 0, 0)
95
+ self.K_input = QDoubleSpinBox()
96
+ self.K_input.setRange(-100, 100)
97
+ self.K_input.setDecimals(4)
98
+ self.K_input.setValue(0.1)
99
+ k_form_layout.addRow("K:", self.K_input)
100
+ k_layout.addWidget(k_form)
101
+ hkl_inputs_layout.addWidget(self.k_row)
102
+
103
+ # L input row
104
+ self.l_row = QWidget()
105
+ l_layout = QHBoxLayout(self.l_row)
106
+ l_layout.setContentsMargins(0, 0, 0, 0)
107
+ l_form = QWidget()
108
+ l_form_layout = QFormLayout(l_form)
109
+ l_form_layout.setContentsMargins(0, 0, 0, 0)
110
+ self.L_input = QDoubleSpinBox()
111
+ self.L_input.setRange(-100, 100)
112
+ self.L_input.setDecimals(4)
113
+ self.L_input.setValue(-0.5)
114
+ l_form_layout.addRow("L:", self.L_input)
115
+ l_layout.addWidget(l_form)
116
+ hkl_inputs_layout.addWidget(self.l_row)
117
+
118
+ hkl_layout.addRow(hkl_inputs_widget)
119
+ main_layout.addWidget(hkl_group)
120
+
121
+
122
+ # Unified Fixed Angles panel
123
+ fixed_angles_group = QGroupBox("Fixed Angles")
124
+ fixed_angles_layout = QVBoxLayout(fixed_angles_group)
125
+
126
+ # Top row: Fix χ/Fix φ buttons
127
+ angle_selection = QWidget()
128
+ angle_selection_layout = QHBoxLayout(angle_selection)
129
+ angle_selection_layout.setContentsMargins(0, 0, 0, 0)
130
+
131
+ self.fix_chi_btn = QPushButton("Fix χ")
132
+ self.fix_phi_btn = QPushButton("Fix φ")
133
+
134
+ # Make buttons checkable for toggle behavior
135
+ for btn in (self.fix_chi_btn, self.fix_phi_btn):
136
+ btn.setCheckable(True)
137
+
138
+ self.fix_chi_btn.setChecked(True) # Default to fixed chi
139
+
140
+ # Create a button group for mutual exclusion
141
+ self.angle_button_group = QButtonGroup(self)
142
+ self.angle_button_group.addButton(self.fix_chi_btn)
143
+ self.angle_button_group.addButton(self.fix_phi_btn)
144
+
145
+ angle_selection_layout.addWidget(self.fix_chi_btn)
146
+ angle_selection_layout.addWidget(self.fix_phi_btn)
147
+
148
+ fixed_angles_layout.addWidget(angle_selection)
149
+
150
+ # Bottom row: tth on left, chi/phi angles on right
151
+ angle_values = QWidget()
152
+ angle_values_layout = QHBoxLayout(angle_values)
153
+ angle_values_layout.setContentsMargins(0, 0, 0, 0)
154
+
155
+ # tth input (left side)
156
+ self.tth_widget = QWidget()
157
+ tth_layout = QFormLayout(self.tth_widget)
158
+ tth_layout.setContentsMargins(0, 0, 0, 0)
159
+ self.tth_input = QDoubleSpinBox()
160
+ self.tth_input.setRange(0.0, 180.0)
161
+ self.tth_input.setValue(150.0)
162
+ self.tth_input.setSuffix(" °")
163
+ tth_layout.addRow("tth:", self.tth_input)
164
+ angle_values_layout.addWidget(self.tth_widget)
165
+
166
+ # Chi input
167
+ self.chi_widget = QWidget()
168
+ chi_layout = QFormLayout(self.chi_widget)
169
+ chi_layout.setContentsMargins(0, 0, 0, 0)
170
+ self.chi_input = QDoubleSpinBox()
171
+ self.chi_input.setRange(-180.0, 180.0)
172
+ self.chi_input.setValue(0.0)
173
+ self.chi_input.setSuffix(" °")
174
+ chi_layout.addRow("χ:", self.chi_input)
175
+ angle_values_layout.addWidget(self.chi_widget)
176
+
177
+ # Phi input
178
+ self.phi_widget = QWidget()
179
+ phi_layout = QFormLayout(self.phi_widget)
180
+ phi_layout.setContentsMargins(0, 0, 0, 0)
181
+ self.phi_input = QDoubleSpinBox()
182
+ self.phi_input.setRange(-180.0, 180.0)
183
+ self.phi_input.setValue(0.0)
184
+ self.phi_input.setSuffix(" °")
185
+ phi_layout.addRow("φ:", self.phi_input)
186
+ angle_values_layout.addWidget(self.phi_widget)
187
+
188
+ fixed_angles_layout.addWidget(angle_values)
189
+
190
+ main_layout.addWidget(fixed_angles_group)
191
+
192
+
193
+ # Calculate button
194
+ self.calculate_button = QPushButton("Calculate Angles")
195
+ self.calculate_button.clicked.connect(self.calculateClicked.emit)
196
+ self.calculate_button.setObjectName("calculateButton")
197
+ main_layout.addWidget(self.calculate_button)
198
+
199
+ # Connect signals
200
+ self.hk_plane_btn.clicked.connect(lambda: self._set_active_plane("HK"))
201
+ self.hl_plane_btn.clicked.connect(lambda: self._set_active_plane("HL"))
202
+ self.kl_plane_btn.clicked.connect(lambda: self._set_active_plane("KL"))
203
+ self.fix_chi_btn.clicked.connect(lambda: self._set_active_fixed_angle("chi"))
204
+ self.fix_phi_btn.clicked.connect(lambda: self._set_active_fixed_angle("phi"))
205
+
206
+ # Initialize widget states
207
+ self._set_active_plane("HK") # Set initial plane and apply styling
208
+ self._set_active_fixed_angle("chi") # Set initial fixed angle and apply styling
209
+
210
+ def _update_hkl_visibility(self):
211
+ """Update visibility of HKL inputs based on current plane selection.
212
+
213
+ HK plane: H and K inputs visible, L input hidden
214
+ HL plane: H and L inputs visible, K input hidden
215
+ KL plane: K and L inputs visible, H input hidden
216
+ """
217
+ if self.hk_plane_btn.isChecked():
218
+ # HK plane: H and K visible, L hidden
219
+ self.h_row.setVisible(True)
220
+ self.k_row.setVisible(True)
221
+ self.l_row.setVisible(False)
222
+ elif self.hl_plane_btn.isChecked():
223
+ # HL plane: H and L visible, K hidden
224
+ self.h_row.setVisible(True)
225
+ self.k_row.setVisible(False)
226
+ self.l_row.setVisible(True)
227
+ elif self.kl_plane_btn.isChecked():
228
+ # KL plane: K and L visible, H hidden
229
+ self.h_row.setVisible(False)
230
+ self.k_row.setVisible(True)
231
+ self.l_row.setVisible(True)
232
+
233
+ def _update_fixed_angle_ui(self):
234
+ """Update UI based on which angle is fixed.
235
+
236
+ If chi is fixed: Show chi input, hide phi input
237
+ If phi is fixed: Show phi input, hide chi input
238
+ """
239
+ is_chi_fixed = self.fix_chi_btn.isChecked()
240
+ self.chi_widget.setVisible(is_chi_fixed)
241
+ self.phi_widget.setVisible(not is_chi_fixed)
242
+
243
+ def _update_plane_styles(self, active: str):
244
+ """Update plane button colors based on active plane."""
245
+ mapping = {
246
+ "HK": self.hk_plane_btn,
247
+ "HL": self.hl_plane_btn,
248
+ "KL": self.kl_plane_btn,
249
+ }
250
+ for name, btn in mapping.items():
251
+ if name == active:
252
+ btn.setChecked(True)
253
+ btn.setProperty("class", "activeToggle")
254
+ else:
255
+ btn.setChecked(False)
256
+ btn.setProperty("class", "inactiveToggle")
257
+ # Force style refresh
258
+ btn.style().unpolish(btn)
259
+ btn.style().polish(btn)
260
+
261
+ def _update_fixed_angle_styles(self, active: str):
262
+ """Update fixed angle button colors based on active selection."""
263
+ mapping = {
264
+ "chi": self.fix_chi_btn,
265
+ "phi": self.fix_phi_btn,
266
+ }
267
+ for name, btn in mapping.items():
268
+ if name == active:
269
+ btn.setChecked(True)
270
+ btn.setProperty("class", "activeToggle")
271
+ else:
272
+ btn.setChecked(False)
273
+ btn.setProperty("class", "inactiveToggle")
274
+ # Force style refresh
275
+ btn.style().unpolish(btn)
276
+ btn.style().polish(btn)
277
+
278
+ def _set_active_plane(self, plane: str):
279
+ """Set the active plane and update widget states and styling."""
280
+ plane = plane.upper()
281
+ self._update_plane_styles(plane)
282
+ self._update_hkl_visibility()
283
+
284
+ def _set_active_fixed_angle(self, angle: str):
285
+ """Set the active fixed angle and update widget states and styling."""
286
+ angle = angle.lower()
287
+ self._update_fixed_angle_styles(angle)
288
+ self._update_fixed_angle_ui()
289
+
290
+ def get_calculation_parameters(self):
291
+ """Get parameters for angle calculation."""
292
+ # Get fixed index based on plane selection
293
+ # HK plane means L is fixed, HL plane means K is fixed, KL plane means H is fixed
294
+ fixed_index = None
295
+ if self.hk_plane_btn.isChecked():
296
+ fixed_index = "L" # HK plane: L is fixed
297
+ elif self.hl_plane_btn.isChecked():
298
+ fixed_index = "K" # HL plane: K is fixed
299
+ elif self.kl_plane_btn.isChecked():
300
+ fixed_index = "H" # KL plane: H is fixed
301
+
302
+ # Get fixed angle
303
+ fixed_angle_name = "chi" if self.fix_chi_btn.isChecked() else "phi"
304
+ fixed_angle_value = (
305
+ self.chi_input.value()
306
+ if self.fix_chi_btn.isChecked()
307
+ else self.phi_input.value()
308
+ )
309
+
310
+ # Get HKL values (None for fixed index)
311
+ H = self.H_input.value() if fixed_index != "H" else None
312
+ K = self.K_input.value() if fixed_index != "K" else None
313
+ L = self.L_input.value() if fixed_index != "L" else None
314
+
315
+ return {
316
+ "tth": self.tth_input.value(),
317
+ "H": H,
318
+ "K": K,
319
+ "L": L,
320
+ "fixed_index": fixed_index,
321
+ "fixed_angle_name": fixed_angle_name,
322
+ "fixed_angle_value": fixed_angle_value,
323
+ }
324
+
325
+
326
+ class HKAnglesResultsTable(QTableWidget):
327
+ """Table to display HK to Angles calculation results."""
328
+
329
+ # Signal emitted when a solution is selected
330
+ solutionSelected = pyqtSignal(dict)
331
+
332
+ def __init__(self, parent=None):
333
+ super().__init__(parent)
334
+
335
+ # Set up table
336
+ self.setColumnCount(4)
337
+ self.setHorizontalHeaderLabels(["tth (°)", "θ (°)", "φ (°)", "χ (°)"])
338
+ self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
339
+
340
+ # Hide vertical header (row numbers)
341
+ self.verticalHeader().setVisible(False)
342
+
343
+ # Connect selection change signal
344
+ self.itemSelectionChanged.connect(self.on_selection_changed)
345
+
346
+ # Store the last results for reference
347
+ self.last_results = None
348
+
349
+ def display_results(self, result):
350
+ """Append calculation results to the table."""
351
+ # Don't clear table - append new results
352
+ self.last_results = result
353
+
354
+ # Check if we have results
355
+ if not result or not result.get("success", False):
356
+ return
357
+
358
+
359
+ # Add rows for each new solution
360
+ row_position = self.rowCount()
361
+ self.insertRow(row_position)
362
+
363
+ # Add solution data
364
+ self.setItem(row_position, 0, QTableWidgetItem(f"{result['tth']:.1f}"))
365
+ self.setItem(row_position, 1, QTableWidgetItem(f"{result['theta']:.1f}"))
366
+ self.setItem(row_position, 2, QTableWidgetItem(f"{result['phi']:.1f}"))
367
+ self.setItem(row_position, 3, QTableWidgetItem(f"{result['chi']:.1f}"))
368
+
369
+ # Highlight new solutions with light blue background
370
+ feasible_brush = QBrush(QColor(198, 239, 206)) # light green
371
+ infeasible_brush = QBrush(QColor(255, 199, 206)) # light red
372
+ row_color = feasible_brush if result["feasible"] else infeasible_brush
373
+ for col in range(self.columnCount()):
374
+ item = self.item(row_position, col)
375
+ if item:
376
+ item.setBackground(row_color)
377
+
378
+ # Scroll to the bottom to show the new results
379
+ self.scrollToBottom()
380
+
381
+ def on_selection_changed(self):
382
+ """Handle selection change in the table."""
383
+ current_row = self.currentRow()
384
+ if current_row >= 0 and self.last_results:
385
+ if current_row < 1:
386
+ selected_solution = self.last_results
387
+ self.solutionSelected.emit(selected_solution)
388
+
389
+ def clear_results(self):
390
+ """Clear all results from the table."""
391
+ self.setRowCount(0)
392
+ self.last_results = None
393
+
394
+
395
+ class HKAnglesResultsWidget(QWidget):
396
+ """Complete results widget with table and clear button."""
397
+
398
+ # Signal emitted when a solution is selected
399
+ solutionSelected = pyqtSignal(dict)
400
+
401
+ def __init__(self, parent=None):
402
+ super().__init__(parent)
403
+
404
+ # Main layout
405
+ layout = QVBoxLayout(self)
406
+
407
+ # Results group
408
+ results_group = QGroupBox("Results")
409
+ results_layout = QVBoxLayout(results_group)
410
+
411
+ # Create table
412
+ self.results_table = HKAnglesResultsTable(self)
413
+ self.results_table.solutionSelected.connect(self.solutionSelected.emit)
414
+ results_layout.addWidget(self.results_table)
415
+
416
+ # Add clear button
417
+ self.clear_button = QPushButton("Clear Results")
418
+ self.clear_button.clicked.connect(self.clear_results)
419
+ self.clear_button.setObjectName("clearButton")
420
+ results_layout.addWidget(self.clear_button)
421
+
422
+ layout.addWidget(results_group)
423
+
424
+ def display_results(self, results):
425
+ """Display calculation results."""
426
+ self.results_table.display_results(results)
427
+
428
+ def clear_results(self):
429
+ """Clear all results."""
430
+ self.results_table.clear_results()