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.
Files changed (69) hide show
  1. advisor/__init__.py +3 -0
  2. advisor/__main__.py +7 -0
  3. advisor/app.py +40 -0
  4. advisor/controllers/__init__.py +6 -0
  5. advisor/controllers/app_controller.py +69 -0
  6. advisor/controllers/feature_controller.py +25 -0
  7. advisor/domain/__init__.py +23 -0
  8. advisor/domain/core/__init__.py +8 -0
  9. advisor/domain/core/lab.py +121 -0
  10. advisor/domain/core/lattice.py +79 -0
  11. advisor/domain/core/sample.py +101 -0
  12. advisor/domain/geometry.py +212 -0
  13. advisor/domain/unit_converter.py +82 -0
  14. advisor/features/__init__.py +6 -0
  15. advisor/features/scattering_geometry/controllers/__init__.py +5 -0
  16. advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
  17. advisor/features/scattering_geometry/domain/__init__.py +5 -0
  18. advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
  19. advisor/features/scattering_geometry/domain/core.py +516 -0
  20. advisor/features/scattering_geometry/ui/__init__.py +5 -0
  21. advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
  22. advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
  23. advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
  24. advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
  25. advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
  26. advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
  27. advisor/features/structure_factor/controllers/__init__.py +6 -0
  28. advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
  29. advisor/features/structure_factor/domain/__init__.py +6 -0
  30. advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
  31. advisor/features/structure_factor/ui/__init__.py +6 -0
  32. advisor/features/structure_factor/ui/components/__init__.py +12 -0
  33. advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
  34. advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
  35. advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
  36. advisor/resources/__init__.py +0 -0
  37. advisor/resources/config/app_config.json +14 -0
  38. advisor/resources/config/tips.json +4 -0
  39. advisor/resources/data/nacl.cif +111 -0
  40. advisor/resources/icons/bz_caculator.jpg +0 -0
  41. advisor/resources/icons/bz_calculator.png +0 -0
  42. advisor/resources/icons/minus.svg +3 -0
  43. advisor/resources/icons/placeholder.png +0 -0
  44. advisor/resources/icons/plus.svg +3 -0
  45. advisor/resources/icons/reset.png +0 -0
  46. advisor/resources/icons/sf_calculator.jpg +0 -0
  47. advisor/resources/icons/sf_calculator.png +0 -0
  48. advisor/resources/icons.qrc +6 -0
  49. advisor/resources/qss/styles.qss +348 -0
  50. advisor/resources/resources_rc.py +83 -0
  51. advisor/ui/__init__.py +7 -0
  52. advisor/ui/init_window.py +566 -0
  53. advisor/ui/main_window.py +174 -0
  54. advisor/ui/tab_interface.py +44 -0
  55. advisor/ui/tips.py +30 -0
  56. advisor/ui/utils/__init__.py +6 -0
  57. advisor/ui/utils/readcif.py +129 -0
  58. advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
  59. advisor/ui/visualizers/__init__.py +8 -0
  60. advisor/ui/visualizers/coordinate_visualizer.py +203 -0
  61. advisor/ui/visualizers/scattering_visualizer.py +301 -0
  62. advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
  63. advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
  64. advisor/ui/visualizers/unitcell_visualizer.py +518 -0
  65. advisor_scattering-0.5.0.dist-info/METADATA +122 -0
  66. advisor_scattering-0.5.0.dist-info/RECORD +69 -0
  67. advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
  68. advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
  69. 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()