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.
- advisor/controllers/app_controller.py +2 -1
- advisor/domain/__init__.py +4 -0
- advisor/domain/core/lab.py +9 -2
- advisor/domain/core/lattice.py +2 -5
- advisor/domain/core/sample.py +9 -2
- advisor/domain/orientation.py +219 -0
- advisor/domain/orientation_calculator.py +173 -0
- advisor/features/__init__.py +7 -4
- advisor/features/scattering_geometry/domain/brillouin_calculator.py +43 -1
- advisor/features/scattering_geometry/domain/core.py +4 -4
- advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +10 -18
- advisor/features/scattering_geometry/ui/components/hk_angles_components.py +16 -29
- advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +14 -25
- advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +18 -29
- advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +9 -1
- advisor/ui/dialogs/__init__.py +7 -0
- advisor/ui/dialogs/diffraction_test_dialog.py +287 -0
- advisor/ui/init_window.py +39 -15
- advisor/ui/visualizers/HKLScan2DVisualizer.py +37 -2
- {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/METADATA +4 -2
- {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/RECORD +24 -20
- {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/WHEEL +0 -0
- {advisor_scattering-0.5.3.dist-info → advisor_scattering-0.9.5.dist-info}/entry_points.txt +0 -0
- {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
|
|
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"{
|
|
390
|
-
table.setItem(i, 4, QTableWidgetItem(f"{
|
|
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"{
|
|
486
|
-
self.results_table.setItem(row, 3, QTableWidgetItem(f"{
|
|
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"{
|
|
460
|
-
self.setItem(row_position, 6, QTableWidgetItem(f"{
|
|
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
|
|
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"{
|
|
275
|
-
table.setItem(i, 4, QTableWidgetItem(f"{
|
|
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"{
|
|
371
|
-
self.results_table.setItem(row, 3, QTableWidgetItem(f"{
|
|
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,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
|