advisor-scattering 0.5.3__tar.gz → 0.9.5__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-0.9.5}/PKG-INFO +4 -2
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/README.md +3 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/controllers/app_controller.py +2 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/__init__.py +4 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/core/lab.py +9 -2
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/core/lattice.py +2 -5
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/core/sample.py +9 -2
- advisor_scattering-0.9.5/advisor/domain/orientation.py +219 -0
- advisor_scattering-0.9.5/advisor/domain/orientation_calculator.py +173 -0
- advisor_scattering-0.9.5/advisor/features/__init__.py +9 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/domain/brillouin_calculator.py +43 -1
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/domain/core.py +4 -4
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +10 -18
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/components/hk_angles_components.py +16 -29
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +14 -25
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +18 -29
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +9 -1
- advisor_scattering-0.9.5/advisor/ui/dialogs/__init__.py +7 -0
- advisor_scattering-0.9.5/advisor/ui/dialogs/diffraction_test_dialog.py +287 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/init_window.py +39 -15
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/HKLScan2DVisualizer.py +37 -2
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/PKG-INFO +4 -2
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/SOURCES.txt +4 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/pyproject.toml +1 -1
- advisor_scattering-0.5.3/advisor/features/__init__.py +0 -6
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/MANIFEST.in +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/__main__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/app.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/controllers/feature_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/core/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/geometry.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/domain/unit_converter.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/domain/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/scattering_geometry/ui/components/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/controllers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/controllers/structure_factor_controller.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/domain/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/domain/structure_factor_calculator.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/ui/components/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/ui/components/customized_plane_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/ui/components/hkl_plane_components.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/features/structure_factor/ui/structure_factor_tab.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/config/app_config.json +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/config/tips.json +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/data/nacl.cif +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/bz_caculator.jpg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/bz_calculator.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/minus.svg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/placeholder.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/plus.svg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/reset.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/sf_calculator.jpg +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons/sf_calculator.png +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/icons.qrc +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/qss/styles.qss +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/resources/resources_rc.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/main_window.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/tab_interface.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/tips.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/utils/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/utils/readcif.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/__init__.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/coordinate_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/scattering_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/structure_factor_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/structure_factor_visualizer_2d.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor/ui/visualizers/unitcell_visualizer.py +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/dependency_links.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/entry_points.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/requires.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/advisor_scattering.egg-info/top_level.txt +0 -0
- {advisor_scattering-0.5.3 → advisor_scattering-0.9.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: advisor-scattering
|
|
3
|
-
Version: 0.5
|
|
3
|
+
Version: 0.9.5
|
|
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
|
|
@@ -37,6 +37,7 @@ or use the link below to view the demo video.
|
|
|
37
37
|
- Visualize scattering geometry and unit cells
|
|
38
38
|
- Compute and visualize structure factors in reciprocal space.
|
|
39
39
|
- CIF file drop-in support
|
|
40
|
+
- Import crystal orientation from diffraction test data
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
## Install
|
|
@@ -75,7 +76,8 @@ python -m advisor
|
|
|
75
76
|
### 1. Initialization window
|
|
76
77
|
- Enter lattice constants (a, b, c) and angles (alpha, beta, gamma); beam energy auto-updates wavelength/|k|.
|
|
77
78
|
- Optional: drop a CIF to autofill lattice parameters and preview the unit cell.
|
|
78
|
-
- Adjust Euler angles (roll, pitch, yaw) to orient the sample relative to the
|
|
79
|
+
- Adjust Euler angles (roll, pitch, yaw) to orient the lattice/sample relative to the goniometer.
|
|
80
|
+
- **New:** Use **Import from Diffraction Test** to automatically determine Euler angles from known diffraction measurements.
|
|
79
81
|
- Click **Initialize** to load the main interface and pass parameters to all tabs.
|
|
80
82
|
|
|
81
83
|
### 2. Scattering Geometry tab
|
|
@@ -15,6 +15,7 @@ or use the link below to view the demo video.
|
|
|
15
15
|
- Visualize scattering geometry and unit cells
|
|
16
16
|
- Compute and visualize structure factors in reciprocal space.
|
|
17
17
|
- CIF file drop-in support
|
|
18
|
+
- Import crystal orientation from diffraction test data
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
## Install
|
|
@@ -53,7 +54,8 @@ python -m advisor
|
|
|
53
54
|
### 1. Initialization window
|
|
54
55
|
- Enter lattice constants (a, b, c) and angles (alpha, beta, gamma); beam energy auto-updates wavelength/|k|.
|
|
55
56
|
- Optional: drop a CIF to autofill lattice parameters and preview the unit cell.
|
|
56
|
-
- Adjust Euler angles (roll, pitch, yaw) to orient the sample relative to the
|
|
57
|
+
- Adjust Euler angles (roll, pitch, yaw) to orient the lattice/sample relative to the goniometer.
|
|
58
|
+
- **New:** Use **Import from Diffraction Test** to automatically determine Euler angles from known diffraction measurements.
|
|
57
59
|
- Click **Initialize** to load the main interface and pass parameters to all tabs.
|
|
58
60
|
|
|
59
61
|
### 2. Scattering Geometry tab
|
|
@@ -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,173 @@
|
|
|
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 version of Brillouincalculator for orientation fitting (Set UB Matrix calculation).
|
|
18
|
+
|
|
19
|
+
This class provides only the methods needed for fitting crystal orientation (UB matrix)
|
|
20
|
+
from diffraction data.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Physical constants
|
|
24
|
+
EV_TO_LAMBDA = 12398.42 # eV to Angstrom conversion
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize the calculator."""
|
|
28
|
+
self._initialized = False
|
|
29
|
+
self.lab = Lab()
|
|
30
|
+
self.energy = None
|
|
31
|
+
self.lambda_A = None
|
|
32
|
+
self.k_in = None
|
|
33
|
+
|
|
34
|
+
def initialize(self, params: dict) -> bool:
|
|
35
|
+
"""Initialize with lattice parameters.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
params: Dictionary containing:
|
|
39
|
+
- a, b, c (float): Lattice constants in Angstroms
|
|
40
|
+
- alpha, beta, gamma (float): Lattice angles in degrees
|
|
41
|
+
- energy (float): X-ray energy in eV
|
|
42
|
+
- roll, pitch, yaw (float, optional): Euler angles in degrees
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: True if initialization was successful
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
a = params.get("a", 4.0)
|
|
49
|
+
b = params.get("b", 4.0)
|
|
50
|
+
c = params.get("c", 12.0)
|
|
51
|
+
alpha = params.get("alpha", 90.0)
|
|
52
|
+
beta = params.get("beta", 90.0)
|
|
53
|
+
gamma = params.get("gamma", 90.0)
|
|
54
|
+
roll = params.get("roll", 0.0)
|
|
55
|
+
pitch = params.get("pitch", 0.0)
|
|
56
|
+
yaw = params.get("yaw", 0.0)
|
|
57
|
+
self.energy = params["energy"]
|
|
58
|
+
|
|
59
|
+
# Initialize lab with default sample rotation (0, 0, 0)
|
|
60
|
+
self.lab.initialize(a, b, c, alpha, beta, gamma, roll, pitch, yaw, 0, 0, 0)
|
|
61
|
+
|
|
62
|
+
# Calculate wavelength and wavevector
|
|
63
|
+
self.lambda_A = self.EV_TO_LAMBDA / self.energy
|
|
64
|
+
self.k_in = 2 * np.pi / self.lambda_A
|
|
65
|
+
|
|
66
|
+
self._initialized = True
|
|
67
|
+
return True
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"Error initializing OrientationCalculator: {e}")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def change_energy(self, energy: float) -> bool:
|
|
73
|
+
"""Change the X-ray energy.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
energy: X-ray energy in eV
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: True if successful
|
|
80
|
+
"""
|
|
81
|
+
self.energy = energy
|
|
82
|
+
self.lambda_A = self.EV_TO_LAMBDA / self.energy
|
|
83
|
+
self.k_in = 2 * np.pi / self.lambda_A
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def reorient_sample(self, roll: float, pitch: float, yaw: float) -> bool:
|
|
87
|
+
"""Reorient the sample (change Euler angles).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
roll, pitch, yaw: Euler angles in degrees
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
bool: True if successful
|
|
94
|
+
"""
|
|
95
|
+
self.lab.reorient(roll, pitch, yaw)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def calculate_hkl(
|
|
99
|
+
self, tth: float, theta: float, phi: float, chi: float
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Calculate HKL from scattering angles.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
tth: Scattering angle 2θ in degrees
|
|
105
|
+
theta: Sample theta rotation in degrees
|
|
106
|
+
phi: Sample phi rotation in degrees
|
|
107
|
+
chi: Sample chi rotation in degrees
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict: Dictionary containing:
|
|
111
|
+
- H, K, L (float): Miller indices
|
|
112
|
+
- tth, theta, phi, chi (float): Input angles
|
|
113
|
+
- success (bool): Whether calculation succeeded
|
|
114
|
+
- error (str or None): Error message if any
|
|
115
|
+
"""
|
|
116
|
+
if not self._initialized:
|
|
117
|
+
return {
|
|
118
|
+
"H": None, "K": None, "L": None,
|
|
119
|
+
"tth": tth, "theta": theta, "phi": phi, "chi": chi,
|
|
120
|
+
"success": False,
|
|
121
|
+
"error": "Calculator not initialized",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Get real space vectors in lab frame
|
|
126
|
+
a_vec_lab, b_vec_lab, c_vec_lab = self.lab.get_real_space_vectors()
|
|
127
|
+
|
|
128
|
+
# Calculate momentum transfer magnitude
|
|
129
|
+
k_magnitude = 2.0 * self.k_in * np.sin(np.radians(tth / 2.0))
|
|
130
|
+
|
|
131
|
+
# Calculate delta angle
|
|
132
|
+
delta = 90 - (tth / 2.0)
|
|
133
|
+
sin_delta = np.sin(np.radians(delta))
|
|
134
|
+
cos_delta = np.cos(np.radians(delta))
|
|
135
|
+
|
|
136
|
+
# Momentum transfer at theta, phi, chi = 0
|
|
137
|
+
k_vec_initial = np.array(
|
|
138
|
+
[-k_magnitude * sin_delta, -k_magnitude * cos_delta, 0.0]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Rotation of the beam is the reverse rotation of the sample
|
|
142
|
+
rotation_matrix = angle_to_matrix(theta, phi, chi).T
|
|
143
|
+
|
|
144
|
+
# Momentum transfer at the given angles
|
|
145
|
+
k_vec_lab = rotation_matrix @ k_vec_initial
|
|
146
|
+
|
|
147
|
+
# Calculate HKL by projecting onto real space vectors
|
|
148
|
+
H = np.dot(k_vec_lab, a_vec_lab) / (2 * np.pi)
|
|
149
|
+
K = np.dot(k_vec_lab, b_vec_lab) / (2 * np.pi)
|
|
150
|
+
L = np.dot(k_vec_lab, c_vec_lab) / (2 * np.pi)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"H": H,
|
|
154
|
+
"K": K,
|
|
155
|
+
"L": L,
|
|
156
|
+
"tth": tth,
|
|
157
|
+
"theta": theta,
|
|
158
|
+
"phi": phi,
|
|
159
|
+
"chi": chi,
|
|
160
|
+
"success": True,
|
|
161
|
+
"error": None,
|
|
162
|
+
}
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return {
|
|
165
|
+
"H": None, "K": None, "L": None,
|
|
166
|
+
"tth": tth, "theta": theta, "phi": phi, "chi": chi,
|
|
167
|
+
"success": False,
|
|
168
|
+
"error": str(e),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def is_initialized(self) -> bool:
|
|
172
|
+
"""Check if the calculator is initialized."""
|
|
173
|
+
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
|
|
@@ -434,7 +446,8 @@ class BrillouinCalculator:
|
|
|
434
446
|
"error": None,
|
|
435
447
|
"feasible": all_feasible,
|
|
436
448
|
}
|
|
437
|
-
|
|
449
|
+
|
|
450
|
+
|
|
438
451
|
def is_initialized(self):
|
|
439
452
|
"""Check if the calculator is initialized.
|
|
440
453
|
|
|
@@ -459,6 +472,35 @@ class BrillouinCalculator:
|
|
|
459
472
|
"gamma": gamma,
|
|
460
473
|
}
|
|
461
474
|
|
|
475
|
+
|
|
476
|
+
def get_max_hkl_values(self, tth):
|
|
477
|
+
"""Get the maximum HKL values, valid only for orthorhombic crystals.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
tuple: Maximum HKL values
|
|
481
|
+
"""
|
|
482
|
+
if not self.is_initialized():
|
|
483
|
+
raise ValueError("Calculator not initialized")
|
|
484
|
+
a, b, c, alpha, beta, gamma = self.lab.get_lattice_parameters()
|
|
485
|
+
k_magnitude = self.get_k_magnitude(tth)
|
|
486
|
+
h_max = k_magnitude / (2 * np.pi / a)
|
|
487
|
+
k_max = k_magnitude / (2 * np.pi / b)
|
|
488
|
+
l_max = k_magnitude / (2 * np.pi / c)
|
|
489
|
+
if alpha != 90.0 or beta != 90.0 or gamma != 90.0:
|
|
490
|
+
success = False
|
|
491
|
+
message = "The crystal is not orthorhombic,\nthe accesible area is just approximate."
|
|
492
|
+
else:
|
|
493
|
+
success = True
|
|
494
|
+
message = None
|
|
495
|
+
return {
|
|
496
|
+
"h_max": h_max,
|
|
497
|
+
"k_max": k_max,
|
|
498
|
+
"l_max": l_max,
|
|
499
|
+
"success": success,
|
|
500
|
+
"message": message,
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
462
504
|
def get_real_space_vectors(self):
|
|
463
505
|
"""Get the real space vectors.
|
|
464
506
|
|
|
@@ -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:
|