advisor-scattering 0.5.3__py3-none-any.whl → 0.9.1__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.
@@ -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, StructureFactorController
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 sample.
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 rotate(self, theta, phi, chi):
101
- """Rotate the sample."""
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
@@ -1,6 +1,9 @@
1
- """Feature packages."""
1
+ """Feature packages.
2
2
 
3
- from advisor.features.scattering_geometry.controllers import ScatteringGeometryController
4
- from advisor.features.structure_factor.controllers import StructureFactorController
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
+ """
5
8
 
6
- __all__ = ["ScatteringGeometryController", "StructureFactorController"]
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=20,
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 20.
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=20,
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 20.
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,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,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
advisor/ui/init_window.py CHANGED
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: advisor-scattering
3
- Version: 0.5.3
3
+ Version: 0.9.1
4
4
  Summary: Advisor-Scattering: Advanced Visual X-ray Scattering Toolkit for Reciprocal-space visualization and calculation
5
5
  Author: Xunyang Hong
6
6
  License: MIT
@@ -2,21 +2,23 @@ advisor/__init__.py,sha256=9gmF8qfMHMVLbmvWW_20KkHU5zOCwUmmFHtju21klPQ,103
2
2
  advisor/__main__.py,sha256=FueZKRQwCGBKNtk4W_Z9yYpsI0YB5WiVREWyZXmSl8A,124
3
3
  advisor/app.py,sha256=52DxOOlZV_NHUzsnuYCCo_cI48D2THKE1m0-1ZVoNvE,1165
4
4
  advisor/controllers/__init__.py,sha256=DEzJDUTJcRrbe7ZYnYsQQ3NvnyayNwTafcQmY7aRtrw,198
5
- advisor/controllers/app_controller.py,sha256=cl7sKgfyz9J2o7RLgP18mExNjU_FDK5rUPrdmI15b1g,2344
5
+ advisor/controllers/app_controller.py,sha256=CGqlhez9467JMZZeMbnbF-HwqwCayoYIfajy4vRBZtE,2433
6
6
  advisor/controllers/feature_controller.py,sha256=6_XOck8ipQxbef24y16tCE08q7OUT0gs285d5tF5XRs,642
7
- advisor/domain/__init__.py,sha256=Lkpnerzg6qHzk-u4jOAU1rg6rYY4iIorIECdOTPd2DY,536
7
+ advisor/domain/__init__.py,sha256=SuMfLp1CJFqrpBE6UeyVe9BWdjbBlcwuHRTBN-UsOzg,733
8
8
  advisor/domain/geometry.py,sha256=E8YmecQpBh7PQeJIamOlPo2aBgJSbhyBlrYS02uxFMg,6856
9
+ advisor/domain/orientation.py,sha256=PDPxosByY_Yo7FePUAZLsrN1OcH4_kUiF8TwC5jFGL8,7649
10
+ advisor/domain/orientation_calculator.py,sha256=5AKd5OqsQWTKgB3kv83aM7eLq9TEAlPXJ-C79X65dV8,5872
9
11
  advisor/domain/unit_converter.py,sha256=D20FvdV7_ilAX7KDPxQ1GDCA6IxdjjcQ2QBiahb3lLI,2085
10
12
  advisor/domain/core/__init__.py,sha256=UPDQYo7-5f9Ghx3p6qlwljTLhHsxlGmEl_MvdaHM2Jg,173
11
- advisor/domain/core/lab.py,sha256=pLA5DhG_LMbJhbEABN53BU0FdTUAZbA38jiPjXzd29g,4871
12
- advisor/domain/core/lattice.py,sha256=UXxkZG6iHGLXwHPEH6VQLXAg274rD3BTpq4ObDIOxIg,2728
13
- advisor/domain/core/sample.py,sha256=iMPvQprPGllp6WfwnsESzNode4tujYNbVHhcNL6Uhuk,4105
14
- advisor/features/__init__.py,sha256=wQYN67pLXVgxmsoG70WJ51BnE9ACUgbpx-G740KHsEA,272
13
+ advisor/domain/core/lab.py,sha256=_IbRVbPvffkM73f41uJ3R43ZeZ6LOxNOrywWz_DDT_8,5129
14
+ advisor/domain/core/lattice.py,sha256=339_veWj1KFTAwuH37-vx5a9wjUfQzJI6MoSxMm8tX8,2757
15
+ advisor/domain/core/sample.py,sha256=zz0qMlpeGHEXmmkoY1GtOBI5-uK4hhOVeLjuje6-Osc,4278
16
+ advisor/features/__init__.py,sha256=Arhqas6uY2uJp58ttNEVap9gRYE0uOVw1dA6gtB9544,330
15
17
  advisor/features/scattering_geometry/controllers/__init__.py,sha256=2V9KZMlxjHpsBIdkphveylQgL9z6BV09DaufqQcLX_Y,169
16
18
  advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py,sha256=QGcm5_y8EPNYUF_FM9xViVzPJs6qh1dB_OA3fjoKAEg,1076
17
19
  advisor/features/scattering_geometry/domain/__init__.py,sha256=BsK0j9-RJiK0-g0qB3DH8qoSqlEZNAPBQb7eXYrg0oU,142
18
- advisor/features/scattering_geometry/domain/brillouin_calculator.py,sha256=_vpR8T1j1h-agZOlxk4xfpBq1XmbrZsssgookIqYwr0,15620
19
- advisor/features/scattering_geometry/domain/core.py,sha256=OQ7gZ19XwZPtC3mXukkzlmOiDmgjvKBrVzRNJRi1OuU,20149
20
+ advisor/features/scattering_geometry/domain/brillouin_calculator.py,sha256=pMpjq5HbutFzWubo-x7goOY2N_7OjqALYZN8zGQkfsQ,16044
21
+ advisor/features/scattering_geometry/domain/core.py,sha256=2JdHNjdl_GnyhOr0iGRMgTK1JlYFg6WA0uWe0mYUgyY,20149
20
22
  advisor/features/scattering_geometry/ui/__init__.py,sha256=9Lm-sc1Q3KeQ-0kKJneD10MWuqyDgh29QDAVdHghh1Q,154
21
23
  advisor/features/scattering_geometry/ui/scattering_geometry_tab.py,sha256=GHczdi74GxCxo3svF7oBdXrbTilLZ4ODkTuatJrfTkE,29448
22
24
  advisor/features/scattering_geometry/ui/components/__init__.py,sha256=OsX27CeUv5gznYXmkZWqeXwcskf4DmxOH6xJ0-cUwDg,358
@@ -49,10 +51,12 @@ advisor/resources/icons/sf_calculator.jpg,sha256=W7e89x2K6np5l200hklRfdh0pkA3SGz
49
51
  advisor/resources/icons/sf_calculator.png,sha256=ArnSfIQo8Gc8uWQhpG7DTCBNFcuVGXDIiQ7Yb-x9D_M,44466
50
52
  advisor/resources/qss/styles.qss,sha256=R29qftSIvN0AI1UVsLSsHcUImIYGnbbuF_1LvViF3mg,8070
51
53
  advisor/ui/__init__.py,sha256=isDNFKHsZq1s8Och-5tHcFC5FAJA653x4r5DYGsVowU,202
52
- advisor/ui/init_window.py,sha256=pOxGUfKcObnQKcGDKtEbBmQ18yOOfwEY1SxhHO3DRqs,22273
54
+ advisor/ui/init_window.py,sha256=dQTTLNNiBVyDtSKqFOPicg3Su75ikfCKBB8Avy6_6_4,23751
53
55
  advisor/ui/main_window.py,sha256=I5vCpPPQCwaSag538YPQC_bgarVJGQ1ryuxjWxuMP-0,6170
54
56
  advisor/ui/tab_interface.py,sha256=8okMV0NurIyEGxRf-YXHrjftzWyMzXcea3cdt3i1FtE,1180
55
57
  advisor/ui/tips.py,sha256=5iP28xJvKBkdgvo3eDD2kzV52FEeT_Sd8ivtWlhi0Xs,742
58
+ advisor/ui/dialogs/__init__.py,sha256=yFeoZOe0euOXeie-fqwJquZ9Vhl8mtszIWg1aunawXI,197
59
+ advisor/ui/dialogs/diffraction_test_dialog.py,sha256=33MZCYlSn0ZhSp-Q5Cj1n0xbgbtSJugCD-KaiZCmxcY,9608
56
60
  advisor/ui/utils/__init__.py,sha256=U_IwI_KEtmKLMKw6e2ko7GkUa50-PvkeMFS_6QfHoyg,84
57
61
  advisor/ui/utils/readcif.py,sha256=tRuz1WSBr8XWkgf6VUOlS5aTf2LYDi8CY4peJ1_0bjI,4554
58
62
  advisor/ui/visualizers/HKLScan2DVisualizer.py,sha256=Q8_MWKiyzrFk4-3S50Ro7_4I_ERbSagG52686lPSuFo,8706
@@ -62,8 +66,8 @@ advisor/ui/visualizers/scattering_visualizer.py,sha256=iyjTgy1WbqI7G0BpiJ59CKFCI
62
66
  advisor/ui/visualizers/structure_factor_visualizer.py,sha256=Rt9ui7Qv4vvDCWiuIngoYzHJlFVef8M_h3BBRcmuFTc,15866
63
67
  advisor/ui/visualizers/structure_factor_visualizer_2d.py,sha256=xA1hrp7pKIfgD1G_Pt-uemHggmKf-7QC94dS-GasZI4,8119
64
68
  advisor/ui/visualizers/unitcell_visualizer.py,sha256=gwpMkU1YIu-8zkKVu5bHpyu_iLbzYEYTbVArchNMLOI,21384
65
- advisor_scattering-0.5.3.dist-info/METADATA,sha256=flwNil7WdTQXXr4pfw32egU2fBC6B7ewfvzcwMLH1Fg,4658
66
- advisor_scattering-0.5.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
67
- advisor_scattering-0.5.3.dist-info/entry_points.txt,sha256=b__0W6TTAPUTc0UIUyHdxvxtsVz025Y-QNrMKzt8b9o,83
68
- advisor_scattering-0.5.3.dist-info/top_level.txt,sha256=4YoJT7oclQ2yU-4KY0AB36cfAZG9yAI1tJNHrxEioMA,8
69
- advisor_scattering-0.5.3.dist-info/RECORD,,
69
+ advisor_scattering-0.9.1.dist-info/METADATA,sha256=rg86280T-GTYofxoa9BMwSJcNEcP_Rmf8JoHECbDP60,4658
70
+ advisor_scattering-0.9.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
71
+ advisor_scattering-0.9.1.dist-info/entry_points.txt,sha256=b__0W6TTAPUTc0UIUyHdxvxtsVz025Y-QNrMKzt8b9o,83
72
+ advisor_scattering-0.9.1.dist-info/top_level.txt,sha256=4YoJT7oclQ2yU-4KY0AB36cfAZG9yAI1tJNHrxEioMA,8
73
+ advisor_scattering-0.9.1.dist-info/RECORD,,