advisor-scattering 0.5.3__py3-none-any.whl → 0.9.5__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 (24) hide show
  1. advisor/controllers/app_controller.py +2 -1
  2. advisor/domain/__init__.py +4 -0
  3. advisor/domain/core/lab.py +9 -2
  4. advisor/domain/core/lattice.py +2 -5
  5. advisor/domain/core/sample.py +9 -2
  6. advisor/domain/orientation.py +219 -0
  7. advisor/domain/orientation_calculator.py +173 -0
  8. advisor/features/__init__.py +7 -4
  9. advisor/features/scattering_geometry/domain/brillouin_calculator.py +43 -1
  10. advisor/features/scattering_geometry/domain/core.py +4 -4
  11. advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +10 -18
  12. advisor/features/scattering_geometry/ui/components/hk_angles_components.py +16 -29
  13. advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +14 -25
  14. advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +18 -29
  15. advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +9 -1
  16. advisor/ui/dialogs/__init__.py +7 -0
  17. advisor/ui/dialogs/diffraction_test_dialog.py +287 -0
  18. advisor/ui/init_window.py +39 -15
  19. advisor/ui/visualizers/HKLScan2DVisualizer.py +37 -2
  20. {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/METADATA +4 -2
  21. {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/RECORD +24 -20
  22. {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/WHEEL +0 -0
  23. {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/entry_points.txt +0 -0
  24. {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
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
- QDialog,
19
- QScrollArea,
20
- QFrame,
21
- )
22
4
  from PyQt5.QtCore import Qt, pyqtSignal
23
- from PyQt5.QtGui import QColor, QBrush, QFont
5
+ from PyQt5.QtGui import QBrush, QColor, QFont
6
+ from PyQt5.QtWidgets import (QButtonGroup, QDialog, QDoubleSpinBox,
7
+ QFormLayout, QFrame, QGroupBox, QHBoxLayout,
8
+ QHeaderView, QLabel, QMessageBox, QPushButton,
9
+ QScrollArea, QTableWidget, QTableWidgetItem,
10
+ QVBoxLayout, QWidget)
24
11
 
25
12
 
26
13
  class HKAnglesControls(QWidget):
@@ -369,7 +356,7 @@ class HKAnglesHistoryDialog(QDialog):
369
356
  # Create table for solutions
370
357
  table = QTableWidget()
371
358
  table.setColumnCount(5)
372
- table.setHorizontalHeaderLabels(["#", "tth (°)", "θ (°)", "φ (°)", "χ (°)"])
359
+ table.setHorizontalHeaderLabels(["#", "tth (°)", "θ (°)", "χ (°)", "φ (°)"])
373
360
  table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
374
361
  table.verticalHeader().setVisible(False)
375
362
  table.setMaximumHeight(100)
@@ -377,8 +364,8 @@ class HKAnglesHistoryDialog(QDialog):
377
364
  # Add rows for each solution
378
365
  tth_list = entry.get('tth', [])
379
366
  theta_list = entry.get('theta', [])
380
- phi_list = entry.get('phi', [])
381
367
  chi_list = entry.get('chi', [])
368
+ phi_list = entry.get('phi', [])
382
369
  feasible_list = entry.get('feasible', [])
383
370
 
384
371
  for i in range(len(tth_list)):
@@ -386,8 +373,8 @@ class HKAnglesHistoryDialog(QDialog):
386
373
  table.setItem(i, 0, QTableWidgetItem(f"{i + 1}"))
387
374
  table.setItem(i, 1, QTableWidgetItem(f"{tth_list[i]:.2f}"))
388
375
  table.setItem(i, 2, QTableWidgetItem(f"{theta_list[i]:.2f}"))
389
- table.setItem(i, 3, QTableWidgetItem(f"{phi_list[i]:.2f}"))
390
- table.setItem(i, 4, QTableWidgetItem(f"{chi_list[i]:.2f}"))
376
+ table.setItem(i, 3, QTableWidgetItem(f"{chi_list[i]:.2f}"))
377
+ table.setItem(i, 4, QTableWidgetItem(f"{phi_list[i]:.2f}"))
391
378
 
392
379
  # Color based on feasibility
393
380
  feasible = feasible_list[i] if i < len(feasible_list) else True
@@ -438,7 +425,7 @@ class HKAnglesResultsWidget(QWidget):
438
425
  # Current solutions display (table for up to 2 solutions)
439
426
  self.results_table = QTableWidget()
440
427
  self.results_table.setColumnCount(4)
441
- self.results_table.setHorizontalHeaderLabels(["tth (°)", "θ (°)", "φ (°)", "χ (°)"])
428
+ self.results_table.setHorizontalHeaderLabels(["tth (°)", "θ (°)", "χ (°)", "φ (°)"])
442
429
  self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
443
430
  self.results_table.verticalHeader().setVisible(False)
444
431
  self.results_table.setMaximumHeight(90)
@@ -472,8 +459,8 @@ class HKAnglesResultsWidget(QWidget):
472
459
 
473
460
  tth_list = results.get('tth', [])
474
461
  theta_list = results.get('theta', [])
475
- phi_list = results.get('phi', [])
476
462
  chi_list = results.get('chi', [])
463
+ phi_list = results.get('phi', [])
477
464
  feasible_list = results.get('feasible', [])
478
465
 
479
466
  for i in range(len(tth_list)):
@@ -482,8 +469,8 @@ class HKAnglesResultsWidget(QWidget):
482
469
 
483
470
  self.results_table.setItem(row, 0, QTableWidgetItem(f"{tth_list[i]:.2f}"))
484
471
  self.results_table.setItem(row, 1, QTableWidgetItem(f"{theta_list[i]:.2f}"))
485
- self.results_table.setItem(row, 2, QTableWidgetItem(f"{phi_list[i]:.2f}"))
486
- self.results_table.setItem(row, 3, QTableWidgetItem(f"{chi_list[i]:.2f}"))
472
+ self.results_table.setItem(row, 2, QTableWidgetItem(f"{chi_list[i]:.2f}"))
473
+ self.results_table.setItem(row, 3, QTableWidgetItem(f"{phi_list[i]:.2f}"))
487
474
 
488
475
  # Color based on feasibility
489
476
  feasible = feasible_list[i] if i < len(feasible_list) else True
@@ -499,15 +486,15 @@ class HKAnglesResultsWidget(QWidget):
499
486
  if current_row >= 0 and self.current_result:
500
487
  tth_list = self.current_result.get('tth', [])
501
488
  theta_list = self.current_result.get('theta', [])
502
- phi_list = self.current_result.get('phi', [])
503
489
  chi_list = self.current_result.get('chi', [])
490
+ phi_list = self.current_result.get('phi', [])
504
491
 
505
492
  if current_row < len(tth_list):
506
493
  selected_solution = {
507
494
  'tth': tth_list[current_row],
508
495
  'theta': theta_list[current_row],
509
- 'phi': phi_list[current_row],
510
496
  'chi': chi_list[current_row],
497
+ 'phi': phi_list[current_row],
511
498
  'H': self.current_result.get('H'),
512
499
  'K': self.current_result.get('K'),
513
500
  'L': self.current_result.get('L'),
@@ -1,29 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
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
4
  import csv
25
5
  import os
6
+
26
7
  import numpy as np
8
+ from PyQt5.QtCore import Qt, pyqtSignal
9
+ from PyQt5.QtGui import QBrush, QColor
10
+ from PyQt5.QtWidgets import (QButtonGroup, QDoubleSpinBox, QFileDialog,
11
+ QFormLayout, QGroupBox, QHBoxLayout, QHeaderView,
12
+ QLabel, QMessageBox, QPushButton, QRadioButton,
13
+ QSpinBox, QTableWidget, QTableWidgetItem,
14
+ QVBoxLayout, QWidget)
15
+
27
16
 
28
17
  class RangeInputWidget(QWidget):
29
18
  """Widget for input range (start, end, num_points)."""
@@ -434,8 +423,8 @@ class HKLScanResultsTable(QTableWidget):
434
423
  l_values = results["L"]
435
424
  tth_values = results["tth"]
436
425
  theta_values = results["theta"]
437
- phi_values = results["phi"]
438
426
  chi_values = results["chi"]
427
+ phi_values = results["phi"]
439
428
  feasible_values = results.get("feasible", [True] * len(h_values))
440
429
  solution_groups = results.get("solution_group", list(range(len(h_values))))
441
430
  solution_indices = results.get("solution_index", [1] * len(h_values))
@@ -456,8 +445,8 @@ class HKLScanResultsTable(QTableWidget):
456
445
 
457
446
  # Add angle values
458
447
  self.setItem(row_position, 4, QTableWidgetItem(f"{theta_values[i]:.1f}"))
459
- self.setItem(row_position, 5, QTableWidgetItem(f"{phi_values[i]:.1f}"))
460
- self.setItem(row_position, 6, QTableWidgetItem(f"{chi_values[i]:.1f}"))
448
+ self.setItem(row_position, 5, QTableWidgetItem(f"{chi_values[i]:.1f}"))
449
+ self.setItem(row_position, 6, QTableWidgetItem(f"{phi_values[i]:.1f}"))
461
450
  self.setItem(row_position, 7, QTableWidgetItem(f"{tth_values[0]-theta_values[i]:.1f}"))
462
451
 
463
452
  # Determine row color based on feasibility and group
@@ -510,8 +499,8 @@ class HKLScanResultsTable(QTableWidget):
510
499
  "Solution#",
511
500
  "tth (deg)",
512
501
  "theta (deg)",
513
- "phi (deg)",
514
502
  "chi (deg)",
503
+ "phi (deg)",
515
504
  "feasible",
516
505
  ]
517
506
  )
@@ -529,8 +518,8 @@ class HKLScanResultsTable(QTableWidget):
529
518
  f"{solution_indices[i]}",
530
519
  f"{self.last_results['tth'][i]:.6f}",
531
520
  f"{self.last_results['theta'][i]:.6f}",
532
- f"{self.last_results['phi'][i]:.6f}",
533
521
  f"{self.last_results['chi'][i]:.6f}",
522
+ f"{self.last_results['phi'][i]:.6f}",
534
523
  f"{feasible_values[i]}",
535
524
  ]
536
525
  )
@@ -1,26 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
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
- QRadioButton,
17
- QButtonGroup,
18
- QDialog,
19
- QScrollArea,
20
- QFrame,
21
- )
22
4
  from PyQt5.QtCore import Qt, pyqtSignal
23
- from PyQt5.QtGui import QColor, QBrush, QFont
5
+ from PyQt5.QtGui import QBrush, QColor, QFont
6
+ from PyQt5.QtWidgets import (QButtonGroup, QDialog, QDoubleSpinBox,
7
+ QFormLayout, QFrame, QGroupBox, QHBoxLayout,
8
+ QHeaderView, QLabel, QPushButton, QRadioButton,
9
+ QScrollArea, QTableWidget, QTableWidgetItem,
10
+ QVBoxLayout, QWidget)
11
+
12
+
24
13
  class HKLToAnglesControls(QWidget):
25
14
  """Widget for HKL to Angles calculation controls."""
26
15
 
@@ -254,7 +243,7 @@ class HistoryDialog(QDialog):
254
243
  # Create table for solutions
255
244
  table = QTableWidget()
256
245
  table.setColumnCount(5)
257
- table.setHorizontalHeaderLabels(["#", "tth (°)", "θ (°)", "φ (°)", "χ (°)"])
246
+ table.setHorizontalHeaderLabels(["#", "tth (°)", "θ (°)", "χ (°)", "φ (°)"])
258
247
  table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
259
248
  table.verticalHeader().setVisible(False)
260
249
  table.setMaximumHeight(100)
@@ -262,8 +251,8 @@ class HistoryDialog(QDialog):
262
251
  # Add rows for each solution
263
252
  tth_list = entry.get('tth', [])
264
253
  theta_list = entry.get('theta', [])
265
- phi_list = entry.get('phi', [])
266
254
  chi_list = entry.get('chi', [])
255
+ phi_list = entry.get('phi', [])
267
256
  feasible_list = entry.get('feasible', [])
268
257
 
269
258
  for i in range(len(tth_list)):
@@ -271,8 +260,8 @@ class HistoryDialog(QDialog):
271
260
  table.setItem(i, 0, QTableWidgetItem(f"{i + 1}"))
272
261
  table.setItem(i, 1, QTableWidgetItem(f"{tth_list[i]:.2f}"))
273
262
  table.setItem(i, 2, QTableWidgetItem(f"{theta_list[i]:.2f}"))
274
- table.setItem(i, 3, QTableWidgetItem(f"{phi_list[i]:.2f}"))
275
- table.setItem(i, 4, QTableWidgetItem(f"{chi_list[i]:.2f}"))
263
+ table.setItem(i, 3, QTableWidgetItem(f"{chi_list[i]:.2f}"))
264
+ table.setItem(i, 4, QTableWidgetItem(f"{phi_list[i]:.2f}"))
276
265
 
277
266
  # Color based on feasibility
278
267
  feasible = feasible_list[i] if i < len(feasible_list) else True
@@ -323,7 +312,7 @@ class HKLToAnglesResultsWidget(QWidget):
323
312
  # Current solutions display (table for up to 2 solutions)
324
313
  self.results_table = QTableWidget()
325
314
  self.results_table.setColumnCount(4)
326
- self.results_table.setHorizontalHeaderLabels(["tth (°)", "θ (°)", "φ (°)", "χ (°)"])
315
+ self.results_table.setHorizontalHeaderLabels(["tth (°)", "θ (°)", "χ (°)", "φ (°)"])
327
316
  self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
328
317
  self.results_table.verticalHeader().setVisible(False)
329
318
  self.results_table.setMaximumHeight(90)
@@ -357,8 +346,8 @@ class HKLToAnglesResultsWidget(QWidget):
357
346
 
358
347
  tth_list = results.get('tth', [])
359
348
  theta_list = results.get('theta', [])
360
- phi_list = results.get('phi', [])
361
349
  chi_list = results.get('chi', [])
350
+ phi_list = results.get('phi', [])
362
351
  feasible_list = results.get('feasible', [])
363
352
 
364
353
  for i in range(len(tth_list)):
@@ -367,8 +356,8 @@ class HKLToAnglesResultsWidget(QWidget):
367
356
 
368
357
  self.results_table.setItem(row, 0, QTableWidgetItem(f"{tth_list[i]:.2f}"))
369
358
  self.results_table.setItem(row, 1, QTableWidgetItem(f"{theta_list[i]:.2f}"))
370
- self.results_table.setItem(row, 2, QTableWidgetItem(f"{phi_list[i]:.2f}"))
371
- self.results_table.setItem(row, 3, QTableWidgetItem(f"{chi_list[i]:.2f}"))
359
+ self.results_table.setItem(row, 2, QTableWidgetItem(f"{chi_list[i]:.2f}"))
360
+ self.results_table.setItem(row, 3, QTableWidgetItem(f"{phi_list[i]:.2f}"))
372
361
 
373
362
  # Color based on feasibility
374
363
  feasible = feasible_list[i] if i < len(feasible_list) else True
@@ -384,15 +373,15 @@ class HKLToAnglesResultsWidget(QWidget):
384
373
  if current_row >= 0 and self.current_result:
385
374
  tth_list = self.current_result.get('tth', [])
386
375
  theta_list = self.current_result.get('theta', [])
387
- phi_list = self.current_result.get('phi', [])
388
376
  chi_list = self.current_result.get('chi', [])
377
+ phi_list = self.current_result.get('phi', [])
389
378
 
390
379
  if current_row < len(tth_list):
391
380
  selected_solution = {
392
381
  'tth': tth_list[current_row],
393
382
  'theta': theta_list[current_row],
394
- 'phi': phi_list[current_row],
395
383
  'chi': chi_list[current_row],
384
+ 'phi': phi_list[current_row],
396
385
  'H': self.current_result.get('H'),
397
386
  'K': self.current_result.get('K'),
398
387
  'L': self.current_result.get('L'),
@@ -437,7 +437,7 @@ class ScatteringGeometryTab(TabInterface):
437
437
  """Update the HKL scan visualization with auto-detected ranges and results."""
438
438
  try:
439
439
  # Structure factor calculator not needed for trajectory-only visualization
440
-
440
+
441
441
  # Use the provided scan results or the last stored results
442
442
  if scan_results is None:
443
443
  scan_results = getattr(self.hkl_scan_visualizer, 'last_scan_results', None)
@@ -456,6 +456,14 @@ class ScatteringGeometryTab(TabInterface):
456
456
 
457
457
  # Visualize results (ranges will be auto-detected)
458
458
  self.hkl_scan_visualizer.visualize_results(scan_results, plane_type)
459
+
460
+ # plot the accessible area in k-space
461
+ params = self.hkl_scan_controls.get_scan_parameters()
462
+ tth = params["tth"]
463
+ accessible_area = self.calculator.get_max_hkl_values(tth)
464
+ h_max, k_max, l_max = accessible_area["h_max"], accessible_area["k_max"], accessible_area["l_max"]
465
+ success, message = accessible_area["success"], accessible_area["message"]
466
+ self.hkl_scan_visualizer.plot_accessible_area(plane_type, h_max, k_max, l_max, message)
459
467
  else:
460
468
  # No scan results yet, just clear the plot
461
469
  self.hkl_scan_visualizer.clear_plot()
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Dialog components for the Advisor application."""
4
+
5
+ from .diffraction_test_dialog import DiffractionTestDialog
6
+
7
+ __all__ = ["DiffractionTestDialog"]
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Dialog for importing orientation from diffraction test data."""
4
+
5
+ from PyQt5.QtCore import Qt
6
+ from PyQt5.QtWidgets import (QDialog, QDoubleSpinBox, QFormLayout, QGroupBox,
7
+ QHBoxLayout, QHeaderView, QLabel, QMessageBox,
8
+ QPushButton, QTableWidget, QTableWidgetItem,
9
+ QVBoxLayout)
10
+
11
+ from advisor.domain.orientation import fit_orientation_from_diffraction_tests
12
+
13
+
14
+ class DiffractionTestDialog(QDialog):
15
+ """Dialog for entering diffraction test data and calculating orientation.
16
+
17
+ This dialog allows users to input multiple diffraction tests (H, K, L, energy,
18
+ tth, theta, phi, chi) and calculates the optimal Euler angles (roll, pitch, yaw)
19
+ that best fit the data.
20
+ """
21
+
22
+ def __init__(self, lattice_params: dict, parent=None):
23
+ """Initialize the dialog.
24
+
25
+ Args:
26
+ lattice_params: Dictionary containing lattice parameters (a, b, c, alpha, beta, gamma)
27
+ parent: Parent widget
28
+ """
29
+ super().__init__(parent)
30
+ self.lattice_params = lattice_params
31
+ self.result = None # Will store (roll, pitch, yaw) on success
32
+
33
+ self.setWindowTitle("Import Orientation from UB Matrix Tests")
34
+ self.setMinimumWidth(800)
35
+ self.setMinimumHeight(550)
36
+
37
+ self._init_ui()
38
+
39
+ def _init_ui(self):
40
+ """Initialize the UI components."""
41
+ layout = QVBoxLayout(self)
42
+
43
+ # Instructions label
44
+ instructions = QLabel(
45
+ "Enter UB matrix test (diffraction test) data below. Each row represents a measurement "
46
+ "with known HKL indices and measured angles. At least one test is required."
47
+ )
48
+ instructions.setWordWrap(True)
49
+ layout.addWidget(instructions)
50
+
51
+ # Table for diffraction tests
52
+ self.table = QTableWidget()
53
+ self.table.setColumnCount(8)
54
+ self.table.setHorizontalHeaderLabels(
55
+ ["H", "K", "L", "Energy (eV)", "tth (°)", "θ (°)", "χ (°)", "φ (°)"]
56
+ )
57
+
58
+ # Set column resize mode
59
+ header = self.table.horizontalHeader()
60
+ for i in range(8):
61
+ header.setSectionResizeMode(i, QHeaderView.Stretch)
62
+
63
+ # Add initial empty rows
64
+ self._add_row()
65
+ self._add_row()
66
+
67
+ layout.addWidget(self.table)
68
+
69
+ # Row management buttons
70
+ row_buttons_layout = QHBoxLayout()
71
+
72
+ add_row_btn = QPushButton("Add Row")
73
+ add_row_btn.clicked.connect(self._add_row)
74
+ row_buttons_layout.addWidget(add_row_btn)
75
+
76
+ remove_row_btn = QPushButton("Remove Selected Row")
77
+ remove_row_btn.clicked.connect(self._remove_selected_row)
78
+ row_buttons_layout.addWidget(remove_row_btn)
79
+
80
+ row_buttons_layout.addStretch()
81
+ layout.addLayout(row_buttons_layout)
82
+
83
+ # Results display area
84
+ self.results_group = QGroupBox("Calculated Orientation")
85
+ results_layout = QFormLayout(self.results_group)
86
+
87
+ self.roll_result = QDoubleSpinBox()
88
+ self.roll_result.setRange(-180, 180)
89
+ self.roll_result.setDecimals(4)
90
+ self.roll_result.setReadOnly(True)
91
+ self.roll_result.setSuffix(" °")
92
+ results_layout.addRow("Roll:", self.roll_result)
93
+
94
+ self.pitch_result = QDoubleSpinBox()
95
+ self.pitch_result.setRange(-180, 180)
96
+ self.pitch_result.setDecimals(4)
97
+ self.pitch_result.setReadOnly(True)
98
+ self.pitch_result.setSuffix(" °")
99
+ results_layout.addRow("Pitch:", self.pitch_result)
100
+
101
+ self.yaw_result = QDoubleSpinBox()
102
+ self.yaw_result.setRange(-180, 180)
103
+ self.yaw_result.setDecimals(4)
104
+ self.yaw_result.setReadOnly(True)
105
+ self.yaw_result.setSuffix(" °")
106
+ results_layout.addRow("Yaw:", self.yaw_result)
107
+
108
+ self.error_label = QLabel("Residual Error: --")
109
+ results_layout.addRow(self.error_label)
110
+
111
+ self.results_group.setVisible(False)
112
+ layout.addWidget(self.results_group)
113
+
114
+ # Action buttons
115
+ button_layout = QHBoxLayout()
116
+
117
+ self.calculate_btn = QPushButton("Calculate Orientation")
118
+ self.calculate_btn.clicked.connect(self._calculate_orientation)
119
+ self._set_button_highlighted(self.calculate_btn, True) # Highlight initially
120
+ button_layout.addWidget(self.calculate_btn)
121
+
122
+ self.apply_btn = QPushButton("Apply and Close")
123
+ self.apply_btn.clicked.connect(self._apply_and_close)
124
+ self.apply_btn.setEnabled(False)
125
+ button_layout.addWidget(self.apply_btn)
126
+
127
+ cancel_btn = QPushButton("Cancel")
128
+ cancel_btn.clicked.connect(self.reject)
129
+ button_layout.addWidget(cancel_btn)
130
+
131
+ layout.addLayout(button_layout)
132
+
133
+ def _set_button_highlighted(self, button: QPushButton, highlighted: bool):
134
+ """Set button to highlighted (pastel blue) or normal style."""
135
+ if highlighted:
136
+ button.setStyleSheet(
137
+ "background-color: #a8d0f0; color: #2c5282; font-weight: bold;"
138
+ )
139
+ else:
140
+ button.setStyleSheet("") # Reset to default
141
+
142
+ def _set_button_success(self, button: QPushButton, success: bool):
143
+ """Set button to success (pastel green) or normal style."""
144
+ if success:
145
+ button.setStyleSheet(
146
+ "background-color: #a8e6c1; color: #2d5a3d; font-weight: bold;"
147
+ )
148
+ else:
149
+ button.setStyleSheet("") # Reset to default
150
+
151
+ def _add_row(self):
152
+ """Add a new empty row to the table."""
153
+ row = self.table.rowCount()
154
+ self.table.insertRow(row)
155
+
156
+ # Set default values (H, K, L, energy, tth, theta, chi, phi)
157
+ defaults = [0.0, 0.0, 0.0, 2200.0, 90.0, 45.0, 0.0, 0.0]
158
+ for col, default in enumerate(defaults):
159
+ item = QTableWidgetItem(str(default))
160
+ item.setTextAlignment(Qt.AlignCenter)
161
+ self.table.setItem(row, col, item)
162
+
163
+ def _remove_selected_row(self):
164
+ """Remove the currently selected row."""
165
+ current_row = self.table.currentRow()
166
+ if current_row >= 0:
167
+ self.table.removeRow(current_row)
168
+ elif self.table.rowCount() > 0:
169
+ # If no row selected, remove the last row
170
+ self.table.removeRow(self.table.rowCount() - 1)
171
+
172
+ def _get_diffraction_tests(self) -> list:
173
+ """Extract diffraction test data from the table.
174
+
175
+ Returns:
176
+ List of dictionaries containing test data, or None if validation fails.
177
+ """
178
+ tests = []
179
+ for row in range(self.table.rowCount()):
180
+ try:
181
+ test = {
182
+ "H": float(self.table.item(row, 0).text()),
183
+ "K": float(self.table.item(row, 1).text()),
184
+ "L": float(self.table.item(row, 2).text()),
185
+ "energy": float(self.table.item(row, 3).text()),
186
+ "tth": float(self.table.item(row, 4).text()),
187
+ "theta": float(self.table.item(row, 5).text()),
188
+ "chi": float(self.table.item(row, 6).text()),
189
+ "phi": float(self.table.item(row, 7).text()),
190
+ }
191
+ tests.append(test)
192
+ except (ValueError, AttributeError) as e:
193
+ QMessageBox.warning(
194
+ self,
195
+ "Invalid Input",
196
+ f"Row {row + 1} contains invalid data. Please enter numeric values.\n\nError: {e}",
197
+ )
198
+ return None
199
+
200
+ if not tests:
201
+ QMessageBox.warning(
202
+ self,
203
+ "No Data",
204
+ "Please enter at least one diffraction test.",
205
+ )
206
+ return None
207
+
208
+ return tests
209
+
210
+ def _calculate_orientation(self):
211
+ """Calculate the optimal orientation from the entered data."""
212
+ tests = self._get_diffraction_tests()
213
+ if tests is None:
214
+ return
215
+
216
+ # Run the fitting algorithm
217
+ result = fit_orientation_from_diffraction_tests(
218
+ self.lattice_params, tests
219
+ )
220
+
221
+ if not result["success"]:
222
+ QMessageBox.warning(
223
+ self,
224
+ "Calculation Failed",
225
+ f"Failed to calculate orientation:\n\n{result.get('message', 'Unknown error')}",
226
+ )
227
+ return
228
+
229
+ # Display results (overwrites any previous results)
230
+ self.roll_result.setValue(result["roll"])
231
+ self.pitch_result.setValue(result["pitch"])
232
+ self.yaw_result.setValue(result["yaw"])
233
+ self.error_label.setText(f"Residual Error: {result['residual_error']:.6f}")
234
+
235
+ # Update group title to indicate results are current
236
+ self.results_group.setTitle("Calculated Orientation (Updated)")
237
+ self.results_group.setVisible(True)
238
+ self.apply_btn.setEnabled(True)
239
+
240
+ # Update button styles: remove highlight from Calculate, add to Apply
241
+ self._set_button_highlighted(self.calculate_btn, False)
242
+ self._set_button_success(self.apply_btn, True)
243
+
244
+ # Store result for later retrieval
245
+ self.result = {
246
+ "roll": result["roll"],
247
+ "pitch": result["pitch"],
248
+ "yaw": result["yaw"],
249
+ }
250
+
251
+ # Show detailed errors if available
252
+ if result.get("individual_errors"):
253
+ error_text = "Individual test errors:\n"
254
+ for i, err in enumerate(result["individual_errors"]):
255
+ error_text += (
256
+ f" Test {i+1}: ΔH={err['H_error']:.4f}, "
257
+ f"ΔK={err['K_error']:.4f}, ΔL={err['L_error']:.4f}\n"
258
+ )
259
+ QMessageBox.information(
260
+ self,
261
+ "Calculation Complete",
262
+ f"Orientation calculated successfully!\n\n"
263
+ f"Roll: {result['roll']:.4f}°\n"
264
+ f"Pitch: {result['pitch']:.4f}°\n"
265
+ f"Yaw: {result['yaw']:.4f}°\n\n"
266
+ f"Residual Error: {result['residual_error']:.6f}\n\n"
267
+ f"{error_text}",
268
+ )
269
+
270
+ def _apply_and_close(self):
271
+ """Apply the calculated orientation and close the dialog."""
272
+ if self.result is not None:
273
+ self.accept()
274
+ else:
275
+ QMessageBox.warning(
276
+ self,
277
+ "No Result",
278
+ "Please calculate the orientation first.",
279
+ )
280
+
281
+ def get_result(self) -> dict:
282
+ """Get the calculated orientation.
283
+
284
+ Returns:
285
+ Dictionary with roll, pitch, yaw values, or None if not calculated.
286
+ """
287
+ return self.result