advisor-scattering 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- advisor/__init__.py +3 -0
- advisor/__main__.py +7 -0
- advisor/app.py +40 -0
- advisor/controllers/__init__.py +6 -0
- advisor/controllers/app_controller.py +69 -0
- advisor/controllers/feature_controller.py +25 -0
- advisor/domain/__init__.py +23 -0
- advisor/domain/core/__init__.py +8 -0
- advisor/domain/core/lab.py +121 -0
- advisor/domain/core/lattice.py +79 -0
- advisor/domain/core/sample.py +101 -0
- advisor/domain/geometry.py +212 -0
- advisor/domain/unit_converter.py +82 -0
- advisor/features/__init__.py +6 -0
- advisor/features/scattering_geometry/controllers/__init__.py +5 -0
- advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
- advisor/features/scattering_geometry/domain/__init__.py +5 -0
- advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
- advisor/features/scattering_geometry/domain/core.py +516 -0
- advisor/features/scattering_geometry/ui/__init__.py +5 -0
- advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
- advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
- advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
- advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
- advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
- advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
- advisor/features/structure_factor/controllers/__init__.py +6 -0
- advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
- advisor/features/structure_factor/domain/__init__.py +6 -0
- advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
- advisor/features/structure_factor/ui/__init__.py +6 -0
- advisor/features/structure_factor/ui/components/__init__.py +12 -0
- advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
- advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
- advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
- advisor/resources/__init__.py +0 -0
- advisor/resources/config/app_config.json +14 -0
- advisor/resources/config/tips.json +4 -0
- advisor/resources/data/nacl.cif +111 -0
- advisor/resources/icons/bz_caculator.jpg +0 -0
- advisor/resources/icons/bz_calculator.png +0 -0
- advisor/resources/icons/minus.svg +3 -0
- advisor/resources/icons/placeholder.png +0 -0
- advisor/resources/icons/plus.svg +3 -0
- advisor/resources/icons/reset.png +0 -0
- advisor/resources/icons/sf_calculator.jpg +0 -0
- advisor/resources/icons/sf_calculator.png +0 -0
- advisor/resources/icons.qrc +6 -0
- advisor/resources/qss/styles.qss +348 -0
- advisor/resources/resources_rc.py +83 -0
- advisor/ui/__init__.py +7 -0
- advisor/ui/init_window.py +566 -0
- advisor/ui/main_window.py +174 -0
- advisor/ui/tab_interface.py +44 -0
- advisor/ui/tips.py +30 -0
- advisor/ui/utils/__init__.py +6 -0
- advisor/ui/utils/readcif.py +129 -0
- advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
- advisor/ui/visualizers/__init__.py +8 -0
- advisor/ui/visualizers/coordinate_visualizer.py +203 -0
- advisor/ui/visualizers/scattering_visualizer.py +301 -0
- advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
- advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
- advisor/ui/visualizers/unitcell_visualizer.py +518 -0
- advisor_scattering-0.5.0.dist-info/METADATA +122 -0
- advisor_scattering-0.5.0.dist-info/RECORD +69 -0
- advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
- advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
- advisor_scattering-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Controller for the Structure Factor feature."""
|
|
2
|
+
|
|
3
|
+
from advisor.controllers.feature_controller import FeatureController
|
|
4
|
+
from advisor.features.structure_factor.domain import StructureFactorCalculator
|
|
5
|
+
from advisor.features.structure_factor.ui.structure_factor_tab import StructureFactorTab
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StructureFactorController(FeatureController):
|
|
9
|
+
"""Manages structure factor calculations."""
|
|
10
|
+
|
|
11
|
+
title = "Structure Factor"
|
|
12
|
+
description = "Calculate structure factors from CIF files using Dans_Diffraction."
|
|
13
|
+
icon = "sf_calculator.png"
|
|
14
|
+
|
|
15
|
+
def __init__(self, app_controller):
|
|
16
|
+
super().__init__(app_controller)
|
|
17
|
+
self.calculator = StructureFactorCalculator()
|
|
18
|
+
self.view = self.build_view()
|
|
19
|
+
|
|
20
|
+
def build_view(self):
|
|
21
|
+
return StructureFactorTab(controller=self, calculator=self.calculator)
|
|
22
|
+
|
|
23
|
+
def set_parameters(self, params: dict):
|
|
24
|
+
if self.view and hasattr(self.view, "set_parameters"):
|
|
25
|
+
self.view.set_parameters(params)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""This is a module responsible for calculating the structure factor of a given crystal
|
|
2
|
+
structure.
|
|
3
|
+
|
|
4
|
+
CONVENTION:
|
|
5
|
+
- energy input is in eV
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import Dans_Diffraction as dif
|
|
10
|
+
import numpy as np
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
DEFAULT_HKL_LIST = [
|
|
14
|
+
[1, 0, 0],
|
|
15
|
+
[0, 1, 0],
|
|
16
|
+
[0, 0, 1],
|
|
17
|
+
[1, 1, 0],
|
|
18
|
+
[1, 0, 1],
|
|
19
|
+
[0, 1, 1],
|
|
20
|
+
[1, 1, 1],
|
|
21
|
+
[2, 0, 0],
|
|
22
|
+
[0, 2, 0],
|
|
23
|
+
[0, 0, 2],
|
|
24
|
+
[2, 1, 0],
|
|
25
|
+
[1, 2, 0],
|
|
26
|
+
[2, 2, 0],
|
|
27
|
+
]
|
|
28
|
+
SCATTERING_TYPE = "xray dispersion"
|
|
29
|
+
METHOD = "x-ray"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StructureFactorCalculator:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.calculator = None
|
|
35
|
+
self._cif_file_path = None
|
|
36
|
+
self._energy = None # in eV
|
|
37
|
+
self._is_initialized = False
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def cif_file_path(self):
|
|
41
|
+
return self._cif_file_path
|
|
42
|
+
|
|
43
|
+
@cif_file_path.setter
|
|
44
|
+
def cif_file_path(self, cif_file_path: str):
|
|
45
|
+
# check if the file exists
|
|
46
|
+
if not os.path.exists(cif_file_path):
|
|
47
|
+
raise FileNotFoundError(f"File {cif_file_path} does not exist.")
|
|
48
|
+
self._cif_file_path = cif_file_path
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_initialized(self):
|
|
52
|
+
return self._is_initialized
|
|
53
|
+
|
|
54
|
+
@is_initialized.setter
|
|
55
|
+
def is_initialized(self, is_initialized: bool):
|
|
56
|
+
self._is_initialized = is_initialized
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def energy(self):
|
|
60
|
+
"""Energy in eV"""
|
|
61
|
+
return self._energy
|
|
62
|
+
|
|
63
|
+
@energy.setter
|
|
64
|
+
def energy(self, energy: float):
|
|
65
|
+
"""Energy in eV"""
|
|
66
|
+
self._energy = energy
|
|
67
|
+
if self.is_initialized:
|
|
68
|
+
self.calculator.Scatter.setup_scatter(
|
|
69
|
+
scattering_type=METHOD, energy_kev=self.energy / 1000
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def initialize(self, cif_file_path: str, energy: float):
|
|
73
|
+
self.cif_file_path = cif_file_path
|
|
74
|
+
self.energy = energy
|
|
75
|
+
self.calculator = dif.Crystal(self.cif_file_path)
|
|
76
|
+
self.calculator.Scatter.setup_scatter(
|
|
77
|
+
scattering_type=METHOD, energy_kev=self.energy / 1000
|
|
78
|
+
)
|
|
79
|
+
self.is_initialized = True
|
|
80
|
+
|
|
81
|
+
def calculate_structure_factors(
|
|
82
|
+
self,
|
|
83
|
+
hkl_input_list: list[list[int]] = None,
|
|
84
|
+
energy: float = None,
|
|
85
|
+
):
|
|
86
|
+
if energy is not None:
|
|
87
|
+
# reset the energy
|
|
88
|
+
self.energy = energy
|
|
89
|
+
if hkl_input_list is None:
|
|
90
|
+
hkl_input_list = DEFAULT_HKL_LIST
|
|
91
|
+
result = self.calculator.Scatter.structure_factor(
|
|
92
|
+
hkl=hkl_input_list, scattering_type=SCATTERING_TYPE
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
result = self.calculator.Scatter.structure_factor(
|
|
96
|
+
hkl=hkl_input_list, scattering_type=SCATTERING_TYPE
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
calculator = StructureFactorCalculator()
|
|
104
|
+
calculator.initialize(cif_file_path="data/nacl.cif", energy=10000)
|
|
105
|
+
result = calculator.calculate_structure_factors()
|
|
106
|
+
for hkl, F_hkl in zip(DEFAULT_HKL_LIST, result):
|
|
107
|
+
print(f"hkl = {hkl}, |F| = {np.abs(F_hkl)}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .hkl_plane_components import (
|
|
2
|
+
HKLPlaneControls,
|
|
3
|
+
HKLPlane3DWidget,
|
|
4
|
+
HKLPlane2DWidget,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .customized_plane_components import (
|
|
8
|
+
CustomizedPlaneControls,
|
|
9
|
+
CustomizedPlane3DWidget,
|
|
10
|
+
CustomizedPlane2DWidget,
|
|
11
|
+
CustomizedPlaneWidget,
|
|
12
|
+
)
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# pylint: disable=no-name-in-module, import-error
|
|
4
|
+
from PyQt5.QtWidgets import (
|
|
5
|
+
QWidget,
|
|
6
|
+
QGridLayout,
|
|
7
|
+
QFormLayout,
|
|
8
|
+
QLabel,
|
|
9
|
+
QPushButton,
|
|
10
|
+
QGroupBox,
|
|
11
|
+
QHBoxLayout,
|
|
12
|
+
QVBoxLayout,
|
|
13
|
+
QSpinBox,
|
|
14
|
+
QLineEdit,
|
|
15
|
+
)
|
|
16
|
+
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
|
17
|
+
import numpy as np
|
|
18
|
+
import sys
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
# Add parent directory to path for imports
|
|
22
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
|
|
23
|
+
from advisor.ui.visualizers import StructureFactorVisualizer3D, StructureFactorVisualizer2D
|
|
24
|
+
from .hkl_plane_components import EnergySpinBox
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CustomizedPlaneControls(QWidget):
|
|
28
|
+
"""Control panel for customized plane visualization with vector inputs."""
|
|
29
|
+
|
|
30
|
+
initializeClicked = pyqtSignal()
|
|
31
|
+
updatePlotsClicked = pyqtSignal()
|
|
32
|
+
parametersChanged = pyqtSignal() # Emitted when any parameter changes
|
|
33
|
+
|
|
34
|
+
def __init__(self, parent=None):
|
|
35
|
+
super().__init__(parent)
|
|
36
|
+
self.init_ui()
|
|
37
|
+
|
|
38
|
+
def init_ui(self):
|
|
39
|
+
"""Initialize the control panel UI."""
|
|
40
|
+
layout = QVBoxLayout(self)
|
|
41
|
+
|
|
42
|
+
# Configuration group
|
|
43
|
+
config_group = QGroupBox("Configuration")
|
|
44
|
+
# set the width of the group box to 300px
|
|
45
|
+
config_layout = QFormLayout(config_group)
|
|
46
|
+
|
|
47
|
+
# Energy input (in keV, converted to eV internally)
|
|
48
|
+
self.energy_input = EnergySpinBox()
|
|
49
|
+
config_layout.addRow("X-ray Energy:", self.energy_input)
|
|
50
|
+
|
|
51
|
+
# Compact U, V, Center in one row using text inputs like 110, 010, 000
|
|
52
|
+
uvc_row = QWidget()
|
|
53
|
+
uvc_layout = QHBoxLayout(uvc_row)
|
|
54
|
+
uvc_layout.setContentsMargins(0, 0, 0, 0)
|
|
55
|
+
|
|
56
|
+
self.u_line = QLineEdit()
|
|
57
|
+
self.u_line.setPlaceholderText("1,1,0")
|
|
58
|
+
self.u_line.setText("1,1,0")
|
|
59
|
+
|
|
60
|
+
self.u_line.setFixedWidth(80)
|
|
61
|
+
self.v_line = QLineEdit()
|
|
62
|
+
self.v_line.setPlaceholderText("0,0,1")
|
|
63
|
+
self.v_line.setText("0,0,1")
|
|
64
|
+
self.v_line.setFixedWidth(80)
|
|
65
|
+
|
|
66
|
+
self.c_line = QLineEdit()
|
|
67
|
+
self.c_line.setPlaceholderText("0,0,0")
|
|
68
|
+
self.c_line.setText("0,0,0")
|
|
69
|
+
self.c_line.setFixedWidth(80)
|
|
70
|
+
|
|
71
|
+
uvc_layout.addWidget(QLabel("U"))
|
|
72
|
+
uvc_layout.addWidget(self.u_line)
|
|
73
|
+
uvc_layout.addWidget(QLabel("V"))
|
|
74
|
+
uvc_layout.addWidget(self.v_line)
|
|
75
|
+
uvc_layout.addWidget(QLabel("Center"))
|
|
76
|
+
uvc_layout.addWidget(self.c_line)
|
|
77
|
+
config_layout.addRow("", uvc_row)
|
|
78
|
+
|
|
79
|
+
# u,v range controls on the same row
|
|
80
|
+
ranges_row = QWidget()
|
|
81
|
+
ranges_layout = QHBoxLayout(ranges_row)
|
|
82
|
+
ranges_layout.setContentsMargins(0, 0, 0, 0)
|
|
83
|
+
|
|
84
|
+
self.u_range_spin = QSpinBox()
|
|
85
|
+
self.u_range_spin.setRange(0, 35)
|
|
86
|
+
self.u_range_spin.setValue(3)
|
|
87
|
+
|
|
88
|
+
self.v_range_spin = QSpinBox()
|
|
89
|
+
self.v_range_spin.setRange(0, 35)
|
|
90
|
+
self.v_range_spin.setValue(3)
|
|
91
|
+
|
|
92
|
+
ranges_layout.addWidget(QLabel("U range"))
|
|
93
|
+
ranges_layout.addWidget(self.u_range_spin)
|
|
94
|
+
ranges_layout.addWidget(QLabel("V range"))
|
|
95
|
+
ranges_layout.addWidget(self.v_range_spin)
|
|
96
|
+
config_layout.addRow("", ranges_row)
|
|
97
|
+
|
|
98
|
+
# Initialize button and status
|
|
99
|
+
self.init_btn = QPushButton("Initialize Calculator")
|
|
100
|
+
self.init_btn.clicked.connect(self.initializeClicked.emit)
|
|
101
|
+
|
|
102
|
+
self.status_label = QLabel("Status: Provide CIF in initialization window, then initialize")
|
|
103
|
+
self.status_label.setStyleSheet("color: orange; font-weight: bold;")
|
|
104
|
+
config_layout.addRow("", self.status_label)
|
|
105
|
+
|
|
106
|
+
# Update button (kept for manual refresh if needed)
|
|
107
|
+
self.update_plane_btn = QPushButton("Update Plane & Plots")
|
|
108
|
+
self.update_plane_btn.clicked.connect(self.updatePlotsClicked.emit)
|
|
109
|
+
|
|
110
|
+
# Put init and update button on the same row
|
|
111
|
+
buttons_row = QWidget()
|
|
112
|
+
buttons_layout = QHBoxLayout(buttons_row)
|
|
113
|
+
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
|
114
|
+
buttons_layout.addWidget(self.init_btn)
|
|
115
|
+
buttons_layout.addWidget(self.update_plane_btn)
|
|
116
|
+
config_layout.addRow("", buttons_row)
|
|
117
|
+
|
|
118
|
+
layout.addWidget(config_group)
|
|
119
|
+
|
|
120
|
+
# Connect parameter change signals
|
|
121
|
+
self._connect_signals()
|
|
122
|
+
|
|
123
|
+
def _connect_signals(self):
|
|
124
|
+
"""Connect signals for automatic updates."""
|
|
125
|
+
self.u_range_spin.valueChanged.connect(self.parametersChanged.emit)
|
|
126
|
+
self.v_range_spin.valueChanged.connect(self.parametersChanged.emit)
|
|
127
|
+
self.u_line.textChanged.connect(self.parametersChanged.emit)
|
|
128
|
+
self.v_line.textChanged.connect(self.parametersChanged.emit)
|
|
129
|
+
self.c_line.textChanged.connect(self.parametersChanged.emit)
|
|
130
|
+
|
|
131
|
+
def set_status(self, message: str, color: str = "orange"):
|
|
132
|
+
"""Update status label."""
|
|
133
|
+
self.status_label.setText(f"Status: {message}")
|
|
134
|
+
self.status_label.setStyleSheet(f"color: {color}; font-weight: bold;")
|
|
135
|
+
|
|
136
|
+
def get_energy_ev(self):
|
|
137
|
+
"""Get current energy in eV."""
|
|
138
|
+
return self.energy_input.energy_ev
|
|
139
|
+
|
|
140
|
+
def get_custom_vectors(self):
|
|
141
|
+
"""Return U, V, and Center vectors parsed from text inputs.
|
|
142
|
+
|
|
143
|
+
Expected format: comma-separated values like '1,3,11' for h=1, k=3, l=11.
|
|
144
|
+
Negative values are supported, e.g. '-1,2,-3'.
|
|
145
|
+
"""
|
|
146
|
+
def parse_hkl(text: str, default: tuple) -> tuple:
|
|
147
|
+
try:
|
|
148
|
+
# Remove all spaces and split by comma
|
|
149
|
+
parts = text.strip().replace(" ", "").split(",")
|
|
150
|
+
|
|
151
|
+
# Must have exactly 3 values
|
|
152
|
+
if len(parts) != 3:
|
|
153
|
+
return default
|
|
154
|
+
|
|
155
|
+
# Parse each part as an integer (supports negative values)
|
|
156
|
+
vals = []
|
|
157
|
+
for part in parts:
|
|
158
|
+
if not part: # Empty string after split
|
|
159
|
+
return default
|
|
160
|
+
vals.append(int(part))
|
|
161
|
+
|
|
162
|
+
return tuple(vals)
|
|
163
|
+
except (ValueError, AttributeError):
|
|
164
|
+
return default
|
|
165
|
+
|
|
166
|
+
U = parse_hkl(self.u_line.text(), (1, 1, 0))
|
|
167
|
+
V = parse_hkl(self.v_line.text(), (0, 0, 1))
|
|
168
|
+
C = parse_hkl(self.c_line.text(), (0, 0, 0))
|
|
169
|
+
return U, V, C
|
|
170
|
+
|
|
171
|
+
def get_ranges(self):
|
|
172
|
+
"""Get u and v ranges."""
|
|
173
|
+
return self.u_range_spin.value(), self.v_range_spin.value()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CustomizedPlane3DWidget(QWidget):
|
|
177
|
+
"""3D visualization widget for customized plane with overlay."""
|
|
178
|
+
|
|
179
|
+
def __init__(self, parent=None):
|
|
180
|
+
super().__init__(parent)
|
|
181
|
+
self.init_ui()
|
|
182
|
+
|
|
183
|
+
def init_ui(self):
|
|
184
|
+
"""Initialize the 3D widget."""
|
|
185
|
+
layout = QVBoxLayout(self)
|
|
186
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
187
|
+
|
|
188
|
+
# Create group box
|
|
189
|
+
self.visualizer3d = StructureFactorVisualizer3D()
|
|
190
|
+
|
|
191
|
+
layout.addWidget(self.visualizer3d)
|
|
192
|
+
|
|
193
|
+
def visualize_structure_factors(self, hkl_list, sf_values):
|
|
194
|
+
"""Visualize structure factors in 3D."""
|
|
195
|
+
self.visualizer3d.visualize_structure_factors(hkl_list, np.abs(sf_values))
|
|
196
|
+
|
|
197
|
+
def set_custom_plane(self, U, V, u_min, u_max, v_min, v_max, steps=2, center=(0, 0, 0)):
|
|
198
|
+
"""Set custom plane overlay."""
|
|
199
|
+
try:
|
|
200
|
+
self.visualizer3d.set_custom_plane(
|
|
201
|
+
U, V, u_min, u_max, v_min, v_max, steps, center
|
|
202
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"Error setting custom plane: {e}")
|
|
205
|
+
|
|
206
|
+
def clear_plot(self):
|
|
207
|
+
"""Clear the 3D plot."""
|
|
208
|
+
self.visualizer3d.clear_plot()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class CustomizedPlane2DWidget(QWidget):
|
|
212
|
+
"""2D visualization widget for customized UV plane."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, parent=None):
|
|
215
|
+
super().__init__(parent)
|
|
216
|
+
self.init_ui()
|
|
217
|
+
|
|
218
|
+
def init_ui(self):
|
|
219
|
+
"""Initialize the 2D widget."""
|
|
220
|
+
layout = QVBoxLayout(self)
|
|
221
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
222
|
+
|
|
223
|
+
self.visualizer2d = StructureFactorVisualizer2D()
|
|
224
|
+
layout.addWidget(self.visualizer2d)
|
|
225
|
+
|
|
226
|
+
def visualize_uv_plane_points(self, uv_points, sf_values, u_label, v_label,
|
|
227
|
+
vector_center=(0, 0, 0), value_max=None):
|
|
228
|
+
"""Visualize UV plane points."""
|
|
229
|
+
self.visualizer2d.visualize_uv_plane_points(
|
|
230
|
+
uv_points, sf_values, u_label, v_label, vector_center, value_max
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def clear_plot(self):
|
|
234
|
+
"""Clear the 2D plot."""
|
|
235
|
+
self.visualizer2d.clear_plot()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class CustomizedPlaneWidget(QWidget):
|
|
239
|
+
"""Complete customized plane widget combining controls and visualizations."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, parent=None):
|
|
242
|
+
super().__init__(parent)
|
|
243
|
+
self.calculator = None # Will be set by parent
|
|
244
|
+
self.init_ui()
|
|
245
|
+
|
|
246
|
+
def init_ui(self):
|
|
247
|
+
"""Initialize the complete widget."""
|
|
248
|
+
main_layout = QGridLayout(self)
|
|
249
|
+
|
|
250
|
+
# Left panel: configuration
|
|
251
|
+
self.controls = CustomizedPlaneControls()
|
|
252
|
+
main_layout.addWidget(self.controls, 1, 0)
|
|
253
|
+
|
|
254
|
+
# 2D plane visualizer below configuration for more space
|
|
255
|
+
self.plane_2d = CustomizedPlane2DWidget()
|
|
256
|
+
main_layout.addWidget(self.plane_2d, 0, 1, 1, 1)
|
|
257
|
+
|
|
258
|
+
# Right panel: 3D spanning both rows
|
|
259
|
+
self.plane_3d = CustomizedPlane3DWidget()
|
|
260
|
+
main_layout.addWidget(self.plane_3d, 0, 0)
|
|
261
|
+
|
|
262
|
+
# Set layout proportions
|
|
263
|
+
main_layout.setColumnStretch(0, 1) # Left column
|
|
264
|
+
main_layout.setColumnStretch(1, 1) # Right column
|
|
265
|
+
main_layout.setRowStretch(0, 3) # More space for visualizers
|
|
266
|
+
main_layout.setRowStretch(1, 1) # Less space for controls
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# Connect signals
|
|
270
|
+
self.controls.parametersChanged.connect(self.update_plots)
|
|
271
|
+
self.controls.updatePlotsClicked.connect(self.update_plots)
|
|
272
|
+
|
|
273
|
+
def set_calculator(self, calculator):
|
|
274
|
+
"""Set the calculator instance."""
|
|
275
|
+
self.calculator = calculator
|
|
276
|
+
|
|
277
|
+
def get_controls(self):
|
|
278
|
+
"""Get the controls widget."""
|
|
279
|
+
return self.controls
|
|
280
|
+
|
|
281
|
+
def _generate_hkl_cube(self, max_index: int = 5):
|
|
282
|
+
"""Generate a full integer HKL grid from 0..max_index for 3D visualization."""
|
|
283
|
+
cube = []
|
|
284
|
+
for h in range(0, max_index + 1):
|
|
285
|
+
for k in range(0, max_index + 1):
|
|
286
|
+
for l in range(0, max_index + 1):
|
|
287
|
+
cube.append([h, k, l])
|
|
288
|
+
return cube
|
|
289
|
+
|
|
290
|
+
@pyqtSlot()
|
|
291
|
+
def update_plots(self):
|
|
292
|
+
"""Update 3D scatter (all HKL) with a custom plane overlay and 2D uv plot."""
|
|
293
|
+
try:
|
|
294
|
+
# Always update the plane overlay for immediate feedback
|
|
295
|
+
U, V, C = self.controls.get_custom_vectors()
|
|
296
|
+
u_max, v_max = self.controls.get_ranges()
|
|
297
|
+
|
|
298
|
+
# Symmetric parameter ranges around 0; apply center offset in HKL
|
|
299
|
+
u_min_param = -(u_max // 2)
|
|
300
|
+
u_max_param = u_max - (u_max // 2)
|
|
301
|
+
v_min_param = -(v_max // 2)
|
|
302
|
+
v_max_param = v_max - (v_max // 2)
|
|
303
|
+
|
|
304
|
+
# Update plane overlay
|
|
305
|
+
self.plane_3d.set_custom_plane(
|
|
306
|
+
U, V, u_min_param, u_max_param, v_min_param, v_max_param, steps=2, center=C
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if not self.calculator or not self.calculator.is_initialized:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# 3D: plot all HKL points 0..5
|
|
313
|
+
hkl_list = self._generate_hkl_cube(5)
|
|
314
|
+
sf_values = self.calculator.calculate_structure_factors(hkl_list)
|
|
315
|
+
self.plane_3d.visualize_structure_factors(hkl_list, sf_values)
|
|
316
|
+
|
|
317
|
+
# Re-apply plane overlay after replot
|
|
318
|
+
self.plane_3d.set_custom_plane(
|
|
319
|
+
U, V, u_min_param, u_max_param, v_min_param, v_max_param, steps=2, center=C
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# 2D: points on the plane using integer combinations of U and V in ranges
|
|
323
|
+
uv_points = []
|
|
324
|
+
hkl_points = []
|
|
325
|
+
# symmetric parameter ranges around 0 with given max steps, shifted by center
|
|
326
|
+
for u in range(u_min_param, u_max_param + 1):
|
|
327
|
+
for v in range(v_min_param, v_max_param + 1):
|
|
328
|
+
H = C[0] + u * U[0] + v * V[0]
|
|
329
|
+
K = C[1] + u * U[1] + v * V[1]
|
|
330
|
+
L = C[2] + u * U[2] + v * V[2]
|
|
331
|
+
uv_points.append({"u": u, "v": v, "H": H, "K": K, "L": L})
|
|
332
|
+
hkl_points.append([H, K, L])
|
|
333
|
+
|
|
334
|
+
if len(hkl_points) > 0:
|
|
335
|
+
sf_plane = self.calculator.calculate_structure_factors(hkl_points)
|
|
336
|
+
# Reference value for sizing: use |F(0,0,0)| for consistency
|
|
337
|
+
ref = self.calculator.calculate_structure_factors([[0, 0, 0]])
|
|
338
|
+
value_max = (
|
|
339
|
+
float(np.abs(ref[0]))
|
|
340
|
+
if len(ref) > 0
|
|
341
|
+
else (
|
|
342
|
+
float(np.max(np.abs(sf_plane))) if len(sf_plane) > 0 else None
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
u_label = f"[{U[0]} {U[1]} {U[2]}]"
|
|
346
|
+
v_label = f"[{V[0]} {V[1]} {V[2]}]"
|
|
347
|
+
self.plane_2d.visualize_uv_plane_points(
|
|
348
|
+
uv_points, np.abs(sf_plane), u_label, v_label, vector_center=C, value_max=value_max
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
# Keep UI responsive
|
|
353
|
+
print(f"Error updating customized plots: {e}")
|
|
354
|
+
|
|
355
|
+
def clear_plots(self):
|
|
356
|
+
"""Clear all plots."""
|
|
357
|
+
self.plane_2d.clear_plot()
|
|
358
|
+
self.plane_3d.clear_plot()
|