advisor-scattering 0.5.2__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__ = []