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,566 @@
|
|
|
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
|
+
QLineEdit,
|
|
10
|
+
QPushButton,
|
|
11
|
+
QDoubleSpinBox,
|
|
12
|
+
QGroupBox,
|
|
13
|
+
QMessageBox,
|
|
14
|
+
QFileDialog,
|
|
15
|
+
)
|
|
16
|
+
from PyQt5.QtCore import pyqtSlot, pyqtSignal
|
|
17
|
+
from PyQt5.QtGui import QDragEnterEvent, QDropEvent
|
|
18
|
+
|
|
19
|
+
from advisor.domain import UnitConverter
|
|
20
|
+
from advisor.ui.utils import readcif
|
|
21
|
+
from advisor.ui.visualizers import CoordinateVisualizer, UnitcellVisualizer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DragDropLineEdit(QLineEdit):
|
|
25
|
+
"""Custom QLineEdit that accepts drag and drop events."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.setAcceptDrops(True)
|
|
30
|
+
self.setPlaceholderText("Drag and drop CIF file here or click Browse...")
|
|
31
|
+
self.setReadOnly(True)
|
|
32
|
+
|
|
33
|
+
def dragEnterEvent(self, event: QDragEnterEvent):
|
|
34
|
+
if event.mimeData().hasUrls():
|
|
35
|
+
urls = event.mimeData().urls()
|
|
36
|
+
if urls and urls[0].toLocalFile().endswith(".cif"):
|
|
37
|
+
event.acceptProposedAction()
|
|
38
|
+
|
|
39
|
+
def dropEvent(self, event: QDropEvent):
|
|
40
|
+
urls = event.mimeData().urls()
|
|
41
|
+
if urls:
|
|
42
|
+
file_path = urls[0].toLocalFile()
|
|
43
|
+
if file_path.endswith(".cif"):
|
|
44
|
+
self.setText(file_path)
|
|
45
|
+
self.textChanged.emit(file_path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DragDropGroupBox(QGroupBox):
|
|
49
|
+
"""QGroupBox that accepts CIF file drag-and-drop anywhere in the panel.
|
|
50
|
+
|
|
51
|
+
When a valid CIF is dropped, it will update the target line edit.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, title: str = "", parent=None):
|
|
55
|
+
super().__init__(title, parent)
|
|
56
|
+
self.setAcceptDrops(True)
|
|
57
|
+
self._target_line_edit: QLineEdit = None
|
|
58
|
+
|
|
59
|
+
def set_target_line_edit(self, line_edit: QLineEdit):
|
|
60
|
+
self._target_line_edit = line_edit
|
|
61
|
+
|
|
62
|
+
def dragEnterEvent(self, event: QDragEnterEvent):
|
|
63
|
+
if event.mimeData().hasUrls():
|
|
64
|
+
urls = event.mimeData().urls()
|
|
65
|
+
if urls and urls[0].toLocalFile().endswith(".cif"):
|
|
66
|
+
event.acceptProposedAction()
|
|
67
|
+
|
|
68
|
+
def dropEvent(self, event: QDropEvent):
|
|
69
|
+
urls = event.mimeData().urls()
|
|
70
|
+
if not urls:
|
|
71
|
+
return
|
|
72
|
+
file_path = urls[0].toLocalFile()
|
|
73
|
+
if file_path.endswith(".cif") and self._target_line_edit is not None:
|
|
74
|
+
# Update target line edit; it will emit textChanged and trigger parsing
|
|
75
|
+
self._target_line_edit.setText(file_path)
|
|
76
|
+
self._target_line_edit.textChanged.emit(file_path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InitWindow(QWidget):
|
|
80
|
+
"""Initialization window for setting up lattice parameters."""
|
|
81
|
+
|
|
82
|
+
initialized = pyqtSignal(dict)
|
|
83
|
+
|
|
84
|
+
def __init__(self, controller=None):
|
|
85
|
+
super().__init__()
|
|
86
|
+
self.controller = controller
|
|
87
|
+
self.unit_converter = UnitConverter()
|
|
88
|
+
self._lattice_locked = False
|
|
89
|
+
self._accepted_cif_path = None
|
|
90
|
+
self.init_ui()
|
|
91
|
+
|
|
92
|
+
def init_ui(self):
|
|
93
|
+
"""Initialize UI components."""
|
|
94
|
+
layout = QGridLayout(self)
|
|
95
|
+
layout.setContentsMargins(20, 20, 20, 20)
|
|
96
|
+
layout.setSpacing(40)
|
|
97
|
+
|
|
98
|
+
# Group box for lattice parameters
|
|
99
|
+
lattice_group = QGroupBox("Lattice Parameters")
|
|
100
|
+
lattice_layout = QGridLayout(lattice_group)
|
|
101
|
+
|
|
102
|
+
# Lattice constants (left column)
|
|
103
|
+
self.a_input = QDoubleSpinBox()
|
|
104
|
+
self.a_input.setRange(0.1, 100.0)
|
|
105
|
+
self.a_input.setValue(5.0)
|
|
106
|
+
self.a_input.setSuffix(" Å")
|
|
107
|
+
lattice_layout.addWidget(QLabel("a:"), 0, 0)
|
|
108
|
+
lattice_layout.addWidget(self.a_input, 0, 1)
|
|
109
|
+
|
|
110
|
+
self.b_input = QDoubleSpinBox()
|
|
111
|
+
self.b_input.setRange(0.1, 100.0)
|
|
112
|
+
self.b_input.setValue(5.0)
|
|
113
|
+
self.b_input.setSuffix(" Å")
|
|
114
|
+
lattice_layout.addWidget(QLabel("b:"), 1, 0)
|
|
115
|
+
lattice_layout.addWidget(self.b_input, 1, 1)
|
|
116
|
+
|
|
117
|
+
self.c_input = QDoubleSpinBox()
|
|
118
|
+
self.c_input.setRange(0.1, 100.0)
|
|
119
|
+
self.c_input.setValue(5.0)
|
|
120
|
+
self.c_input.setSuffix(" Å")
|
|
121
|
+
lattice_layout.addWidget(QLabel("c:"), 2, 0)
|
|
122
|
+
lattice_layout.addWidget(self.c_input, 2, 1)
|
|
123
|
+
|
|
124
|
+
# Lattice angles (right column)
|
|
125
|
+
self.alpha_input = QDoubleSpinBox()
|
|
126
|
+
self.alpha_input.setRange(1.0, 179.0)
|
|
127
|
+
self.alpha_input.setValue(90.0)
|
|
128
|
+
self.alpha_input.setSuffix(" °")
|
|
129
|
+
self.alpha_input.valueChanged.connect(self.update_visualization)
|
|
130
|
+
lattice_layout.addWidget(QLabel("α:"), 0, 2)
|
|
131
|
+
lattice_layout.addWidget(self.alpha_input, 0, 3)
|
|
132
|
+
|
|
133
|
+
self.beta_input = QDoubleSpinBox()
|
|
134
|
+
self.beta_input.setRange(1.0, 179.0)
|
|
135
|
+
self.beta_input.setValue(90.0)
|
|
136
|
+
self.beta_input.setSuffix(" °")
|
|
137
|
+
self.beta_input.valueChanged.connect(self.update_visualization)
|
|
138
|
+
lattice_layout.addWidget(QLabel("β:"), 1, 2)
|
|
139
|
+
lattice_layout.addWidget(self.beta_input, 1, 3)
|
|
140
|
+
|
|
141
|
+
self.gamma_input = QDoubleSpinBox()
|
|
142
|
+
self.gamma_input.setRange(1.0, 179.0)
|
|
143
|
+
self.gamma_input.setValue(90.0)
|
|
144
|
+
self.gamma_input.setSuffix(" °")
|
|
145
|
+
self.gamma_input.valueChanged.connect(self.update_visualization)
|
|
146
|
+
lattice_layout.addWidget(QLabel("γ:"), 2, 2)
|
|
147
|
+
lattice_layout.addWidget(self.gamma_input, 2, 3)
|
|
148
|
+
|
|
149
|
+
# Add spacing between columns and margins
|
|
150
|
+
lattice_layout.setColumnStretch(1, 1)
|
|
151
|
+
lattice_layout.setColumnStretch(3, 1)
|
|
152
|
+
lattice_layout.setHorizontalSpacing(40)
|
|
153
|
+
lattice_layout.setVerticalSpacing(10)
|
|
154
|
+
lattice_layout.setContentsMargins(20, 20, 20, 20)
|
|
155
|
+
|
|
156
|
+
# Add lattice group to main layout at (0,0)
|
|
157
|
+
layout.addWidget(lattice_group, 0, 0)
|
|
158
|
+
|
|
159
|
+
# Group box for X-ray energy
|
|
160
|
+
energy_group = QGroupBox("X-ray Energy")
|
|
161
|
+
energy_layout = QFormLayout(energy_group)
|
|
162
|
+
|
|
163
|
+
self.energy_input = QDoubleSpinBox()
|
|
164
|
+
self.energy_input.setRange(0, 1000000)
|
|
165
|
+
self.energy_input.setValue(950.0)
|
|
166
|
+
self.energy_input.setSuffix(" eV")
|
|
167
|
+
self.energy_input.valueChanged.connect(self.on_energy_changed)
|
|
168
|
+
energy_layout.addRow("Energy:", self.energy_input)
|
|
169
|
+
|
|
170
|
+
self.wavelength_input = QDoubleSpinBox()
|
|
171
|
+
self.wavelength_input.setRange(0, 1000)
|
|
172
|
+
self.wavelength_input.setDecimals(3)
|
|
173
|
+
self.wavelength_input.setValue(self.unit_converter.ev_to_angstrom(950.0))
|
|
174
|
+
self.wavelength_input.setSuffix(" Å")
|
|
175
|
+
self.wavelength_input.valueChanged.connect(self.on_wavelength_changed)
|
|
176
|
+
energy_layout.addRow("λ:", self.wavelength_input)
|
|
177
|
+
|
|
178
|
+
# wavevector
|
|
179
|
+
self.wavevector_input = QDoubleSpinBox()
|
|
180
|
+
self.wavevector_input.setRange(0, 100)
|
|
181
|
+
self.wavevector_input.setDecimals(3)
|
|
182
|
+
self.wavevector_input.setValue(2 * 3.1415926 / self.wavelength_input.value())
|
|
183
|
+
self.wavevector_input.setSuffix(" Å⁻¹")
|
|
184
|
+
self.wavevector_input.valueChanged.connect(self.on_wavevector_changed)
|
|
185
|
+
energy_layout.addRow("|k|:", self.wavevector_input)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Add energy group to main layout at (0,1)
|
|
189
|
+
layout.addWidget(energy_group, 0, 1)
|
|
190
|
+
|
|
191
|
+
# Group box for Euler angles
|
|
192
|
+
euler_group = QGroupBox("Euler Angles")
|
|
193
|
+
euler_layout = QFormLayout(euler_group)
|
|
194
|
+
|
|
195
|
+
self.roll_input = QDoubleSpinBox()
|
|
196
|
+
self.roll_input.setObjectName("eulerAngleSpinBox")
|
|
197
|
+
self.roll_input.setRange(-180.0, 180.0)
|
|
198
|
+
self.roll_input.setValue(0.0)
|
|
199
|
+
self.roll_input.setSuffix(" °")
|
|
200
|
+
self.roll_input.setToolTip("Rotation about the new X axis")
|
|
201
|
+
self.roll_input.valueChanged.connect(self.update_visualization)
|
|
202
|
+
euler_layout.addRow("Roll:", self.roll_input)
|
|
203
|
+
|
|
204
|
+
self.pitch_input = QDoubleSpinBox()
|
|
205
|
+
self.pitch_input.setObjectName("eulerAngleSpinBox")
|
|
206
|
+
self.pitch_input.setRange(-180.0, 180.0)
|
|
207
|
+
self.pitch_input.setValue(0.0)
|
|
208
|
+
self.pitch_input.setSuffix(" °")
|
|
209
|
+
self.pitch_input.setToolTip("Rotation about the new Y axis")
|
|
210
|
+
self.pitch_input.valueChanged.connect(self.update_visualization)
|
|
211
|
+
euler_layout.addRow("Pitch:", self.pitch_input)
|
|
212
|
+
|
|
213
|
+
self.yaw_input = QDoubleSpinBox()
|
|
214
|
+
self.yaw_input.setObjectName("eulerAngleSpinBox")
|
|
215
|
+
self.yaw_input.setRange(-180.0, 180.0)
|
|
216
|
+
self.yaw_input.setValue(0.0)
|
|
217
|
+
self.yaw_input.setSuffix(" °")
|
|
218
|
+
self.yaw_input.setToolTip("Rotation about the original Z axis")
|
|
219
|
+
self.yaw_input.valueChanged.connect(self.update_visualization)
|
|
220
|
+
euler_layout.addRow("Yaw:", self.yaw_input)
|
|
221
|
+
|
|
222
|
+
# Add euler group to main layout at (0,2)
|
|
223
|
+
layout.addWidget(euler_group, 0, 2)
|
|
224
|
+
|
|
225
|
+
# Create and add the coordinate visualizer
|
|
226
|
+
self.visualizer = CoordinateVisualizer()
|
|
227
|
+
# initialize the visualizer
|
|
228
|
+
self.visualizer.initialize(
|
|
229
|
+
{
|
|
230
|
+
"a": self.a_input.value(),
|
|
231
|
+
"b": self.b_input.value(),
|
|
232
|
+
"c": self.c_input.value(),
|
|
233
|
+
"alpha": self.alpha_input.value(),
|
|
234
|
+
"beta": self.beta_input.value(),
|
|
235
|
+
"gamma": self.gamma_input.value(),
|
|
236
|
+
"roll": self.roll_input.value(),
|
|
237
|
+
"pitch": self.pitch_input.value(),
|
|
238
|
+
"yaw": self.yaw_input.value(),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
self.visualizer.visualize_lab_system()
|
|
242
|
+
layout.addWidget(self.visualizer, 1, 1)
|
|
243
|
+
|
|
244
|
+
# Create and add the unit cell visualizer
|
|
245
|
+
self.unitcell_visualizer = UnitcellVisualizer()
|
|
246
|
+
layout.addWidget(self.unitcell_visualizer, 1, 2)
|
|
247
|
+
|
|
248
|
+
# File input area
|
|
249
|
+
file_group = DragDropGroupBox("Crystal Structure File")
|
|
250
|
+
file_layout = QGridLayout(file_group)
|
|
251
|
+
|
|
252
|
+
self.file_path_input = DragDropLineEdit()
|
|
253
|
+
self.file_path_input.textChanged.connect(self.on_cif_file_changed)
|
|
254
|
+
file_layout.addWidget(self.file_path_input, 0, 0)
|
|
255
|
+
|
|
256
|
+
browse_button = QPushButton("Browse...")
|
|
257
|
+
browse_button.clicked.connect(self.browse_cif_file)
|
|
258
|
+
file_layout.addWidget(browse_button, 0, 1)
|
|
259
|
+
|
|
260
|
+
# Allow dropping on the entire group box
|
|
261
|
+
file_group.set_target_line_edit(self.file_path_input)
|
|
262
|
+
|
|
263
|
+
# Add file group to main layout at (1,0) spanning 1 column
|
|
264
|
+
layout.addWidget(file_group, 1, 0, 1, 1)
|
|
265
|
+
|
|
266
|
+
# Initialize button
|
|
267
|
+
initialize_button = QPushButton("Initialize")
|
|
268
|
+
initialize_button.clicked.connect(self.initialize)
|
|
269
|
+
# Add initialize button at (2,0) spanning 1 column
|
|
270
|
+
layout.addWidget(initialize_button, 2, 0, 1, 1)
|
|
271
|
+
|
|
272
|
+
# Add spacer
|
|
273
|
+
layout.setRowStretch(3, 1)
|
|
274
|
+
|
|
275
|
+
@pyqtSlot()
|
|
276
|
+
def browse_cif_file(self):
|
|
277
|
+
"""Browse for CIF file."""
|
|
278
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
279
|
+
self, "Open CIF File", "", "CIF Files (*.cif);;All Files (*)"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if file_path:
|
|
283
|
+
self.file_path_input.setText(file_path)
|
|
284
|
+
# on_cif_file_changed will be triggered by textChanged
|
|
285
|
+
|
|
286
|
+
def set_lattice_inputs_enabled(self, enabled: bool):
|
|
287
|
+
"""Enable/disable lattice parameter inputs (a,b,c,alpha,beta,gamma)."""
|
|
288
|
+
self.a_input.setEnabled(enabled)
|
|
289
|
+
self.b_input.setEnabled(enabled)
|
|
290
|
+
self.c_input.setEnabled(enabled)
|
|
291
|
+
self.alpha_input.setEnabled(enabled)
|
|
292
|
+
self.beta_input.setEnabled(enabled)
|
|
293
|
+
self.gamma_input.setEnabled(enabled)
|
|
294
|
+
|
|
295
|
+
@pyqtSlot(str)
|
|
296
|
+
def on_cif_file_changed(self, file_path: str):
|
|
297
|
+
"""Handle CIF file path changes: parse CIF and apply lattice params.
|
|
298
|
+
|
|
299
|
+
Once a CIF is successfully applied, lattice inputs are locked for the session.
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
if self._lattice_locked:
|
|
303
|
+
# Prevent changing CIF after lock
|
|
304
|
+
if self._accepted_cif_path and file_path != self._accepted_cif_path:
|
|
305
|
+
QMessageBox.warning(
|
|
306
|
+
self,
|
|
307
|
+
"Lattice Parameters Locked",
|
|
308
|
+
"Lattice parameters are locked from a previously accepted CIF. "
|
|
309
|
+
"Restart the application to change the CIF.",
|
|
310
|
+
)
|
|
311
|
+
# revert displayed path
|
|
312
|
+
# Block signal to avoid recursion
|
|
313
|
+
self.file_path_input.blockSignals(True)
|
|
314
|
+
self.file_path_input.setText(self._accepted_cif_path)
|
|
315
|
+
self.file_path_input.blockSignals(False)
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Empty path - user cleared the field; keep inputs editable
|
|
319
|
+
if not file_path:
|
|
320
|
+
self._accepted_cif_path = None
|
|
321
|
+
self._lattice_locked = False
|
|
322
|
+
self.set_lattice_inputs_enabled(True)
|
|
323
|
+
# Clear unit cell visualization
|
|
324
|
+
self.clear_unitcell_visualization()
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Parse CIF using custom readcif function
|
|
328
|
+
cif = readcif(file_path)
|
|
329
|
+
if not cif or len(cif.keys()) == 0:
|
|
330
|
+
raise ValueError("No data found in CIF file")
|
|
331
|
+
|
|
332
|
+
def parse_numeric(value) -> float:
|
|
333
|
+
s = str(value).strip()
|
|
334
|
+
if "(" in s:
|
|
335
|
+
s = s.split("(")[0]
|
|
336
|
+
return float(s)
|
|
337
|
+
|
|
338
|
+
def get_float(key: str) -> float:
|
|
339
|
+
value = cif.get(key)
|
|
340
|
+
if value is None:
|
|
341
|
+
raise KeyError(f"Missing required CIF field: {key}")
|
|
342
|
+
return parse_numeric(value)
|
|
343
|
+
|
|
344
|
+
a = get_float("_cell_length_a")
|
|
345
|
+
b = get_float("_cell_length_b")
|
|
346
|
+
c = get_float("_cell_length_c")
|
|
347
|
+
alpha = get_float("_cell_angle_alpha")
|
|
348
|
+
beta = get_float("_cell_angle_beta")
|
|
349
|
+
gamma = get_float("_cell_angle_gamma")
|
|
350
|
+
|
|
351
|
+
# Basic validation (units assumed Angstroms and degrees)
|
|
352
|
+
if min(a, b, c) <= 0:
|
|
353
|
+
raise ValueError("Cell lengths must be positive")
|
|
354
|
+
for ang, name in [(alpha, "alpha"), (beta, "beta"), (gamma, "gamma")]:
|
|
355
|
+
if not (0.0 < ang < 180.0):
|
|
356
|
+
raise ValueError(
|
|
357
|
+
f"Cell angle {name} must be between 0 and 180 degrees"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Apply to UI and lock
|
|
361
|
+
self.apply_cif_parameters(a, b, c, alpha, beta, gamma)
|
|
362
|
+
self._lattice_locked = True
|
|
363
|
+
self._accepted_cif_path = file_path
|
|
364
|
+
self.set_lattice_inputs_enabled(False)
|
|
365
|
+
|
|
366
|
+
# Update unit cell visualizer with the CIF file
|
|
367
|
+
self.update_unitcell_visualization(file_path)
|
|
368
|
+
|
|
369
|
+
QMessageBox.information(
|
|
370
|
+
self,
|
|
371
|
+
"CIF Loaded",
|
|
372
|
+
"Lattice parameters have been loaded from the CIF and inputs are now locked.",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
QMessageBox.warning(
|
|
377
|
+
self,
|
|
378
|
+
"Invalid CIF",
|
|
379
|
+
f"Failed to read lattice parameters from CIF: {str(e)}",
|
|
380
|
+
)
|
|
381
|
+
# clear text and keep inputs editable
|
|
382
|
+
self.file_path_input.blockSignals(True)
|
|
383
|
+
self.file_path_input.clear()
|
|
384
|
+
self.file_path_input.blockSignals(False)
|
|
385
|
+
self._accepted_cif_path = None
|
|
386
|
+
self._lattice_locked = False
|
|
387
|
+
self.set_lattice_inputs_enabled(True)
|
|
388
|
+
|
|
389
|
+
def apply_cif_parameters(
|
|
390
|
+
self, a: float, b: float, c: float, alpha: float, beta: float, gamma: float
|
|
391
|
+
):
|
|
392
|
+
"""Set lattice parameter inputs from parsed CIF values and refresh visualization."""
|
|
393
|
+
# Block signals to avoid multiple redraws
|
|
394
|
+
self.a_input.blockSignals(True)
|
|
395
|
+
self.b_input.blockSignals(True)
|
|
396
|
+
self.c_input.blockSignals(True)
|
|
397
|
+
self.alpha_input.blockSignals(True)
|
|
398
|
+
self.beta_input.blockSignals(True)
|
|
399
|
+
self.gamma_input.blockSignals(True)
|
|
400
|
+
|
|
401
|
+
self.a_input.setValue(a)
|
|
402
|
+
self.b_input.setValue(b)
|
|
403
|
+
self.c_input.setValue(c)
|
|
404
|
+
self.alpha_input.setValue(alpha)
|
|
405
|
+
self.beta_input.setValue(beta)
|
|
406
|
+
self.gamma_input.setValue(gamma)
|
|
407
|
+
|
|
408
|
+
self.a_input.blockSignals(False)
|
|
409
|
+
self.b_input.blockSignals(False)
|
|
410
|
+
self.c_input.blockSignals(False)
|
|
411
|
+
self.alpha_input.blockSignals(False)
|
|
412
|
+
self.beta_input.blockSignals(False)
|
|
413
|
+
self.gamma_input.blockSignals(False)
|
|
414
|
+
|
|
415
|
+
# Update visualization with new parameters
|
|
416
|
+
self.update_visualization()
|
|
417
|
+
|
|
418
|
+
def update_unitcell_visualization(self, cif_file_path: str):
|
|
419
|
+
"""Update the unit cell visualizer with a new CIF file."""
|
|
420
|
+
try:
|
|
421
|
+
self.unitcell_visualizer.set_parameters({"cif_file": cif_file_path})
|
|
422
|
+
self.unitcell_visualizer.visualize_unitcell()
|
|
423
|
+
except Exception as e:
|
|
424
|
+
print(f"Error updating unit cell visualization: {e}")
|
|
425
|
+
|
|
426
|
+
def clear_unitcell_visualization(self):
|
|
427
|
+
"""Clear the unit cell visualization."""
|
|
428
|
+
try:
|
|
429
|
+
self.unitcell_visualizer.axes.clear()
|
|
430
|
+
self.unitcell_visualizer.axes.set_facecolor("white")
|
|
431
|
+
self.unitcell_visualizer.axes.set_axis_off()
|
|
432
|
+
self.unitcell_visualizer.draw()
|
|
433
|
+
except Exception as e:
|
|
434
|
+
print(f"Error clearing unit cell visualization: {e}")
|
|
435
|
+
|
|
436
|
+
@pyqtSlot()
|
|
437
|
+
def update_visualization(self):
|
|
438
|
+
"""Update the coordinate visualization when vectors change."""
|
|
439
|
+
try:
|
|
440
|
+
# Get current values
|
|
441
|
+
roll = self.roll_input.value()
|
|
442
|
+
pitch = self.pitch_input.value()
|
|
443
|
+
yaw = self.yaw_input.value()
|
|
444
|
+
|
|
445
|
+
# Validate values are within range
|
|
446
|
+
if not (
|
|
447
|
+
-180 <= roll <= 180 and -180 <= pitch <= 180 and -180 <= yaw <= 180
|
|
448
|
+
):
|
|
449
|
+
# Reset to default values if invalid
|
|
450
|
+
self.roll_input.setValue(0.0)
|
|
451
|
+
self.pitch_input.setValue(0.0)
|
|
452
|
+
self.yaw_input.setValue(0.0)
|
|
453
|
+
roll, pitch, yaw = 0.0, 0.0, 0.0
|
|
454
|
+
|
|
455
|
+
# Update the visualizer
|
|
456
|
+
self.visualizer.initialize(
|
|
457
|
+
{
|
|
458
|
+
"a": self.a_input.value(),
|
|
459
|
+
"b": self.b_input.value(),
|
|
460
|
+
"c": self.c_input.value(),
|
|
461
|
+
"alpha": self.alpha_input.value(),
|
|
462
|
+
"beta": self.beta_input.value(),
|
|
463
|
+
"gamma": self.gamma_input.value(),
|
|
464
|
+
"roll": roll,
|
|
465
|
+
"pitch": pitch,
|
|
466
|
+
"yaw": yaw,
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
self.visualizer.visualize_lab_system()
|
|
470
|
+
except Exception as e:
|
|
471
|
+
raise e
|
|
472
|
+
|
|
473
|
+
@pyqtSlot()
|
|
474
|
+
def initialize(self):
|
|
475
|
+
"""Initialize the application with the provided parameters."""
|
|
476
|
+
try:
|
|
477
|
+
# Get parameters
|
|
478
|
+
params = {
|
|
479
|
+
"a": self.a_input.value(),
|
|
480
|
+
"b": self.b_input.value(),
|
|
481
|
+
"c": self.c_input.value(),
|
|
482
|
+
"alpha": self.alpha_input.value(),
|
|
483
|
+
"beta": self.beta_input.value(),
|
|
484
|
+
"gamma": self.gamma_input.value(),
|
|
485
|
+
"energy": self.energy_input.value(),
|
|
486
|
+
"cif_file": (
|
|
487
|
+
self.file_path_input.text() if self.file_path_input.text() else None
|
|
488
|
+
),
|
|
489
|
+
"roll": self.roll_input.value(),
|
|
490
|
+
"pitch": self.pitch_input.value(),
|
|
491
|
+
"yaw": self.yaw_input.value(),
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
self.initialized.emit(params)
|
|
495
|
+
|
|
496
|
+
except Exception as e:
|
|
497
|
+
QMessageBox.critical(
|
|
498
|
+
self, "Error", f"Error initializing parameters: {str(e)}"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
@pyqtSlot()
|
|
502
|
+
def on_energy_changed(self):
|
|
503
|
+
"""Update wavelength when energy changes."""
|
|
504
|
+
try:
|
|
505
|
+
# Block signals to prevent infinite loop
|
|
506
|
+
self.wavelength_input.blockSignals(True)
|
|
507
|
+
self.wavevector_input.blockSignals(True)
|
|
508
|
+
# Convert energy to wavelength
|
|
509
|
+
wavelength = self.unit_converter.ev_to_angstrom(self.energy_input.value())
|
|
510
|
+
self.wavelength_input.setValue(wavelength)
|
|
511
|
+
wavevector = 2 * 3.1415926 / wavelength
|
|
512
|
+
self.wavevector_input.setValue(wavevector)
|
|
513
|
+
# Unblock signals
|
|
514
|
+
self.wavelength_input.blockSignals(False)
|
|
515
|
+
self.wavevector_input.blockSignals(False)
|
|
516
|
+
except Exception as e:
|
|
517
|
+
QMessageBox.warning(self, "Warning", f"Error converting energy: {str(e)}")
|
|
518
|
+
|
|
519
|
+
@pyqtSlot()
|
|
520
|
+
def on_wavelength_changed(self):
|
|
521
|
+
"""Update energy when wavelength changes."""
|
|
522
|
+
try:
|
|
523
|
+
# Block signals to prevent infinite loop
|
|
524
|
+
self.energy_input.blockSignals(True)
|
|
525
|
+
self.wavevector_input.blockSignals(True)
|
|
526
|
+
# Convert wavelength to energy
|
|
527
|
+
wavelength_value = self.wavelength_input.value()
|
|
528
|
+
energy = self.unit_converter.angstrom_to_ev(wavelength_value)
|
|
529
|
+
self.energy_input.setValue(energy)
|
|
530
|
+
wavevector = 2 * 3.1415926 / wavelength_value
|
|
531
|
+
self.wavevector_input.setValue(wavevector)
|
|
532
|
+
# Unblock signals
|
|
533
|
+
self.energy_input.blockSignals(False)
|
|
534
|
+
self.wavevector_input.blockSignals(False)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
QMessageBox.warning(
|
|
537
|
+
self, "Warning", f"Error converting wavelength: {str(e)}"
|
|
538
|
+
)
|
|
539
|
+
@pyqtSlot()
|
|
540
|
+
def on_wavevector_changed(self):
|
|
541
|
+
"""Update energy and wavelength when wavevector changes."""
|
|
542
|
+
try:
|
|
543
|
+
# Block signals to prevent infinite loop
|
|
544
|
+
self.energy_input.blockSignals(True)
|
|
545
|
+
self.wavelength_input.blockSignals(True)
|
|
546
|
+
# Convert wavevector to energy
|
|
547
|
+
wavelength = 2 * 3.1415926 / self.wavevector_input.value()
|
|
548
|
+
self.wavelength_input.setValue(wavelength)
|
|
549
|
+
energy = self.unit_converter.angstrom_to_ev(wavelength)
|
|
550
|
+
self.energy_input.setValue(energy)
|
|
551
|
+
# Unblock signals
|
|
552
|
+
self.energy_input.blockSignals(False)
|
|
553
|
+
self.wavelength_input.blockSignals(False)
|
|
554
|
+
except Exception as e:
|
|
555
|
+
QMessageBox.warning(self, "Warning", f"Error converting wavevector: {str(e)}")
|
|
556
|
+
|
|
557
|
+
def reset_inputs(self):
|
|
558
|
+
"""Clear CIF lock and re-enable lattice inputs."""
|
|
559
|
+
self._lattice_locked = False
|
|
560
|
+
self._accepted_cif_path = None
|
|
561
|
+
self.file_path_input.blockSignals(True)
|
|
562
|
+
self.file_path_input.clear()
|
|
563
|
+
self.file_path_input.blockSignals(False)
|
|
564
|
+
self.set_lattice_inputs_enabled(True)
|
|
565
|
+
self.clear_unitcell_visualization()
|
|
566
|
+
self.update_visualization()
|