advisor-scattering 0.5.3__tar.gz → 0.9.1__tar.gz
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_scattering-0.5.3/advisor_scattering.egg-info → advisor_scattering-0.9.1}/PKG-INFO +1 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/controllers/app_controller.py +2 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/__init__.py +4 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/core/lab.py +9 -2
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/core/lattice.py +2 -5
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/core/sample.py +9 -2
- advisor_scattering-0.9.1/advisor/domain/orientation.py +219 -0
- advisor_scattering-0.9.1/advisor/domain/orientation_calculator.py +174 -0
- advisor_scattering-0.9.1/advisor/features/__init__.py +9 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/domain/brillouin_calculator.py +12 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/domain/core.py +4 -4
- advisor_scattering-0.9.1/advisor/ui/dialogs/__init__.py +7 -0
- advisor_scattering-0.9.1/advisor/ui/dialogs/diffraction_test_dialog.py +264 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/init_window.py +33 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1/advisor_scattering.egg-info}/PKG-INFO +1 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/SOURCES.txt +4 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/pyproject.toml +1 -1
- advisor_scattering-0.5.3/advisor/features/__init__.py +0 -6
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/MANIFEST.in +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/README.md +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/__main__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/app.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/controllers/feature_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/core/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/geometry.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/domain/unit_converter.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/domain/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/components/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/components/hk_angles_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/controllers/structure_factor_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/domain/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/domain/structure_factor_calculator.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/ui/components/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/ui/components/customized_plane_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/ui/components/hkl_plane_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/features/structure_factor/ui/structure_factor_tab.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/config/app_config.json +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/config/tips.json +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/data/nacl.cif +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/bz_caculator.jpg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/bz_calculator.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/minus.svg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/placeholder.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/plus.svg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/reset.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/sf_calculator.jpg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/sf_calculator.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons.qrc +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/qss/styles.qss +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/resources_rc.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/main_window.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/tab_interface.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/tips.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/utils/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/utils/readcif.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/HKLScan2DVisualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/coordinate_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/scattering_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/structure_factor_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/structure_factor_visualizer_2d.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/unitcell_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/dependency_links.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/entry_points.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/requires.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/top_level.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/setup.cfg +0 -0
|
@@ -6,7 +6,8 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
from PyQt5.QtWidgets import QApplication
|
|
8
8
|
|
|
9
|
-
from advisor.features import ScatteringGeometryController
|
|
9
|
+
from advisor.features.scattering_geometry.controllers import ScatteringGeometryController
|
|
10
|
+
from advisor.features.structure_factor.controllers import StructureFactorController
|
|
10
11
|
from advisor.ui.init_window import InitWindow
|
|
11
12
|
from advisor.ui.main_window import MainWindow
|
|
12
13
|
|
|
@@ -10,6 +10,8 @@ from .geometry import (
|
|
|
10
10
|
lab_to_sample_conversion,
|
|
11
11
|
)
|
|
12
12
|
from .unit_converter import UnitConverter
|
|
13
|
+
from .orientation import fit_orientation_from_diffraction_tests
|
|
14
|
+
from .orientation_calculator import OrientationCalculator
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
15
17
|
"get_real_space_vectors",
|
|
@@ -20,4 +22,6 @@ __all__ = [
|
|
|
20
22
|
"sample_to_lab_conversion",
|
|
21
23
|
"lab_to_sample_conversion",
|
|
22
24
|
"UnitConverter",
|
|
25
|
+
"fit_orientation_from_diffraction_tests",
|
|
26
|
+
"OrientationCalculator",
|
|
23
27
|
]
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from advisor.domain import angle_to_matrix
|
|
6
|
-
from .sample import Sample
|
|
7
6
|
|
|
7
|
+
from .sample import Sample
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Lab:
|
|
@@ -113,9 +113,16 @@ class Lab:
|
|
|
113
113
|
return ex_lattice_in_lab, ey_lattice_in_lab, ez_lattice_in_lab
|
|
114
114
|
|
|
115
115
|
def rotate(self, theta, phi, chi):
|
|
116
|
-
"""Rotate the lab
|
|
116
|
+
"""Rotate the lab / goniometer"""
|
|
117
117
|
self.theta = theta
|
|
118
118
|
self.phi = phi
|
|
119
119
|
self.chi = chi
|
|
120
120
|
self.calculate_real_space_vectors()
|
|
121
121
|
self.calculate_reciprocal_space_vectors()
|
|
122
|
+
|
|
123
|
+
def reorient(self,roll, pitch, yaw):
|
|
124
|
+
"""reorient the sample with respect to the lab"""
|
|
125
|
+
self.sample.reorient(roll, pitch, yaw)
|
|
126
|
+
self.calculate_real_space_vectors()
|
|
127
|
+
self.calculate_reciprocal_space_vectors()
|
|
128
|
+
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
from advisor.domain import
|
|
6
|
-
get_real_space_vectors,
|
|
7
|
-
get_reciprocal_space_vectors,
|
|
8
|
-
)
|
|
5
|
+
from advisor.domain import get_real_space_vectors, get_reciprocal_space_vectors
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
class Lattice:
|
|
@@ -31,7 +28,7 @@ class Lattice:
|
|
|
31
28
|
self.c_star_vec_lattice = None
|
|
32
29
|
|
|
33
30
|
def initialize(self, a, b, c, alpha, beta, gamma):
|
|
34
|
-
"""Initialize the
|
|
31
|
+
"""Initialize by calculating the real and reciprocal space vectors.
|
|
35
32
|
|
|
36
33
|
Args:
|
|
37
34
|
a, b, c (float): Lattice constants in Angstroms
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from advisor.domain import euler_to_matrix
|
|
6
|
+
|
|
6
7
|
from .lattice import Lattice
|
|
7
8
|
|
|
9
|
+
|
|
8
10
|
class Sample:
|
|
9
11
|
"""This is a class for the sample."""
|
|
10
12
|
|
|
@@ -97,5 +99,10 @@ class Sample:
|
|
|
97
99
|
self.b_star_vec_sample = rotation_matrix @ b_star_vec_lattice
|
|
98
100
|
self.c_star_vec_sample = rotation_matrix @ c_star_vec_lattice
|
|
99
101
|
|
|
100
|
-
def
|
|
101
|
-
"""
|
|
102
|
+
def reorient(self,roll, pitch, yaw):
|
|
103
|
+
"""reorient the sample"""
|
|
104
|
+
self.roll = roll
|
|
105
|
+
self.pitch = pitch
|
|
106
|
+
self.yaw = yaw
|
|
107
|
+
self.calculate_real_space_vectors()
|
|
108
|
+
self.calculate_reciprocal_space_vectors()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Orientation fitting from diffraction tests.
|
|
4
|
+
|
|
5
|
+
This module provides functions to determine the optimal Euler angles (roll, pitch, yaw)
|
|
6
|
+
that align a crystal lattice orientation with observed diffraction data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.optimize import minimize
|
|
11
|
+
|
|
12
|
+
from .orientation_calculator import OrientationCalculator
|
|
13
|
+
|
|
14
|
+
# Default number of random restarts for optimization
|
|
15
|
+
DEFAULT_N_RESTARTS = 20
|
|
16
|
+
# Target residual error - stop early if achieved
|
|
17
|
+
TARGET_RESIDUAL = 1e-10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fit_orientation_from_diffraction_tests(
|
|
21
|
+
lattice_params: dict,
|
|
22
|
+
diffraction_tests: list,
|
|
23
|
+
initial_guess: tuple = (0.0, 0.0, 0.0),
|
|
24
|
+
n_restarts: int = DEFAULT_N_RESTARTS,
|
|
25
|
+
) -> dict:
|
|
26
|
+
"""Fit crystal orientation from diffraction test data.
|
|
27
|
+
|
|
28
|
+
Given lattice parameters and a list of diffraction tests (each containing
|
|
29
|
+
known HKL values and measured angles), find the Euler angles (roll, pitch, yaw)
|
|
30
|
+
that best explain the observations.
|
|
31
|
+
|
|
32
|
+
Uses multiple random restarts to avoid local minima.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
lattice_params: Dictionary containing lattice parameters:
|
|
36
|
+
- a, b, c (float): Lattice constants in Angstroms
|
|
37
|
+
- alpha, beta, gamma (float): Lattice angles in degrees
|
|
38
|
+
diffraction_tests: List of dictionaries, each containing:
|
|
39
|
+
- H, K, L (float): Expected Miller indices
|
|
40
|
+
- energy (float): X-ray energy in eV
|
|
41
|
+
- tth (float): Scattering angle 2θ in degrees
|
|
42
|
+
- theta (float): Sample theta rotation in degrees
|
|
43
|
+
- phi (float): Sample phi rotation in degrees
|
|
44
|
+
- chi (float): Sample chi rotation in degrees
|
|
45
|
+
initial_guess: Initial guess for (roll, pitch, yaw) in degrees
|
|
46
|
+
n_restarts: Number of random restarts to try (default: 20)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
dict: Dictionary containing:
|
|
50
|
+
- roll, pitch, yaw (float): Optimized Euler angles in degrees
|
|
51
|
+
- residual_error (float): Final residual error (sum of squared HKL differences)
|
|
52
|
+
- individual_errors (list): Per-test HKL errors
|
|
53
|
+
- success (bool): Whether optimization converged
|
|
54
|
+
- message (str): Status message
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if not diffraction_tests:
|
|
58
|
+
return {
|
|
59
|
+
"success": False,
|
|
60
|
+
"message": "No diffraction tests provided",
|
|
61
|
+
"roll": 0.0,
|
|
62
|
+
"pitch": 0.0,
|
|
63
|
+
"yaw": 0.0,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Validate diffraction tests
|
|
67
|
+
required_keys = ["H", "K", "L", "energy", "tth", "theta", "phi", "chi"]
|
|
68
|
+
for i, test in enumerate(diffraction_tests):
|
|
69
|
+
missing = [k for k in required_keys if k not in test]
|
|
70
|
+
if missing:
|
|
71
|
+
return {
|
|
72
|
+
"success": False,
|
|
73
|
+
"message": f"Test {i+1} is missing required keys: {missing}",
|
|
74
|
+
"roll": 0.0,
|
|
75
|
+
"pitch": 0.0,
|
|
76
|
+
"yaw": 0.0,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Initialize calculator with lattice parameters (using initial roll, pitch, yaw = 0)
|
|
80
|
+
calculator = OrientationCalculator()
|
|
81
|
+
init_params = {
|
|
82
|
+
"a": lattice_params["a"],
|
|
83
|
+
"b": lattice_params["b"],
|
|
84
|
+
"c": lattice_params["c"],
|
|
85
|
+
"alpha": lattice_params["alpha"],
|
|
86
|
+
"beta": lattice_params["beta"],
|
|
87
|
+
"gamma": lattice_params["gamma"],
|
|
88
|
+
"energy": diffraction_tests[0]["energy"], # Will be updated per test
|
|
89
|
+
"roll": 0.0,
|
|
90
|
+
"pitch": 0.0,
|
|
91
|
+
"yaw": 0.0,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if not calculator.initialize(init_params):
|
|
95
|
+
return {
|
|
96
|
+
"success": False,
|
|
97
|
+
"message": "Failed to initialize calculator with given lattice parameters",
|
|
98
|
+
"roll": 0.0,
|
|
99
|
+
"pitch": 0.0,
|
|
100
|
+
"yaw": 0.0,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def objective(params):
|
|
104
|
+
"""Objective function: sum of squared HKL errors."""
|
|
105
|
+
roll, pitch, yaw = params
|
|
106
|
+
calculator.reorient_sample(roll, pitch, yaw)
|
|
107
|
+
|
|
108
|
+
total_error = 0.0
|
|
109
|
+
for test in diffraction_tests:
|
|
110
|
+
# Update energy for this test
|
|
111
|
+
calculator.change_energy(test["energy"])
|
|
112
|
+
|
|
113
|
+
# Calculate HKL from angles
|
|
114
|
+
result = calculator.calculate_hkl(
|
|
115
|
+
test["tth"], test["theta"], test["phi"], test["chi"]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Compute squared error
|
|
119
|
+
dH = result["H"] - test["H"]
|
|
120
|
+
dK = result["K"] - test["K"]
|
|
121
|
+
dL = result["L"] - test["L"]
|
|
122
|
+
total_error += dH**2 + dK**2 + dL**2
|
|
123
|
+
|
|
124
|
+
return total_error
|
|
125
|
+
|
|
126
|
+
def run_optimization(start_point):
|
|
127
|
+
"""Run a single optimization from a starting point."""
|
|
128
|
+
return minimize(
|
|
129
|
+
objective,
|
|
130
|
+
start_point,
|
|
131
|
+
method="L-BFGS-B",
|
|
132
|
+
bounds=[(-180, 180), (-180, 180), (-180, 180)],
|
|
133
|
+
options={"ftol": 1e-12, "gtol": 1e-10, "maxiter": 1000},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Multiple random restarts to find global minimum
|
|
137
|
+
best_result = None
|
|
138
|
+
best_error = float("inf")
|
|
139
|
+
|
|
140
|
+
# First try the user-provided initial guess
|
|
141
|
+
initial_points = [initial_guess]
|
|
142
|
+
|
|
143
|
+
# Add random starting points
|
|
144
|
+
rng = np.random.default_rng(seed=42) # Reproducible results
|
|
145
|
+
for _ in range(n_restarts - 1):
|
|
146
|
+
# Random angles in [-180, 180]
|
|
147
|
+
random_point = tuple(rng.uniform(-180, 180, 3))
|
|
148
|
+
initial_points.append(random_point)
|
|
149
|
+
|
|
150
|
+
# Also add some structured starting points
|
|
151
|
+
structured_points = [
|
|
152
|
+
(0, 0, 0),
|
|
153
|
+
(90, 0, 0), (-90, 0, 0),
|
|
154
|
+
(0, 90, 0), (0, -90, 0),
|
|
155
|
+
(0, 0, 90), (0, 0, -90),
|
|
156
|
+
]
|
|
157
|
+
for pt in structured_points:
|
|
158
|
+
if pt not in initial_points:
|
|
159
|
+
initial_points.append(pt)
|
|
160
|
+
|
|
161
|
+
# Run optimization from each starting point
|
|
162
|
+
for start_point in initial_points:
|
|
163
|
+
try:
|
|
164
|
+
result = run_optimization(start_point)
|
|
165
|
+
if result.fun < best_error:
|
|
166
|
+
best_error = result.fun
|
|
167
|
+
best_result = result
|
|
168
|
+
|
|
169
|
+
# Early stopping if we found a very good solution
|
|
170
|
+
if best_error < TARGET_RESIDUAL:
|
|
171
|
+
break
|
|
172
|
+
except Exception:
|
|
173
|
+
# Skip failed optimizations
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if best_result is None:
|
|
177
|
+
return {
|
|
178
|
+
"success": False,
|
|
179
|
+
"message": "All optimization attempts failed",
|
|
180
|
+
"roll": 0.0,
|
|
181
|
+
"pitch": 0.0,
|
|
182
|
+
"yaw": 0.0,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Extract optimized parameters
|
|
186
|
+
roll_opt, pitch_opt, yaw_opt = best_result.x
|
|
187
|
+
|
|
188
|
+
# Calculate individual errors at optimal orientation
|
|
189
|
+
calculator.reorient_sample(roll_opt, pitch_opt, yaw_opt)
|
|
190
|
+
individual_errors = []
|
|
191
|
+
for test in diffraction_tests:
|
|
192
|
+
calculator.change_energy(test["energy"])
|
|
193
|
+
calc_result = calculator.calculate_hkl(
|
|
194
|
+
test["tth"], test["theta"], test["phi"], test["chi"]
|
|
195
|
+
)
|
|
196
|
+
error = {
|
|
197
|
+
"H_expected": test["H"],
|
|
198
|
+
"K_expected": test["K"],
|
|
199
|
+
"L_expected": test["L"],
|
|
200
|
+
"H_calculated": calc_result["H"],
|
|
201
|
+
"K_calculated": calc_result["K"],
|
|
202
|
+
"L_calculated": calc_result["L"],
|
|
203
|
+
"H_error": calc_result["H"] - test["H"],
|
|
204
|
+
"K_error": calc_result["K"] - test["K"],
|
|
205
|
+
"L_error": calc_result["L"] - test["L"],
|
|
206
|
+
}
|
|
207
|
+
individual_errors.append(error)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"success": best_result.success,
|
|
211
|
+
"message": best_result.message if hasattr(best_result, "message") else "Optimization completed",
|
|
212
|
+
"roll": float(roll_opt),
|
|
213
|
+
"pitch": float(pitch_opt),
|
|
214
|
+
"yaw": float(yaw_opt),
|
|
215
|
+
"residual_error": float(best_result.fun),
|
|
216
|
+
"individual_errors": individual_errors,
|
|
217
|
+
"n_iterations": best_result.nit if hasattr(best_result, "nit") else None,
|
|
218
|
+
"n_restarts_used": len(initial_points),
|
|
219
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Lightweight calculator for orientation fitting.
|
|
4
|
+
|
|
5
|
+
This module provides a minimal calculator class that can compute HKL values
|
|
6
|
+
from scattering angles, used specifically for orientation fitting.
|
|
7
|
+
It avoids importing from feature modules to prevent circular dependencies.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from advisor.domain import angle_to_matrix
|
|
13
|
+
from advisor.domain.core import Lab
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrientationCalculator:
|
|
17
|
+
"""Lightweight calculator for orientation fitting.
|
|
18
|
+
|
|
19
|
+
This class provides only the methods needed for fitting crystal orientation
|
|
20
|
+
from diffraction test data. It uses the Lab class directly and avoids
|
|
21
|
+
dependencies on feature-specific modules.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Physical constants
|
|
25
|
+
EV_TO_LAMBDA = 12398.42 # eV to Angstrom conversion
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the calculator."""
|
|
29
|
+
self._initialized = False
|
|
30
|
+
self.lab = Lab()
|
|
31
|
+
self.energy = None
|
|
32
|
+
self.lambda_A = None
|
|
33
|
+
self.k_in = None
|
|
34
|
+
|
|
35
|
+
def initialize(self, params: dict) -> bool:
|
|
36
|
+
"""Initialize with lattice parameters.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
params: Dictionary containing:
|
|
40
|
+
- a, b, c (float): Lattice constants in Angstroms
|
|
41
|
+
- alpha, beta, gamma (float): Lattice angles in degrees
|
|
42
|
+
- energy (float): X-ray energy in eV
|
|
43
|
+
- roll, pitch, yaw (float, optional): Euler angles in degrees
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
bool: True if initialization was successful
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
a = params.get("a", 4.0)
|
|
50
|
+
b = params.get("b", 4.0)
|
|
51
|
+
c = params.get("c", 12.0)
|
|
52
|
+
alpha = params.get("alpha", 90.0)
|
|
53
|
+
beta = params.get("beta", 90.0)
|
|
54
|
+
gamma = params.get("gamma", 90.0)
|
|
55
|
+
roll = params.get("roll", 0.0)
|
|
56
|
+
pitch = params.get("pitch", 0.0)
|
|
57
|
+
yaw = params.get("yaw", 0.0)
|
|
58
|
+
self.energy = params["energy"]
|
|
59
|
+
|
|
60
|
+
# Initialize lab with default sample rotation (0, 0, 0)
|
|
61
|
+
self.lab.initialize(a, b, c, alpha, beta, gamma, roll, pitch, yaw, 0, 0, 0)
|
|
62
|
+
|
|
63
|
+
# Calculate wavelength and wavevector
|
|
64
|
+
self.lambda_A = self.EV_TO_LAMBDA / self.energy
|
|
65
|
+
self.k_in = 2 * np.pi / self.lambda_A
|
|
66
|
+
|
|
67
|
+
self._initialized = True
|
|
68
|
+
return True
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"Error initializing OrientationCalculator: {e}")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def change_energy(self, energy: float) -> bool:
|
|
74
|
+
"""Change the X-ray energy.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
energy: X-ray energy in eV
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
bool: True if successful
|
|
81
|
+
"""
|
|
82
|
+
self.energy = energy
|
|
83
|
+
self.lambda_A = self.EV_TO_LAMBDA / self.energy
|
|
84
|
+
self.k_in = 2 * np.pi / self.lambda_A
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def reorient_sample(self, roll: float, pitch: float, yaw: float) -> bool:
|
|
88
|
+
"""Reorient the sample (change Euler angles).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
roll, pitch, yaw: Euler angles in degrees
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
bool: True if successful
|
|
95
|
+
"""
|
|
96
|
+
self.lab.reorient(roll, pitch, yaw)
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def calculate_hkl(
|
|
100
|
+
self, tth: float, theta: float, phi: float, chi: float
|
|
101
|
+
) -> dict:
|
|
102
|
+
"""Calculate HKL from scattering angles.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tth: Scattering angle 2θ in degrees
|
|
106
|
+
theta: Sample theta rotation in degrees
|
|
107
|
+
phi: Sample phi rotation in degrees
|
|
108
|
+
chi: Sample chi rotation in degrees
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
dict: Dictionary containing:
|
|
112
|
+
- H, K, L (float): Miller indices
|
|
113
|
+
- tth, theta, phi, chi (float): Input angles
|
|
114
|
+
- success (bool): Whether calculation succeeded
|
|
115
|
+
- error (str or None): Error message if any
|
|
116
|
+
"""
|
|
117
|
+
if not self._initialized:
|
|
118
|
+
return {
|
|
119
|
+
"H": None, "K": None, "L": None,
|
|
120
|
+
"tth": tth, "theta": theta, "phi": phi, "chi": chi,
|
|
121
|
+
"success": False,
|
|
122
|
+
"error": "Calculator not initialized",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Get real space vectors in lab frame
|
|
127
|
+
a_vec_lab, b_vec_lab, c_vec_lab = self.lab.get_real_space_vectors()
|
|
128
|
+
|
|
129
|
+
# Calculate momentum transfer magnitude
|
|
130
|
+
k_magnitude = 2.0 * self.k_in * np.sin(np.radians(tth / 2.0))
|
|
131
|
+
|
|
132
|
+
# Calculate delta angle
|
|
133
|
+
delta = 90 - (tth / 2.0)
|
|
134
|
+
sin_delta = np.sin(np.radians(delta))
|
|
135
|
+
cos_delta = np.cos(np.radians(delta))
|
|
136
|
+
|
|
137
|
+
# Momentum transfer at theta, phi, chi = 0
|
|
138
|
+
k_vec_initial = np.array(
|
|
139
|
+
[-k_magnitude * sin_delta, -k_magnitude * cos_delta, 0.0]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Rotation of the beam is the reverse rotation of the sample
|
|
143
|
+
rotation_matrix = angle_to_matrix(theta, phi, chi).T
|
|
144
|
+
|
|
145
|
+
# Momentum transfer at the given angles
|
|
146
|
+
k_vec_lab = rotation_matrix @ k_vec_initial
|
|
147
|
+
|
|
148
|
+
# Calculate HKL by projecting onto real space vectors
|
|
149
|
+
H = np.dot(k_vec_lab, a_vec_lab) / (2 * np.pi)
|
|
150
|
+
K = np.dot(k_vec_lab, b_vec_lab) / (2 * np.pi)
|
|
151
|
+
L = np.dot(k_vec_lab, c_vec_lab) / (2 * np.pi)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"H": H,
|
|
155
|
+
"K": K,
|
|
156
|
+
"L": L,
|
|
157
|
+
"tth": tth,
|
|
158
|
+
"theta": theta,
|
|
159
|
+
"phi": phi,
|
|
160
|
+
"chi": chi,
|
|
161
|
+
"success": True,
|
|
162
|
+
"error": None,
|
|
163
|
+
}
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return {
|
|
166
|
+
"H": None, "K": None, "L": None,
|
|
167
|
+
"tth": tth, "theta": theta, "phi": phi, "chi": chi,
|
|
168
|
+
"success": False,
|
|
169
|
+
"error": str(e),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def is_initialized(self) -> bool:
|
|
173
|
+
"""Check if the calculator is initialized."""
|
|
174
|
+
return self._initialized
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Feature packages.
|
|
2
|
+
|
|
3
|
+
Note: Controllers are not exported here to avoid circular imports.
|
|
4
|
+
Import them directly from their modules:
|
|
5
|
+
from advisor.features.scattering_geometry.controllers import ScatteringGeometryController
|
|
6
|
+
from advisor.features.structure_factor.controllers import StructureFactorController
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = []
|
|
@@ -105,6 +105,18 @@ class BrillouinCalculator:
|
|
|
105
105
|
print(f"Error initializing calculator: {str(e)}")
|
|
106
106
|
return False
|
|
107
107
|
|
|
108
|
+
def change_energy(self, energy):
|
|
109
|
+
"""Change the energy of the X-ray source, in eV"""
|
|
110
|
+
self.energy = energy
|
|
111
|
+
self.lambda_A = self.ev_to_lambda / self.energy
|
|
112
|
+
self.k_in = 2 * np.pi / self.lambda_A
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def reorient_sample(self, roll, pitch, yaw):
|
|
116
|
+
"""Reorient the sample with respect to the lab"""
|
|
117
|
+
self.lab.reorient(roll, pitch, yaw)
|
|
118
|
+
return True
|
|
119
|
+
|
|
108
120
|
def _sample_to_lab_conversion(self, a_vec, b_vec, c_vec):
|
|
109
121
|
"""Convert vectors from sample coordinate system to lab coordinate system."""
|
|
110
122
|
# For now, just return the same vectors
|
|
@@ -253,7 +253,7 @@ def _calculate_angles_chi_fixed(
|
|
|
253
253
|
yaw,
|
|
254
254
|
chi_fixed,
|
|
255
255
|
target_objective=1e-10,
|
|
256
|
-
max_restarts=
|
|
256
|
+
max_restarts=40,
|
|
257
257
|
):
|
|
258
258
|
"""Calculate scattering angles with chi angle (in degrees) fixed.
|
|
259
259
|
|
|
@@ -268,7 +268,7 @@ def _calculate_angles_chi_fixed(
|
|
|
268
268
|
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
269
269
|
chi_fixed (float): Fixed chi angle in degrees
|
|
270
270
|
target_objective (float, optional): Convergence tolerance for fsolve. Defaults to 1e-10.
|
|
271
|
-
max_restarts (int, optional): Maximum number of random restarts. Defaults to
|
|
271
|
+
max_restarts (int, optional): Maximum number of random restarts. Defaults to 40.
|
|
272
272
|
|
|
273
273
|
Returns:
|
|
274
274
|
dict: Dictionary containing:
|
|
@@ -384,7 +384,7 @@ def _calculate_angles_phi_fixed(
|
|
|
384
384
|
yaw,
|
|
385
385
|
phi_fixed,
|
|
386
386
|
target_objective=1e-10,
|
|
387
|
-
max_restarts=
|
|
387
|
+
max_restarts=40,
|
|
388
388
|
):
|
|
389
389
|
"""Calculate scattering angles with phi angle fixed.
|
|
390
390
|
|
|
@@ -399,7 +399,7 @@ def _calculate_angles_phi_fixed(
|
|
|
399
399
|
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
400
400
|
phi_fixed (float): Fixed phi angle in degrees
|
|
401
401
|
target_objective (float, optional): Convergence tolerance for fsolve. Defaults to 1e-10.
|
|
402
|
-
max_restarts (int, optional): Maximum number of random restarts. Defaults to
|
|
402
|
+
max_restarts (int, optional): Maximum number of random restarts. Defaults to 40.
|
|
403
403
|
|
|
404
404
|
Returns:
|
|
405
405
|
dict: Dictionary containing:
|
|
@@ -0,0 +1,264 @@
|
|
|
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 Diffraction 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 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
|
+
button_layout.addWidget(self.calculate_btn)
|
|
120
|
+
|
|
121
|
+
self.apply_btn = QPushButton("Apply and Close")
|
|
122
|
+
self.apply_btn.clicked.connect(self._apply_and_close)
|
|
123
|
+
self.apply_btn.setEnabled(False)
|
|
124
|
+
button_layout.addWidget(self.apply_btn)
|
|
125
|
+
|
|
126
|
+
cancel_btn = QPushButton("Cancel")
|
|
127
|
+
cancel_btn.clicked.connect(self.reject)
|
|
128
|
+
button_layout.addWidget(cancel_btn)
|
|
129
|
+
|
|
130
|
+
layout.addLayout(button_layout)
|
|
131
|
+
|
|
132
|
+
def _add_row(self):
|
|
133
|
+
"""Add a new empty row to the table."""
|
|
134
|
+
row = self.table.rowCount()
|
|
135
|
+
self.table.insertRow(row)
|
|
136
|
+
|
|
137
|
+
# Set default values (H, K, L, energy, tth, theta, phi, chi)
|
|
138
|
+
defaults = [0.0, 0.0, 0.0, 2200.0, 90.0, 45.0, 0.0, 0.0]
|
|
139
|
+
for col, default in enumerate(defaults):
|
|
140
|
+
item = QTableWidgetItem(str(default))
|
|
141
|
+
item.setTextAlignment(Qt.AlignCenter)
|
|
142
|
+
self.table.setItem(row, col, item)
|
|
143
|
+
|
|
144
|
+
def _remove_selected_row(self):
|
|
145
|
+
"""Remove the currently selected row."""
|
|
146
|
+
current_row = self.table.currentRow()
|
|
147
|
+
if current_row >= 0:
|
|
148
|
+
self.table.removeRow(current_row)
|
|
149
|
+
elif self.table.rowCount() > 0:
|
|
150
|
+
# If no row selected, remove the last row
|
|
151
|
+
self.table.removeRow(self.table.rowCount() - 1)
|
|
152
|
+
|
|
153
|
+
def _get_diffraction_tests(self) -> list:
|
|
154
|
+
"""Extract diffraction test data from the table.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of dictionaries containing test data, or None if validation fails.
|
|
158
|
+
"""
|
|
159
|
+
tests = []
|
|
160
|
+
for row in range(self.table.rowCount()):
|
|
161
|
+
try:
|
|
162
|
+
test = {
|
|
163
|
+
"H": float(self.table.item(row, 0).text()),
|
|
164
|
+
"K": float(self.table.item(row, 1).text()),
|
|
165
|
+
"L": float(self.table.item(row, 2).text()),
|
|
166
|
+
"energy": float(self.table.item(row, 3).text()),
|
|
167
|
+
"tth": float(self.table.item(row, 4).text()),
|
|
168
|
+
"theta": float(self.table.item(row, 5).text()),
|
|
169
|
+
"phi": float(self.table.item(row, 6).text()),
|
|
170
|
+
"chi": float(self.table.item(row, 7).text()),
|
|
171
|
+
}
|
|
172
|
+
tests.append(test)
|
|
173
|
+
except (ValueError, AttributeError) as e:
|
|
174
|
+
QMessageBox.warning(
|
|
175
|
+
self,
|
|
176
|
+
"Invalid Input",
|
|
177
|
+
f"Row {row + 1} contains invalid data. Please enter numeric values.\n\nError: {e}",
|
|
178
|
+
)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
if not tests:
|
|
182
|
+
QMessageBox.warning(
|
|
183
|
+
self,
|
|
184
|
+
"No Data",
|
|
185
|
+
"Please enter at least one diffraction test.",
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
return tests
|
|
190
|
+
|
|
191
|
+
def _calculate_orientation(self):
|
|
192
|
+
"""Calculate the optimal orientation from the entered data."""
|
|
193
|
+
tests = self._get_diffraction_tests()
|
|
194
|
+
if tests is None:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Run the fitting algorithm
|
|
198
|
+
result = fit_orientation_from_diffraction_tests(
|
|
199
|
+
self.lattice_params, tests
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not result["success"]:
|
|
203
|
+
QMessageBox.warning(
|
|
204
|
+
self,
|
|
205
|
+
"Calculation Failed",
|
|
206
|
+
f"Failed to calculate orientation:\n\n{result.get('message', 'Unknown error')}",
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Display results (overwrites any previous results)
|
|
211
|
+
self.roll_result.setValue(result["roll"])
|
|
212
|
+
self.pitch_result.setValue(result["pitch"])
|
|
213
|
+
self.yaw_result.setValue(result["yaw"])
|
|
214
|
+
self.error_label.setText(f"Residual Error: {result['residual_error']:.6f}")
|
|
215
|
+
|
|
216
|
+
# Update group title to indicate results are current
|
|
217
|
+
self.results_group.setTitle("Calculated Orientation (Updated)")
|
|
218
|
+
self.results_group.setVisible(True)
|
|
219
|
+
self.apply_btn.setEnabled(True)
|
|
220
|
+
|
|
221
|
+
# Store result for later retrieval
|
|
222
|
+
self.result = {
|
|
223
|
+
"roll": result["roll"],
|
|
224
|
+
"pitch": result["pitch"],
|
|
225
|
+
"yaw": result["yaw"],
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Show detailed errors if available
|
|
229
|
+
if result.get("individual_errors"):
|
|
230
|
+
error_text = "Individual test errors:\n"
|
|
231
|
+
for i, err in enumerate(result["individual_errors"]):
|
|
232
|
+
error_text += (
|
|
233
|
+
f" Test {i+1}: ΔH={err['H_error']:.4f}, "
|
|
234
|
+
f"ΔK={err['K_error']:.4f}, ΔL={err['L_error']:.4f}\n"
|
|
235
|
+
)
|
|
236
|
+
QMessageBox.information(
|
|
237
|
+
self,
|
|
238
|
+
"Calculation Complete",
|
|
239
|
+
f"Orientation calculated successfully!\n\n"
|
|
240
|
+
f"Roll: {result['roll']:.4f}°\n"
|
|
241
|
+
f"Pitch: {result['pitch']:.4f}°\n"
|
|
242
|
+
f"Yaw: {result['yaw']:.4f}°\n\n"
|
|
243
|
+
f"Residual Error: {result['residual_error']:.6f}\n\n"
|
|
244
|
+
f"{error_text}",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _apply_and_close(self):
|
|
248
|
+
"""Apply the calculated orientation and close the dialog."""
|
|
249
|
+
if self.result is not None:
|
|
250
|
+
self.accept()
|
|
251
|
+
else:
|
|
252
|
+
QMessageBox.warning(
|
|
253
|
+
self,
|
|
254
|
+
"No Result",
|
|
255
|
+
"Please calculate the orientation first.",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def get_result(self) -> dict:
|
|
259
|
+
"""Get the calculated orientation.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dictionary with roll, pitch, yaw values, or None if not calculated.
|
|
263
|
+
"""
|
|
264
|
+
return self.result
|
|
@@ -19,6 +19,7 @@ from PyQt5.QtGui import QDragEnterEvent, QDropEvent
|
|
|
19
19
|
from advisor.domain import UnitConverter
|
|
20
20
|
from advisor.ui.utils import readcif
|
|
21
21
|
from advisor.ui.visualizers import CoordinateVisualizer, UnitcellVisualizer
|
|
22
|
+
from advisor.ui.dialogs import DiffractionTestDialog
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class DragDropLineEdit(QLineEdit):
|
|
@@ -219,6 +220,14 @@ class InitWindow(QWidget):
|
|
|
219
220
|
self.yaw_input.valueChanged.connect(self.update_visualization)
|
|
220
221
|
euler_layout.addRow("Yaw:", self.yaw_input)
|
|
221
222
|
|
|
223
|
+
# Add "Import from Diffraction Test" button
|
|
224
|
+
self.import_orientation_btn = QPushButton("Import from Diffraction Test")
|
|
225
|
+
self.import_orientation_btn.setToolTip(
|
|
226
|
+
"Calculate Euler angles from known diffraction measurements"
|
|
227
|
+
)
|
|
228
|
+
self.import_orientation_btn.clicked.connect(self.open_diffraction_test_dialog)
|
|
229
|
+
euler_layout.addRow(self.import_orientation_btn)
|
|
230
|
+
|
|
222
231
|
# Add euler group to main layout at (0,2)
|
|
223
232
|
layout.addWidget(euler_group, 0, 2)
|
|
224
233
|
|
|
@@ -283,6 +292,30 @@ class InitWindow(QWidget):
|
|
|
283
292
|
self.file_path_input.setText(file_path)
|
|
284
293
|
# on_cif_file_changed will be triggered by textChanged
|
|
285
294
|
|
|
295
|
+
@pyqtSlot()
|
|
296
|
+
def open_diffraction_test_dialog(self):
|
|
297
|
+
"""Open the diffraction test dialog to import orientation from measurements."""
|
|
298
|
+
# Gather current lattice parameters
|
|
299
|
+
lattice_params = {
|
|
300
|
+
"a": self.a_input.value(),
|
|
301
|
+
"b": self.b_input.value(),
|
|
302
|
+
"c": self.c_input.value(),
|
|
303
|
+
"alpha": self.alpha_input.value(),
|
|
304
|
+
"beta": self.beta_input.value(),
|
|
305
|
+
"gamma": self.gamma_input.value(),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Open the dialog
|
|
309
|
+
dialog = DiffractionTestDialog(lattice_params, self)
|
|
310
|
+
if dialog.exec_() == DiffractionTestDialog.Accepted:
|
|
311
|
+
result = dialog.get_result()
|
|
312
|
+
if result is not None:
|
|
313
|
+
# Apply the calculated Euler angles
|
|
314
|
+
self.roll_input.setValue(result["roll"])
|
|
315
|
+
self.pitch_input.setValue(result["pitch"])
|
|
316
|
+
self.yaw_input.setValue(result["yaw"])
|
|
317
|
+
# update_visualization will be triggered by valueChanged signals
|
|
318
|
+
|
|
286
319
|
def set_lattice_inputs_enabled(self, enabled: bool):
|
|
287
320
|
"""Enable/disable lattice parameter inputs (a,b,c,alpha,beta,gamma)."""
|
|
288
321
|
self.a_input.setEnabled(enabled)
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,8 @@ advisor/controllers/app_controller.py
|
|
|
9
9
|
advisor/controllers/feature_controller.py
|
|
10
10
|
advisor/domain/__init__.py
|
|
11
11
|
advisor/domain/geometry.py
|
|
12
|
+
advisor/domain/orientation.py
|
|
13
|
+
advisor/domain/orientation_calculator.py
|
|
12
14
|
advisor/domain/unit_converter.py
|
|
13
15
|
advisor/domain/core/__init__.py
|
|
14
16
|
advisor/domain/core/lab.py
|
|
@@ -56,6 +58,8 @@ advisor/ui/init_window.py
|
|
|
56
58
|
advisor/ui/main_window.py
|
|
57
59
|
advisor/ui/tab_interface.py
|
|
58
60
|
advisor/ui/tips.py
|
|
61
|
+
advisor/ui/dialogs/__init__.py
|
|
62
|
+
advisor/ui/dialogs/diffraction_test_dialog.py
|
|
59
63
|
advisor/ui/utils/__init__.py
|
|
60
64
|
advisor/ui/utils/readcif.py
|
|
61
65
|
advisor/ui/visualizers/HKLScan2DVisualizer.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "advisor-scattering"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.1"
|
|
8
8
|
description = "Advisor-Scattering: Advanced Visual X-ray Scattering Toolkit for Reciprocal-space visualization and calculation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
"""Feature packages."""
|
|
2
|
-
|
|
3
|
-
from advisor.features.scattering_geometry.controllers import ScatteringGeometryController
|
|
4
|
-
from advisor.features.structure_factor.controllers import StructureFactorController
|
|
5
|
-
|
|
6
|
-
__all__ = ["ScatteringGeometryController", "StructureFactorController"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/controllers/feature_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/config/app_config.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/bz_caculator.jpg
RENAMED
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/bz_calculator.png
RENAMED
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/placeholder.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/sf_calculator.jpg
RENAMED
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/resources/icons/sf_calculator.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/HKLScan2DVisualizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor/ui/visualizers/unitcell_visualizer.py
RENAMED
|
File without changes
|
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/requires.txt
RENAMED
|
File without changes
|
{advisor_scattering-0.5.3 → advisor_scattering-0.9.1}/advisor_scattering.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|