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,391 @@
|
|
|
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
|
+
QSlider,
|
|
15
|
+
QStackedLayout,
|
|
16
|
+
)
|
|
17
|
+
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
|
18
|
+
import numpy as np
|
|
19
|
+
import sys
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
# Add parent directory to path for imports
|
|
23
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
|
|
24
|
+
from advisor.ui.visualizers import StructureFactorVisualizer3D, StructureFactorVisualizer2D
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EnergySpinBox(QWidget):
|
|
28
|
+
"""Custom spinbox that displays keV but stores/returns eV internally."""
|
|
29
|
+
|
|
30
|
+
valueChanged = pyqtSignal(float) # Emits energy in eV
|
|
31
|
+
|
|
32
|
+
def __init__(self, parent=None):
|
|
33
|
+
super().__init__(parent)
|
|
34
|
+
from PyQt5.QtWidgets import QDoubleSpinBox
|
|
35
|
+
|
|
36
|
+
self.spinbox = QDoubleSpinBox()
|
|
37
|
+
self.spinbox.setRange(0.001, 1000.0) # keV range: 1 eV to 100 keV
|
|
38
|
+
self.spinbox.setDecimals(3) # More precision for keV
|
|
39
|
+
self.spinbox.setSuffix(" keV")
|
|
40
|
+
self.spinbox.setValue(100.0) # 10 keV default
|
|
41
|
+
|
|
42
|
+
layout = QHBoxLayout(self)
|
|
43
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
44
|
+
layout.addWidget(self.spinbox)
|
|
45
|
+
|
|
46
|
+
# Connect signal
|
|
47
|
+
self.spinbox.valueChanged.connect(lambda v: self.valueChanged.emit(v * 1000.0))
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def energy_ev(self):
|
|
51
|
+
"""Get energy value in eV (internal storage unit)."""
|
|
52
|
+
return self.spinbox.value() * 1000.0
|
|
53
|
+
|
|
54
|
+
@energy_ev.setter
|
|
55
|
+
def energy_ev(self, value_ev):
|
|
56
|
+
"""Set energy value from eV (converts to keV for display)."""
|
|
57
|
+
self.spinbox.setValue(value_ev / 1000.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HKLPlaneControls(QWidget):
|
|
61
|
+
"""Control panel for HKL plane visualization with energy input and plane toggles."""
|
|
62
|
+
|
|
63
|
+
initializeClicked = pyqtSignal()
|
|
64
|
+
planeChanged = pyqtSignal(str) # Emits "HK", "HL", or "KL"
|
|
65
|
+
energyChanged = pyqtSignal(float) # Emits energy in eV
|
|
66
|
+
|
|
67
|
+
def __init__(self, parent=None):
|
|
68
|
+
super().__init__(parent)
|
|
69
|
+
self.init_ui()
|
|
70
|
+
|
|
71
|
+
def init_ui(self):
|
|
72
|
+
"""Initialize the control panel UI."""
|
|
73
|
+
layout = QVBoxLayout(self)
|
|
74
|
+
|
|
75
|
+
# Configuration group
|
|
76
|
+
config_group = QGroupBox("Configuration")
|
|
77
|
+
config_layout = QFormLayout(config_group)
|
|
78
|
+
|
|
79
|
+
# Energy input
|
|
80
|
+
self.energy_input = EnergySpinBox()
|
|
81
|
+
self.energy_input.valueChanged.connect(self.energyChanged.emit)
|
|
82
|
+
config_layout.addRow("X-ray Energy:", self.energy_input)
|
|
83
|
+
|
|
84
|
+
# Plane toggle buttons
|
|
85
|
+
self.hk_toggle_btn = QPushButton("HK plane")
|
|
86
|
+
self.hl_toggle_btn = QPushButton("HL plane")
|
|
87
|
+
self.kl_toggle_btn = QPushButton("KL plane")
|
|
88
|
+
|
|
89
|
+
for btn in (self.hk_toggle_btn, self.hl_toggle_btn, self.kl_toggle_btn):
|
|
90
|
+
btn.setCheckable(True)
|
|
91
|
+
|
|
92
|
+
self.hk_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("HK"))
|
|
93
|
+
self.hl_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("HL"))
|
|
94
|
+
self.kl_toggle_btn.clicked.connect(lambda: self._on_plane_clicked("KL"))
|
|
95
|
+
|
|
96
|
+
# Add plane toggle row
|
|
97
|
+
toggle_row = QWidget()
|
|
98
|
+
toggle_layout = QHBoxLayout(toggle_row)
|
|
99
|
+
toggle_layout.setContentsMargins(0, 0, 0, 0)
|
|
100
|
+
toggle_layout.addWidget(self.hk_toggle_btn)
|
|
101
|
+
toggle_layout.addWidget(self.hl_toggle_btn)
|
|
102
|
+
toggle_layout.addWidget(self.kl_toggle_btn)
|
|
103
|
+
config_layout.addRow("Plane:", toggle_row)
|
|
104
|
+
|
|
105
|
+
# Initialize button
|
|
106
|
+
self.init_btn = QPushButton("Initialize Calculator")
|
|
107
|
+
self.init_btn.clicked.connect(self.initializeClicked.emit)
|
|
108
|
+
config_layout.addRow("", self.init_btn)
|
|
109
|
+
|
|
110
|
+
# Status label
|
|
111
|
+
self.status_label = QLabel("Status: Provide CIF in initialization window, then initialize")
|
|
112
|
+
self.status_label.setStyleSheet("color: orange; font-weight: bold;")
|
|
113
|
+
config_layout.addRow("", self.status_label)
|
|
114
|
+
|
|
115
|
+
layout.addWidget(config_group)
|
|
116
|
+
|
|
117
|
+
# Default to HK plane
|
|
118
|
+
self._on_plane_clicked("HK")
|
|
119
|
+
|
|
120
|
+
def _on_plane_clicked(self, plane: str):
|
|
121
|
+
"""Handle plane toggle button clicks."""
|
|
122
|
+
self._update_toggle_styles(plane)
|
|
123
|
+
self.planeChanged.emit(plane)
|
|
124
|
+
|
|
125
|
+
def _update_toggle_styles(self, active: str):
|
|
126
|
+
"""Update toggle button colors based on active plane."""
|
|
127
|
+
active_css = "background-color: #2ecc71; color: white; font-weight: bold;"
|
|
128
|
+
inactive_css = "background-color: #bdc3c7; color: #333333;"
|
|
129
|
+
mapping = {
|
|
130
|
+
"HK": self.hk_toggle_btn,
|
|
131
|
+
"HL": self.hl_toggle_btn,
|
|
132
|
+
"KL": self.kl_toggle_btn,
|
|
133
|
+
}
|
|
134
|
+
for name, btn in mapping.items():
|
|
135
|
+
if name == active:
|
|
136
|
+
btn.setChecked(True)
|
|
137
|
+
btn.setStyleSheet(active_css)
|
|
138
|
+
else:
|
|
139
|
+
btn.setChecked(False)
|
|
140
|
+
btn.setStyleSheet(inactive_css)
|
|
141
|
+
|
|
142
|
+
def set_status(self, message: str, color: str = "orange"):
|
|
143
|
+
"""Update status label."""
|
|
144
|
+
self.status_label.setText(f"Status: {message}")
|
|
145
|
+
self.status_label.setStyleSheet(f"color: {color}; font-weight: bold;")
|
|
146
|
+
|
|
147
|
+
def get_energy_ev(self):
|
|
148
|
+
"""Get current energy in eV."""
|
|
149
|
+
return self.energy_input.energy_ev
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class HKLPlane3DWidget(QWidget):
|
|
153
|
+
"""3D visualization widget for HKL structure factors with plane overlays."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, parent=None):
|
|
156
|
+
super().__init__(parent)
|
|
157
|
+
self.init_ui()
|
|
158
|
+
|
|
159
|
+
def init_ui(self):
|
|
160
|
+
"""Initialize the 3D widget."""
|
|
161
|
+
layout = QVBoxLayout(self)
|
|
162
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
163
|
+
|
|
164
|
+
self.visualizer3d = StructureFactorVisualizer3D()
|
|
165
|
+
layout.addWidget(self.visualizer3d)
|
|
166
|
+
|
|
167
|
+
def initialize(self, params):
|
|
168
|
+
"""Initialize the 3D visualizer."""
|
|
169
|
+
self.visualizer3d.initialize(params)
|
|
170
|
+
|
|
171
|
+
def visualize_structure_factors(self, hkl_list, sf_values):
|
|
172
|
+
"""Visualize structure factors in 3D."""
|
|
173
|
+
self.visualizer3d.visualize_structure_factors(hkl_list, np.abs(sf_values))
|
|
174
|
+
|
|
175
|
+
def set_plane_values(self, **kwargs):
|
|
176
|
+
"""Set plane overlay values."""
|
|
177
|
+
self.visualizer3d.set_plane_values(**kwargs)
|
|
178
|
+
|
|
179
|
+
def set_active_plane(self, plane_axis: str):
|
|
180
|
+
"""Set which plane is highlighted."""
|
|
181
|
+
self.visualizer3d.set_active_plane(plane_axis)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class FixedIndexControls(QWidget):
|
|
185
|
+
"""Controls for a single fixed index (spin box + slider)."""
|
|
186
|
+
|
|
187
|
+
valueChanged = pyqtSignal(int)
|
|
188
|
+
|
|
189
|
+
def __init__(self, label_prefix: str, fixed_name: str, default_value: int = 0, parent=None):
|
|
190
|
+
super().__init__(parent)
|
|
191
|
+
self.fixed_name = fixed_name
|
|
192
|
+
self.init_ui(label_prefix, fixed_name, default_value)
|
|
193
|
+
|
|
194
|
+
def init_ui(self, label_prefix: str, fixed_name: str, default_value: int):
|
|
195
|
+
"""Initialize the controls."""
|
|
196
|
+
layout = QVBoxLayout(self)
|
|
197
|
+
|
|
198
|
+
# Group box
|
|
199
|
+
self.group = QGroupBox(f"{label_prefix}")
|
|
200
|
+
group_layout = QFormLayout(self.group)
|
|
201
|
+
|
|
202
|
+
# Controls row
|
|
203
|
+
row = QWidget()
|
|
204
|
+
row_layout = QHBoxLayout(row)
|
|
205
|
+
row_layout.setContentsMargins(0, 0, 0, 0)
|
|
206
|
+
|
|
207
|
+
# Spin box
|
|
208
|
+
self.spin = QSpinBox()
|
|
209
|
+
self.spin.setRange(0, 35)
|
|
210
|
+
self.spin.setValue(default_value)
|
|
211
|
+
|
|
212
|
+
# Slider
|
|
213
|
+
self.slider = QSlider()
|
|
214
|
+
self.slider.setOrientation(Qt.Horizontal)
|
|
215
|
+
self.slider.setRange(0, 35)
|
|
216
|
+
self.slider.setValue(default_value)
|
|
217
|
+
|
|
218
|
+
row_layout.addWidget(self.spin)
|
|
219
|
+
row_layout.addWidget(self.slider)
|
|
220
|
+
group_layout.addRow(f"{fixed_name}:", row)
|
|
221
|
+
|
|
222
|
+
layout.addWidget(self.group)
|
|
223
|
+
|
|
224
|
+
# Connect signals
|
|
225
|
+
self.spin.valueChanged.connect(self._on_spin_changed)
|
|
226
|
+
self.slider.valueChanged.connect(self._on_slider_changed)
|
|
227
|
+
|
|
228
|
+
def _on_spin_changed(self, value):
|
|
229
|
+
"""Handle spin box value change."""
|
|
230
|
+
self.slider.blockSignals(True)
|
|
231
|
+
self.slider.setValue(value)
|
|
232
|
+
self.slider.blockSignals(False)
|
|
233
|
+
self.valueChanged.emit(value)
|
|
234
|
+
|
|
235
|
+
def _on_slider_changed(self, value):
|
|
236
|
+
"""Handle slider value change."""
|
|
237
|
+
self.spin.blockSignals(True)
|
|
238
|
+
self.spin.setValue(value)
|
|
239
|
+
self.spin.blockSignals(False)
|
|
240
|
+
self.valueChanged.emit(value)
|
|
241
|
+
|
|
242
|
+
def get_value(self):
|
|
243
|
+
"""Get current value."""
|
|
244
|
+
return self.spin.value()
|
|
245
|
+
|
|
246
|
+
def set_value(self, value):
|
|
247
|
+
"""Set value programmatically."""
|
|
248
|
+
self.spin.setValue(value)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class HKLPlane2DWidget(QWidget):
|
|
252
|
+
"""Widget containing stacked 2D plane visualizers with individual controls."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, parent=None):
|
|
255
|
+
super().__init__(parent)
|
|
256
|
+
self.grid_max = 5 # inclusive max for varying integer indices
|
|
257
|
+
self.init_ui()
|
|
258
|
+
|
|
259
|
+
def init_ui(self):
|
|
260
|
+
"""Initialize the 2D widget with stacked layout."""
|
|
261
|
+
main_layout = QGridLayout(self)
|
|
262
|
+
|
|
263
|
+
# Create 2D visualizers
|
|
264
|
+
self.hk_visualizer = StructureFactorVisualizer2D()
|
|
265
|
+
self.hl_visualizer = StructureFactorVisualizer2D()
|
|
266
|
+
self.kl_visualizer = StructureFactorVisualizer2D()
|
|
267
|
+
|
|
268
|
+
# Create control widgets
|
|
269
|
+
self.hk_controls = FixedIndexControls("HK plane", "L", 0)
|
|
270
|
+
self.hl_controls = FixedIndexControls("HL plane", "K", 0)
|
|
271
|
+
self.kl_controls = FixedIndexControls("KL plane", "H", 0)
|
|
272
|
+
|
|
273
|
+
# Plane stack (top)
|
|
274
|
+
self.plane_stack_container = QWidget()
|
|
275
|
+
self.plane_stack = QStackedLayout(self.plane_stack_container)
|
|
276
|
+
self.plane_stack.addWidget(self.hk_visualizer)
|
|
277
|
+
self.plane_stack.addWidget(self.hl_visualizer)
|
|
278
|
+
self.plane_stack.addWidget(self.kl_visualizer)
|
|
279
|
+
main_layout.addWidget(self.plane_stack_container, 0, 0)
|
|
280
|
+
|
|
281
|
+
# Control stack (bottom)
|
|
282
|
+
self.ctrl_stack_container = QWidget()
|
|
283
|
+
self.ctrl_stack = QStackedLayout(self.ctrl_stack_container)
|
|
284
|
+
self.ctrl_stack.addWidget(self.hk_controls)
|
|
285
|
+
self.ctrl_stack.addWidget(self.hl_controls)
|
|
286
|
+
self.ctrl_stack.addWidget(self.kl_controls)
|
|
287
|
+
main_layout.addWidget(self.ctrl_stack_container, 1, 0)
|
|
288
|
+
|
|
289
|
+
# Set layout proportions
|
|
290
|
+
main_layout.setRowStretch(0, 3) # More space for visualizer
|
|
291
|
+
main_layout.setRowStretch(1, 1) # Less space for controls
|
|
292
|
+
|
|
293
|
+
# Default to HK plane
|
|
294
|
+
self.set_active_plane("HK")
|
|
295
|
+
|
|
296
|
+
def set_active_plane(self, plane: str):
|
|
297
|
+
"""Set which plane is visible."""
|
|
298
|
+
plane = plane.upper()
|
|
299
|
+
index_map = {"HK": 0, "HL": 1, "KL": 2}
|
|
300
|
+
idx = index_map.get(plane, 0)
|
|
301
|
+
self.plane_stack.setCurrentIndex(idx)
|
|
302
|
+
self.ctrl_stack.setCurrentIndex(idx)
|
|
303
|
+
|
|
304
|
+
def connect_value_changed_signals(self, hk_callback, hl_callback, kl_callback):
|
|
305
|
+
"""Connect value changed signals to callbacks."""
|
|
306
|
+
self.hk_controls.valueChanged.connect(hk_callback)
|
|
307
|
+
self.hl_controls.valueChanged.connect(hl_callback)
|
|
308
|
+
self.kl_controls.valueChanged.connect(kl_callback)
|
|
309
|
+
|
|
310
|
+
def connect_3d_plane_signals(self, plane_3d_widget):
|
|
311
|
+
"""Connect control changes to 3D plane updates."""
|
|
312
|
+
self.hk_controls.valueChanged.connect(
|
|
313
|
+
lambda v: plane_3d_widget.set_plane_values(L=int(v))
|
|
314
|
+
)
|
|
315
|
+
self.hl_controls.valueChanged.connect(
|
|
316
|
+
lambda v: plane_3d_widget.set_plane_values(K=int(v))
|
|
317
|
+
)
|
|
318
|
+
self.kl_controls.valueChanged.connect(
|
|
319
|
+
lambda v: plane_3d_widget.set_plane_values(H=int(v))
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def get_plane_values(self):
|
|
323
|
+
"""Get current plane control values."""
|
|
324
|
+
return {
|
|
325
|
+
"L": self.hk_controls.get_value(),
|
|
326
|
+
"K": self.hl_controls.get_value(),
|
|
327
|
+
"H": self.kl_controls.get_value(),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
def _generate_plane_points(self, varying_a: str, varying_b: str, fixed_name: str, fixed_value: int):
|
|
331
|
+
"""Create integer HKL points for a plane with two varying indices in [0, grid_max]."""
|
|
332
|
+
points = []
|
|
333
|
+
for a in range(0, self.grid_max + 1):
|
|
334
|
+
for b in range(0, self.grid_max + 1):
|
|
335
|
+
values = {"H": 0, "K": 0, "L": 0}
|
|
336
|
+
values[varying_a] = a
|
|
337
|
+
values[varying_b] = b
|
|
338
|
+
values[fixed_name] = fixed_value
|
|
339
|
+
points.append([values["H"], values["K"], values["L"]])
|
|
340
|
+
return points
|
|
341
|
+
|
|
342
|
+
def update_hk_plane(self, calculator):
|
|
343
|
+
"""Update HK plane visualization."""
|
|
344
|
+
if not calculator.is_initialized:
|
|
345
|
+
return
|
|
346
|
+
L_val = self.hk_controls.get_value()
|
|
347
|
+
hkl_list = self._generate_plane_points("H", "K", "L", L_val)
|
|
348
|
+
results = calculator.calculate_structure_factors(hkl_list)
|
|
349
|
+
# Reference value for color scale
|
|
350
|
+
ref = calculator.calculate_structure_factors([[0, 0, 0]])
|
|
351
|
+
value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
|
|
352
|
+
arr = np.array(hkl_list)
|
|
353
|
+
self.hk_visualizer.visualize_plane(
|
|
354
|
+
arr[:, 0], arr[:, 1], np.abs(results), "H", "K", "L", L_val, value_max
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def update_hl_plane(self, calculator):
|
|
358
|
+
"""Update HL plane visualization."""
|
|
359
|
+
if not calculator.is_initialized:
|
|
360
|
+
return
|
|
361
|
+
K_val = self.hl_controls.get_value()
|
|
362
|
+
hkl_list = self._generate_plane_points("H", "L", "K", K_val)
|
|
363
|
+
results = calculator.calculate_structure_factors(hkl_list)
|
|
364
|
+
# Reference value for color scale
|
|
365
|
+
ref = calculator.calculate_structure_factors([[0, 0, 0]])
|
|
366
|
+
value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
|
|
367
|
+
arr = np.array(hkl_list)
|
|
368
|
+
self.hl_visualizer.visualize_plane(
|
|
369
|
+
arr[:, 0], arr[:, 2], np.abs(results), "H", "L", "K", K_val, value_max
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def update_kl_plane(self, calculator):
|
|
373
|
+
"""Update KL plane visualization."""
|
|
374
|
+
if not calculator.is_initialized:
|
|
375
|
+
return
|
|
376
|
+
H_val = self.kl_controls.get_value()
|
|
377
|
+
hkl_list = self._generate_plane_points("K", "L", "H", H_val)
|
|
378
|
+
results = calculator.calculate_structure_factors(hkl_list)
|
|
379
|
+
# Reference value for color scale
|
|
380
|
+
ref = calculator.calculate_structure_factors([[0, 0, 0]])
|
|
381
|
+
value_max = float(np.abs(ref[0])) if len(ref) > 0 else None
|
|
382
|
+
arr = np.array(hkl_list)
|
|
383
|
+
self.kl_visualizer.visualize_plane(
|
|
384
|
+
arr[:, 1], arr[:, 2], np.abs(results), "K", "L", "H", H_val, value_max
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def clear_plots(self):
|
|
388
|
+
"""Clear all plane visualizations."""
|
|
389
|
+
self.hk_visualizer.clear_plot()
|
|
390
|
+
self.hl_visualizer.clear_plot()
|
|
391
|
+
self.kl_visualizer.clear_plot()
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# pylint: disable=no-name-in-module, import-error
|
|
4
|
+
import os
|
|
5
|
+
import numpy as np
|
|
6
|
+
from PyQt5.QtWidgets import (
|
|
7
|
+
QGridLayout,
|
|
8
|
+
QTabWidget,
|
|
9
|
+
QWidget,
|
|
10
|
+
QMessageBox,
|
|
11
|
+
)
|
|
12
|
+
from PyQt5.QtCore import pyqtSlot
|
|
13
|
+
|
|
14
|
+
from advisor.features.structure_factor.domain import StructureFactorCalculator
|
|
15
|
+
from advisor.ui.tab_interface import TabInterface
|
|
16
|
+
from advisor.ui.tips import Tips, set_tip
|
|
17
|
+
from .components import (
|
|
18
|
+
HKLPlaneControls,
|
|
19
|
+
HKLPlane3DWidget,
|
|
20
|
+
HKLPlane2DWidget,
|
|
21
|
+
CustomizedPlaneWidget,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StructureFactorTab(TabInterface):
|
|
26
|
+
"""Tab for calculating structure factors using X-ray scattering."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, controller=None, calculator=None):
|
|
29
|
+
self.controller = controller
|
|
30
|
+
self.calculator = calculator or StructureFactorCalculator()
|
|
31
|
+
self.tips = Tips()
|
|
32
|
+
|
|
33
|
+
# Initialize UI first
|
|
34
|
+
main_window = controller.app_controller.main_window if controller else None
|
|
35
|
+
super().__init__(controller=controller, main_window=main_window)
|
|
36
|
+
self.setWindowTitle("Structure Factor Calculator")
|
|
37
|
+
|
|
38
|
+
def init_ui(self):
|
|
39
|
+
"""Initialize UI components with subtabs."""
|
|
40
|
+
self.tab_widget = QTabWidget()
|
|
41
|
+
self.layout.addWidget(self.tab_widget, 0, 0)
|
|
42
|
+
|
|
43
|
+
self._create_hkl_plane_tab()
|
|
44
|
+
self._create_customized_tab()
|
|
45
|
+
|
|
46
|
+
def set_parameters(self, params: dict):
|
|
47
|
+
"""Set parameters from global lattice configuration.
|
|
48
|
+
|
|
49
|
+
Note: Structure factor calculator requires CIF file and energy,
|
|
50
|
+
which are not part of the global lattice parameters.
|
|
51
|
+
"""
|
|
52
|
+
if not params:
|
|
53
|
+
return
|
|
54
|
+
# Initialize visualizers through components
|
|
55
|
+
if hasattr(self, 'hkl_plane_3d'):
|
|
56
|
+
self.hkl_plane_3d.initialize(params)
|
|
57
|
+
|
|
58
|
+
# Clear all plots
|
|
59
|
+
if hasattr(self, 'hkl_plane_2d'):
|
|
60
|
+
self.hkl_plane_2d.clear_plots()
|
|
61
|
+
if hasattr(self, 'customized_plane_widget'):
|
|
62
|
+
self.customized_plane_widget.clear_plots()
|
|
63
|
+
|
|
64
|
+
def _set_tip(self, widget, name):
|
|
65
|
+
"""Set the tooltip and status tip for a widget by the name"""
|
|
66
|
+
set_tip(widget, self.tips.tip(name))
|
|
67
|
+
|
|
68
|
+
def _create_hkl_plane_tab(self):
|
|
69
|
+
"""Create the HKL plane subtab using components."""
|
|
70
|
+
hkl_tab = QWidget()
|
|
71
|
+
main_layout = QGridLayout(hkl_tab)
|
|
72
|
+
|
|
73
|
+
# Create components
|
|
74
|
+
self.hkl_controls = HKLPlaneControls()
|
|
75
|
+
self.hkl_plane_3d = HKLPlane3DWidget()
|
|
76
|
+
self.hkl_plane_2d = HKLPlane2DWidget()
|
|
77
|
+
|
|
78
|
+
# Layout components
|
|
79
|
+
main_layout.addWidget(self.hkl_plane_3d, 0, 0) # 3D top-left
|
|
80
|
+
main_layout.addWidget(self.hkl_controls, 1, 0) # Controls bottom-left
|
|
81
|
+
main_layout.addWidget(self.hkl_plane_2d, 0, 1, 2, 1) # 2D right side spanning both rows
|
|
82
|
+
|
|
83
|
+
# Set layout proportions
|
|
84
|
+
main_layout.setColumnStretch(0, 1) # Left column
|
|
85
|
+
main_layout.setColumnStretch(1, 1) # Right column
|
|
86
|
+
main_layout.setRowStretch(0, 3) # More space for visualizers
|
|
87
|
+
main_layout.setRowStretch(1, 1) # Less space for controls
|
|
88
|
+
|
|
89
|
+
# Connect signals
|
|
90
|
+
self._connect_hkl_signals()
|
|
91
|
+
|
|
92
|
+
# Add to tab widget
|
|
93
|
+
self.tab_widget.addTab(hkl_tab, "HKL plane")
|
|
94
|
+
|
|
95
|
+
def _connect_hkl_signals(self):
|
|
96
|
+
"""Connect signals for HKL plane tab."""
|
|
97
|
+
# Initialize button
|
|
98
|
+
self.hkl_controls.initializeClicked.connect(self.initialize_calculator_hkl)
|
|
99
|
+
|
|
100
|
+
# Plane change
|
|
101
|
+
self.hkl_controls.planeChanged.connect(self._on_plane_changed)
|
|
102
|
+
|
|
103
|
+
# Connect 2D plane updates to calculator
|
|
104
|
+
self.hkl_plane_2d.connect_value_changed_signals(
|
|
105
|
+
lambda: self.hkl_plane_2d.update_hk_plane(self.calculator),
|
|
106
|
+
lambda: self.hkl_plane_2d.update_hl_plane(self.calculator),
|
|
107
|
+
lambda: self.hkl_plane_2d.update_kl_plane(self.calculator)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Connect 2D controls to 3D plane updates
|
|
111
|
+
self.hkl_plane_2d.connect_3d_plane_signals(self.hkl_plane_3d)
|
|
112
|
+
|
|
113
|
+
def _create_customized_tab(self):
|
|
114
|
+
"""Create the customized plane subtab using components."""
|
|
115
|
+
# Create the complete customized widget
|
|
116
|
+
self.customized_plane_widget = CustomizedPlaneWidget()
|
|
117
|
+
self.customized_plane_widget.set_calculator(self.calculator)
|
|
118
|
+
|
|
119
|
+
# Connect initialize signal
|
|
120
|
+
controls = self.customized_plane_widget.get_controls()
|
|
121
|
+
controls.initializeClicked.connect(self.initialize_calculator_customized)
|
|
122
|
+
|
|
123
|
+
# Add to tab widget
|
|
124
|
+
self.tab_widget.addTab(self.customized_plane_widget, "Customized plane")
|
|
125
|
+
|
|
126
|
+
@pyqtSlot()
|
|
127
|
+
def initialize_calculator_hkl(self):
|
|
128
|
+
"""Initialize the structure factor calculator for HKL plane tab."""
|
|
129
|
+
try:
|
|
130
|
+
params = self.controller.app_controller.get_parameters() if self.controller else None
|
|
131
|
+
cif_path = params.get("cif_file") if params else None
|
|
132
|
+
if not cif_path:
|
|
133
|
+
QMessageBox.warning(
|
|
134
|
+
self,
|
|
135
|
+
"Missing CIF",
|
|
136
|
+
"Please load a valid CIF file in the initialization window first.",
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
energy_ev = self.hkl_controls.get_energy_ev()
|
|
141
|
+
self.calculator.initialize(cif_path, energy_ev)
|
|
142
|
+
|
|
143
|
+
self.hkl_controls.set_status("Calculator initialized successfully", "green")
|
|
144
|
+
|
|
145
|
+
# Populate 3D using full HKL cube ranging 0..5
|
|
146
|
+
hkl_list = self._generate_hkl_cube(5)
|
|
147
|
+
results = self.calculator.calculate_structure_factors(hkl_list)
|
|
148
|
+
self.hkl_plane_3d.visualize_structure_factors(hkl_list, results)
|
|
149
|
+
|
|
150
|
+
# Initialize 2D slices
|
|
151
|
+
self.hkl_plane_2d.update_hk_plane(self.calculator)
|
|
152
|
+
self.hkl_plane_2d.update_hl_plane(self.calculator)
|
|
153
|
+
self.hkl_plane_2d.update_kl_plane(self.calculator)
|
|
154
|
+
|
|
155
|
+
# Initialize 3D planes based on current control values
|
|
156
|
+
plane_values = self.hkl_plane_2d.get_plane_values()
|
|
157
|
+
self.hkl_plane_3d.set_plane_values(**plane_values)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
QMessageBox.critical(
|
|
161
|
+
self, "Error", f"Failed to initialize calculator: {str(e)}"
|
|
162
|
+
)
|
|
163
|
+
self.hkl_controls.set_status(f"Initialization failed - {str(e)}", "red")
|
|
164
|
+
|
|
165
|
+
@pyqtSlot()
|
|
166
|
+
def initialize_calculator_customized(self):
|
|
167
|
+
"""Initialize the structure factor calculator for customized plane tab."""
|
|
168
|
+
try:
|
|
169
|
+
params = self.controller.app_controller.get_parameters() if self.controller else None
|
|
170
|
+
cif_path = params.get("cif_file") if params else None
|
|
171
|
+
if not cif_path:
|
|
172
|
+
QMessageBox.warning(
|
|
173
|
+
self,
|
|
174
|
+
"Missing CIF",
|
|
175
|
+
"Please load a valid CIF file in the initialization window first.",
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
controls = self.customized_plane_widget.get_controls()
|
|
180
|
+
energy_ev = controls.get_energy_ev()
|
|
181
|
+
self.calculator.initialize(cif_path, energy_ev)
|
|
182
|
+
|
|
183
|
+
controls.set_status("Calculator initialized successfully", "green")
|
|
184
|
+
|
|
185
|
+
# Update plots
|
|
186
|
+
self.customized_plane_widget.update_plots()
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
QMessageBox.critical(
|
|
190
|
+
self, "Error", f"Failed to initialize calculator: {str(e)}"
|
|
191
|
+
)
|
|
192
|
+
controls = self.customized_plane_widget.get_controls()
|
|
193
|
+
controls.set_status(f"Initialization failed - {str(e)}", "red")
|
|
194
|
+
|
|
195
|
+
@pyqtSlot(str)
|
|
196
|
+
def _on_plane_changed(self, plane: str):
|
|
197
|
+
"""Handle plane change from controls."""
|
|
198
|
+
# Update 2D widget to show the selected plane
|
|
199
|
+
self.hkl_plane_2d.set_active_plane(plane)
|
|
200
|
+
|
|
201
|
+
# Update 3D plane highlighting
|
|
202
|
+
if plane == "HK":
|
|
203
|
+
self.hkl_plane_3d.set_active_plane("L") # HK plane means L constant
|
|
204
|
+
elif plane == "HL":
|
|
205
|
+
self.hkl_plane_3d.set_active_plane("K")
|
|
206
|
+
elif plane == "KL":
|
|
207
|
+
self.hkl_plane_3d.set_active_plane("H")
|
|
208
|
+
|
|
209
|
+
# Update corresponding plot if initialized
|
|
210
|
+
if self.calculator.is_initialized:
|
|
211
|
+
if plane == "HK":
|
|
212
|
+
self.hkl_plane_2d.update_hk_plane(self.calculator)
|
|
213
|
+
elif plane == "HL":
|
|
214
|
+
self.hkl_plane_2d.update_hl_plane(self.calculator)
|
|
215
|
+
elif plane == "KL":
|
|
216
|
+
self.hkl_plane_2d.update_kl_plane(self.calculator)
|
|
217
|
+
|
|
218
|
+
def _generate_hkl_cube(self, max_index: int = 5):
|
|
219
|
+
"""Generate a full integer HKL grid from 0..max_index for 3D visualization."""
|
|
220
|
+
cube = []
|
|
221
|
+
for h in range(0, max_index + 1):
|
|
222
|
+
for k in range(0, max_index + 1):
|
|
223
|
+
for l in range(0, max_index + 1):
|
|
224
|
+
cube.append([h, k, l])
|
|
225
|
+
return cube
|
|
226
|
+
|
|
227
|
+
def get_module_instance(self):
|
|
228
|
+
"""Get the backend module instance."""
|
|
229
|
+
return self.calculator
|
|
230
|
+
|
|
231
|
+
def clear(self):
|
|
232
|
+
"""Clear all inputs and results."""
|
|
233
|
+
# Clear visualizations
|
|
234
|
+
if hasattr(self, 'hkl_plane_2d'):
|
|
235
|
+
self.hkl_plane_2d.clear_plots()
|
|
236
|
+
if hasattr(self, 'customized_plane_widget'):
|
|
237
|
+
self.customized_plane_widget.clear_plots()
|
|
238
|
+
|
|
239
|
+
def get_state(self):
|
|
240
|
+
"""Get the current state for session saving."""
|
|
241
|
+
state = {}
|
|
242
|
+
if hasattr(self, 'hkl_controls'):
|
|
243
|
+
state["hkl_energy"] = self.hkl_controls.get_energy_ev()
|
|
244
|
+
if hasattr(self, 'customized_plane_widget'):
|
|
245
|
+
controls = self.customized_plane_widget.get_controls()
|
|
246
|
+
state["custom_energy"] = controls.get_energy_ev()
|
|
247
|
+
return state
|
|
248
|
+
|
|
249
|
+
def set_state(self, state):
|
|
250
|
+
"""Restore tab state from saved session."""
|
|
251
|
+
try:
|
|
252
|
+
if "hkl_energy" in state and hasattr(self, 'hkl_controls'):
|
|
253
|
+
self.hkl_controls.energy_input.energy_ev = state["hkl_energy"]
|
|
254
|
+
|
|
255
|
+
if "custom_energy" in state and hasattr(self, 'customized_plane_widget'):
|
|
256
|
+
controls = self.customized_plane_widget.get_controls()
|
|
257
|
+
controls.energy_input.energy_ev = state["custom_energy"]
|
|
258
|
+
|
|
259
|
+
# Try to reinitialize if we have the required data globally
|
|
260
|
+
params = self.controller.app_controller.get_parameters() if self.controller else None
|
|
261
|
+
if (
|
|
262
|
+
params
|
|
263
|
+
and params.get("cif_file")
|
|
264
|
+
and os.path.exists(params.get("cif_file"))
|
|
265
|
+
):
|
|
266
|
+
if hasattr(self, 'hkl_controls'):
|
|
267
|
+
self.initialize_calculator_hkl()
|
|
268
|
+
if hasattr(self, 'customized_plane_widget'):
|
|
269
|
+
self.initialize_calculator_customized()
|
|
270
|
+
|
|
271
|
+
return True
|
|
272
|
+
except Exception:
|
|
273
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app_name": "Advisor-Scattering",
|
|
3
|
+
"app_version": "0.1.0",
|
|
4
|
+
"window_size": {
|
|
5
|
+
"width": 1200,
|
|
6
|
+
"height": 800
|
|
7
|
+
},
|
|
8
|
+
"theme": "default",
|
|
9
|
+
"data_directory": "data",
|
|
10
|
+
"figures_directory": "figures",
|
|
11
|
+
"save_session_on_exit": true,
|
|
12
|
+
"show_welcome_screen": true,
|
|
13
|
+
"debug_mode": false
|
|
14
|
+
}
|