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,174 @@
|
|
|
1
|
+
"""Main window view for Advisor-Scattering."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from PyQt5.QtWidgets import (
|
|
7
|
+
QAction,
|
|
8
|
+
QFileDialog,
|
|
9
|
+
QGridLayout,
|
|
10
|
+
QMainWindow,
|
|
11
|
+
QMessageBox,
|
|
12
|
+
QStatusBar,
|
|
13
|
+
QStackedWidget,
|
|
14
|
+
QTabWidget,
|
|
15
|
+
QToolBar,
|
|
16
|
+
QWidget,
|
|
17
|
+
)
|
|
18
|
+
from PyQt5.QtCore import Qt, QSize
|
|
19
|
+
from PyQt5.QtGui import QIcon
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MainWindow(QMainWindow):
|
|
23
|
+
"""Application shell: hosts init view and feature tabs."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, controller):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.controller = controller
|
|
28
|
+
|
|
29
|
+
self.init_view: Optional[QWidget] = None
|
|
30
|
+
self.tabs_loaded = False
|
|
31
|
+
|
|
32
|
+
self._setup_window()
|
|
33
|
+
self._build_layout()
|
|
34
|
+
self._create_toolbar()
|
|
35
|
+
self._create_menu()
|
|
36
|
+
|
|
37
|
+
def _setup_window(self):
|
|
38
|
+
config = getattr(self.controller, "config", {}) or {}
|
|
39
|
+
self.setWindowTitle(config.get("app_name", "Advisor-Scattering"))
|
|
40
|
+
window_size = config.get("window_size", {"width": 1200, "height": 800})
|
|
41
|
+
self.resize(window_size.get("width", 1200), window_size.get("height", 800))
|
|
42
|
+
|
|
43
|
+
def _build_layout(self):
|
|
44
|
+
container = QWidget(self)
|
|
45
|
+
self.setCentralWidget(container)
|
|
46
|
+
|
|
47
|
+
layout = QGridLayout(container)
|
|
48
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
49
|
+
layout.setSpacing(10)
|
|
50
|
+
|
|
51
|
+
self.stacked_widget = QStackedWidget()
|
|
52
|
+
layout.addWidget(self.stacked_widget, 0, 0)
|
|
53
|
+
|
|
54
|
+
self.tab_widget = QTabWidget()
|
|
55
|
+
self.tab_widget.setTabPosition(QTabWidget.West)
|
|
56
|
+
self.tab_widget.setIconSize(QSize(105, 80))
|
|
57
|
+
self.tab_widget.setMovable(True)
|
|
58
|
+
self.tab_widget.setDocumentMode(True)
|
|
59
|
+
self.stacked_widget.addWidget(self.tab_widget)
|
|
60
|
+
|
|
61
|
+
self.setStatusBar(QStatusBar())
|
|
62
|
+
self.statusBar().showMessage("Please initialize lattice parameters")
|
|
63
|
+
|
|
64
|
+
def _create_toolbar(self):
|
|
65
|
+
toolbar = QToolBar("Main Toolbar", self)
|
|
66
|
+
toolbar.setMovable(False)
|
|
67
|
+
toolbar.setIconSize(QSize(24, 24))
|
|
68
|
+
self.addToolBar(toolbar)
|
|
69
|
+
|
|
70
|
+
reset_action = QAction(self)
|
|
71
|
+
reset_action.setText("Reset Parameters")
|
|
72
|
+
reset_action.setToolTip("Return to initialization")
|
|
73
|
+
reset_icon = self._icon_path("reset.png")
|
|
74
|
+
if reset_icon and os.path.exists(reset_icon):
|
|
75
|
+
reset_action.setIcon(QIcon(reset_icon))
|
|
76
|
+
reset_action.triggered.connect(self.controller.reset_parameters)
|
|
77
|
+
toolbar.addAction(reset_action)
|
|
78
|
+
|
|
79
|
+
def _create_menu(self):
|
|
80
|
+
file_menu = self.menuBar().addMenu("&File")
|
|
81
|
+
|
|
82
|
+
open_action = QAction("&Open", self)
|
|
83
|
+
open_action.setShortcut("Ctrl+O")
|
|
84
|
+
open_action.triggered.connect(self._open_file)
|
|
85
|
+
file_menu.addAction(open_action)
|
|
86
|
+
|
|
87
|
+
save_action = QAction("&Save", self)
|
|
88
|
+
save_action.setShortcut("Ctrl+S")
|
|
89
|
+
save_action.triggered.connect(self._save_file)
|
|
90
|
+
file_menu.addAction(save_action)
|
|
91
|
+
|
|
92
|
+
file_menu.addSeparator()
|
|
93
|
+
reset_action = QAction("&Reset Parameters", self)
|
|
94
|
+
reset_action.setShortcut("Ctrl+R")
|
|
95
|
+
reset_action.triggered.connect(self.controller.reset_parameters)
|
|
96
|
+
file_menu.addAction(reset_action)
|
|
97
|
+
|
|
98
|
+
file_menu.addSeparator()
|
|
99
|
+
exit_action = QAction("E&xit", self)
|
|
100
|
+
exit_action.setShortcut("Ctrl+Q")
|
|
101
|
+
exit_action.triggered.connect(self.close)
|
|
102
|
+
file_menu.addAction(exit_action)
|
|
103
|
+
|
|
104
|
+
help_menu = self.menuBar().addMenu("&Help")
|
|
105
|
+
about_action = QAction("&About", self)
|
|
106
|
+
about_action.triggered.connect(self._show_about)
|
|
107
|
+
help_menu.addAction(about_action)
|
|
108
|
+
|
|
109
|
+
def attach_init_view(self, widget: QWidget):
|
|
110
|
+
"""Attach the initialization view as the first stacked page."""
|
|
111
|
+
self.init_view = widget
|
|
112
|
+
self.stacked_widget.insertWidget(0, widget)
|
|
113
|
+
self.stacked_widget.setCurrentWidget(widget)
|
|
114
|
+
|
|
115
|
+
def add_feature_tab(self, widget: QWidget, title: str, icon_name: Optional[str] = None, tooltip: str = ""):
|
|
116
|
+
"""Add a feature tab to the tab widget."""
|
|
117
|
+
icon_path = self._icon_path(icon_name) if icon_name else None
|
|
118
|
+
if icon_path and os.path.exists(icon_path):
|
|
119
|
+
self.tab_widget.addTab(widget, QIcon(icon_path), "")
|
|
120
|
+
else:
|
|
121
|
+
self.tab_widget.addTab(widget, title)
|
|
122
|
+
|
|
123
|
+
index = self.tab_widget.count() - 1
|
|
124
|
+
self.tab_widget.setTabToolTip(index, tooltip or title)
|
|
125
|
+
|
|
126
|
+
def show_tabs(self):
|
|
127
|
+
"""Switch to the main tab view."""
|
|
128
|
+
self.stacked_widget.setCurrentWidget(self.tab_widget)
|
|
129
|
+
self.statusBar().showMessage("Ready")
|
|
130
|
+
|
|
131
|
+
def show_init(self):
|
|
132
|
+
"""Switch back to initialization view."""
|
|
133
|
+
if self.init_view:
|
|
134
|
+
self.stacked_widget.setCurrentWidget(self.init_view)
|
|
135
|
+
self.statusBar().showMessage("Please initialize lattice parameters")
|
|
136
|
+
|
|
137
|
+
def _open_file(self):
|
|
138
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
139
|
+
self, "Open File", "", "All Files (*);;JSON Files (*.json);;CIF Files (*.cif)"
|
|
140
|
+
)
|
|
141
|
+
if not file_path:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
current_widget = self.tab_widget.currentWidget()
|
|
145
|
+
if hasattr(current_widget, "open_file"):
|
|
146
|
+
current_widget.open_file(file_path)
|
|
147
|
+
else:
|
|
148
|
+
self.statusBar().showMessage("Current tab does not support opening files")
|
|
149
|
+
|
|
150
|
+
def _save_file(self):
|
|
151
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
152
|
+
self, "Save File", "", "All Files (*);;JSON Files (*.json)"
|
|
153
|
+
)
|
|
154
|
+
if not file_path:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
current_widget = self.tab_widget.currentWidget()
|
|
158
|
+
if hasattr(current_widget, "save_file"):
|
|
159
|
+
current_widget.save_file(file_path)
|
|
160
|
+
else:
|
|
161
|
+
self.statusBar().showMessage("Current tab does not support saving files")
|
|
162
|
+
|
|
163
|
+
def _show_about(self):
|
|
164
|
+
QMessageBox.about(
|
|
165
|
+
self,
|
|
166
|
+
"About Advisor-Scattering",
|
|
167
|
+
"<b>Advisor-Scattering</b><p>A PyQt5-based application for X-ray scattering and diffraction preparation.</p>",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _icon_path(self, name: Optional[str]) -> Optional[str]:
|
|
171
|
+
if not name:
|
|
172
|
+
return None
|
|
173
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
174
|
+
return os.path.join(base_dir, "resources", "icons", name)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Base class for all tab implementations."""
|
|
4
|
+
|
|
5
|
+
from PyQt5.QtWidgets import QWidget, QGridLayout
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TabInterface(QWidget):
|
|
9
|
+
"""Base class for feature tabs."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, controller=None, main_window=None):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.controller = controller
|
|
14
|
+
self.main_window = main_window
|
|
15
|
+
|
|
16
|
+
self.layout = QGridLayout(self)
|
|
17
|
+
self.layout.setContentsMargins(10, 10, 10, 10)
|
|
18
|
+
self.layout.setSpacing(10)
|
|
19
|
+
|
|
20
|
+
self.init_ui()
|
|
21
|
+
|
|
22
|
+
def init_ui(self):
|
|
23
|
+
"""Initialize UI components."""
|
|
24
|
+
raise NotImplementedError("Subclasses must implement init_ui()")
|
|
25
|
+
|
|
26
|
+
def open_file(self, file_path: str):
|
|
27
|
+
"""Handle opening a file."""
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def save_file(self, file_path: str):
|
|
31
|
+
"""Handle saving to a file."""
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def clear(self):
|
|
35
|
+
"""Clear all inputs and results."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def get_state(self) -> dict:
|
|
39
|
+
"""Get the current state of the tab for session saving."""
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def set_state(self, state: dict):
|
|
43
|
+
"""Restore tab state from saved session."""
|
|
44
|
+
return False
|
advisor/ui/tips.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tooltip helper utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _tips_path() -> str:
|
|
8
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
return os.path.join(base_dir, "resources", "config", "tips.json")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Tips:
|
|
13
|
+
"""Load tooltip text from resources/config/tips.json."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.tips = {}
|
|
17
|
+
try:
|
|
18
|
+
with open(_tips_path(), "r", encoding="utf-8") as handle:
|
|
19
|
+
self.tips = json.load(handle)
|
|
20
|
+
except FileNotFoundError:
|
|
21
|
+
self.tips = {}
|
|
22
|
+
|
|
23
|
+
def tip(self, key):
|
|
24
|
+
return self.tips.get(key, "")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def set_tip(widget, tip):
|
|
28
|
+
"""Set the tooltip and status tip for a widget."""
|
|
29
|
+
widget.setToolTip(tip)
|
|
30
|
+
widget.setStatusTip(tip)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os, re
|
|
2
|
+
def readcif(filename=None, debug=False):
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Open a Crystallographic Information File (*.cif) file and store all entries in a key:value dictionary
|
|
6
|
+
Looped values are stored as lists under a single key entry
|
|
7
|
+
All values are stored as strings
|
|
8
|
+
E.G.
|
|
9
|
+
crys=readcif('somefile.cif')
|
|
10
|
+
crys['_cell_length_a'] = '2.835(2)'
|
|
11
|
+
|
|
12
|
+
crys[key] = value
|
|
13
|
+
available keys are give by crys.keys()
|
|
14
|
+
|
|
15
|
+
To debug the file with outputted messages, use:
|
|
16
|
+
cif = readcif(file, debug=True)
|
|
17
|
+
|
|
18
|
+
Some useful standard CIF keywords:
|
|
19
|
+
_cell_length_a
|
|
20
|
+
_cell_length_b
|
|
21
|
+
_cell_length_c
|
|
22
|
+
_cell_angle_alpha
|
|
23
|
+
_cell_angle_beta
|
|
24
|
+
_cell_angle_gamma
|
|
25
|
+
_space_group_symop_operation_xyz
|
|
26
|
+
_atom_site_label
|
|
27
|
+
_atom_site_type_symbol
|
|
28
|
+
_atom_site_occupancy
|
|
29
|
+
_atom_site_U_iso_or_equiv
|
|
30
|
+
_atom_site_fract_x
|
|
31
|
+
_atom_site_fract_y
|
|
32
|
+
_atom_site_fract_z
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Get file name
|
|
36
|
+
filename = os.path.abspath(os.path.expanduser(filename))
|
|
37
|
+
(dirName, filetitle) = os.path.split(filename)
|
|
38
|
+
(fname, Ext) = os.path.splitext(filetitle)
|
|
39
|
+
|
|
40
|
+
# Open file
|
|
41
|
+
file = open(filename)
|
|
42
|
+
text = file.read()
|
|
43
|
+
file.close()
|
|
44
|
+
|
|
45
|
+
# Remove blank lines
|
|
46
|
+
while "\n\n" in text:
|
|
47
|
+
text = text.replace("\n\n", "\n")
|
|
48
|
+
lines = text.splitlines()
|
|
49
|
+
|
|
50
|
+
cifvals = {'Filename': filename, 'Directory': dirName, 'FileTitle': fname}
|
|
51
|
+
|
|
52
|
+
# Read file line by line, converting the cif file values to a python dict
|
|
53
|
+
n = 0
|
|
54
|
+
while n < len(lines):
|
|
55
|
+
# Convert line to columns
|
|
56
|
+
vals = lines[n].strip().split()
|
|
57
|
+
|
|
58
|
+
# skip empty lines
|
|
59
|
+
if len(vals) == 0:
|
|
60
|
+
n += 1
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Search for stored value lines
|
|
64
|
+
if vals[0][0] == '_':
|
|
65
|
+
if len(vals) == 1:
|
|
66
|
+
# Record next lines that are not keys as string
|
|
67
|
+
if lines[n + 1][0] == ';': n += 1
|
|
68
|
+
strarg = []
|
|
69
|
+
while n + 1 < len(lines) and (len(lines[n + 1]) == 0 or lines[n + 1][0].strip() not in ['_', ';']):
|
|
70
|
+
strarg += [lines[n + 1].strip('\'"')]
|
|
71
|
+
n += 1
|
|
72
|
+
cifvals[vals[0]] = '\n'.join(strarg)
|
|
73
|
+
chk = 'a'
|
|
74
|
+
else:
|
|
75
|
+
cifvals[vals[0]] = ' '.join(vals[1:]).strip(' \'"\n')
|
|
76
|
+
chk = 'b'
|
|
77
|
+
n += 1
|
|
78
|
+
if debug:
|
|
79
|
+
print('%5d %s %s = %s' % (n, chk, vals[0], cifvals[vals[0]]))
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Search for loops
|
|
83
|
+
elif vals[0] == 'loop_':
|
|
84
|
+
n += 1
|
|
85
|
+
loopvals = []
|
|
86
|
+
# Step 1: Assign loop columns
|
|
87
|
+
# looped columns are given by "_column_name"
|
|
88
|
+
while n < len(lines) and len(lines[n].strip()) > 0 and lines[n].strip()[0] == '_':
|
|
89
|
+
loopvals += [lines[n].split()[0]]
|
|
90
|
+
cifvals[loopvals[-1]] = []
|
|
91
|
+
n += 1
|
|
92
|
+
|
|
93
|
+
# Step 2: Assign data to columns
|
|
94
|
+
# loops until line has less segments than columns
|
|
95
|
+
while n < len(lines):
|
|
96
|
+
# cols = lines[n].split()
|
|
97
|
+
# this fixes error on symmetry arguments having spaces
|
|
98
|
+
# this will only work if the last argument in the loop is split by spaces (in quotes)
|
|
99
|
+
# cols = cols[:len(loopvals) - 1] + [''.join(cols[len(loopvals) - 1:])]
|
|
100
|
+
cols = [col for col in re.split("( |\\\".*?\\\"|'.*?')", lines[n]) if col.strip()]
|
|
101
|
+
if len(cols) != len(loopvals): break
|
|
102
|
+
if cols[0][0] == '_' or cols[0] == 'loop_': break # catches error if loop is only 1 iteration
|
|
103
|
+
if cols[0][0] == '#': n += 1; continue # catches comented out lines
|
|
104
|
+
if len(loopvals) == 1:
|
|
105
|
+
cifvals[loopvals[0]] += [lines[n].strip(' \"\'\n')]
|
|
106
|
+
else:
|
|
107
|
+
for c, ll in enumerate(loopvals):
|
|
108
|
+
cifvals[ll] += [cols[c]]
|
|
109
|
+
n += 1
|
|
110
|
+
|
|
111
|
+
if debug:
|
|
112
|
+
for ll in loopvals:
|
|
113
|
+
print('%5d L %s = %s' % (n, ll, str(cifvals[ll])))
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
# Skip anything else
|
|
118
|
+
if debug:
|
|
119
|
+
print('%5d SKIPPED: %s' % (n, lines[n]))
|
|
120
|
+
n += 1
|
|
121
|
+
|
|
122
|
+
# Replace '.' in keys - fix bug from isodistort cif files
|
|
123
|
+
# e.g. '_space_group_symop_magn_operation.xyz'
|
|
124
|
+
current_keys = list(cifvals.keys())
|
|
125
|
+
for key in current_keys:
|
|
126
|
+
if '.' in key:
|
|
127
|
+
newkey = key.replace('.', '_')
|
|
128
|
+
cifvals[newkey] = cifvals[key]
|
|
129
|
+
return cifvals
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
2
|
+
from matplotlib.figure import Figure
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HKLScan2DVisualizer(FigureCanvas):
|
|
7
|
+
"""2D visualizer for HKL scan results with structure factor display and trajectory line."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, width: float = 5.0, height: float = 4.0, dpi: int = 100, parent=None):
|
|
10
|
+
self.fig = Figure(figsize=(float(width), float(height)), dpi=int(dpi), tight_layout=True)
|
|
11
|
+
super().__init__(self.fig)
|
|
12
|
+
self.axes = self.fig.add_subplot(111)
|
|
13
|
+
self._colorbar = None
|
|
14
|
+
self._initialized = True
|
|
15
|
+
|
|
16
|
+
# Default range settings: H,K in (-1,1), L in (-2,2)
|
|
17
|
+
self.default_h_range = (-1.0, 1.0)
|
|
18
|
+
self.default_k_range = (-1.0, 1.0)
|
|
19
|
+
self.default_l_range = (-2.0, 2.0)
|
|
20
|
+
|
|
21
|
+
# Current range settings
|
|
22
|
+
self.h_range = self.default_h_range
|
|
23
|
+
self.k_range = self.default_k_range
|
|
24
|
+
self.l_range = self.default_l_range
|
|
25
|
+
|
|
26
|
+
# Store last scan results for trajectory
|
|
27
|
+
self.last_scan_results = None
|
|
28
|
+
|
|
29
|
+
def _auto_detect_ranges(self, scan_results):
|
|
30
|
+
"""Auto-detect HKL ranges based on scan results.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
scan_results: Dictionary containing H, K, L arrays from scan
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
tuple: (h_range, k_range, l_range) where each range is (min, max)
|
|
37
|
+
"""
|
|
38
|
+
import math
|
|
39
|
+
epsilon = 1e-6
|
|
40
|
+
try:
|
|
41
|
+
# Extract HKL values from scan results
|
|
42
|
+
h_vals = np.array(scan_results.get("H", []), dtype=np.float64)
|
|
43
|
+
k_vals = np.array(scan_results.get("K", []), dtype=np.float64)
|
|
44
|
+
l_vals = np.array(scan_results.get("L", []), dtype=np.float64)
|
|
45
|
+
|
|
46
|
+
# Find max absolute values and round up to ceiling
|
|
47
|
+
if len(h_vals) > 0:
|
|
48
|
+
h_max = math.ceil(np.max(np.abs(h_vals)) + epsilon)
|
|
49
|
+
h_range = (-h_max, h_max)
|
|
50
|
+
else:
|
|
51
|
+
h_range = self.default_h_range
|
|
52
|
+
|
|
53
|
+
if len(k_vals) > 0:
|
|
54
|
+
k_max = math.ceil(np.max(np.abs(k_vals)) + epsilon)
|
|
55
|
+
k_range = (-k_max, k_max)
|
|
56
|
+
else:
|
|
57
|
+
k_range = self.default_k_range
|
|
58
|
+
|
|
59
|
+
if len(l_vals) > 0:
|
|
60
|
+
l_max = math.ceil(np.max(np.abs(l_vals)) + epsilon)
|
|
61
|
+
l_range = (-l_max, l_max)
|
|
62
|
+
else:
|
|
63
|
+
l_range = self.default_l_range
|
|
64
|
+
|
|
65
|
+
return h_range, k_range, l_range
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"Error auto-detecting ranges: {e}")
|
|
69
|
+
return self.default_h_range, self.default_k_range, self.default_l_range
|
|
70
|
+
|
|
71
|
+
def set_ranges(self, h_range=None, k_range=None, l_range=None):
|
|
72
|
+
"""Set the plotting ranges for H, K, L indices.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
h_range: tuple (min, max) for H range, or None to keep current
|
|
76
|
+
k_range: tuple (min, max) for K range, or None to keep current
|
|
77
|
+
l_range: tuple (min, max) for L range, or None to keep current
|
|
78
|
+
"""
|
|
79
|
+
if h_range is not None:
|
|
80
|
+
self.h_range = h_range
|
|
81
|
+
if k_range is not None:
|
|
82
|
+
self.k_range = k_range
|
|
83
|
+
if l_range is not None:
|
|
84
|
+
self.l_range = l_range
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def visualize_results(self, scan_results, plane_type="HK"):
|
|
88
|
+
"""Visualize HKL scan results with structure factors and trajectory line.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
scan_results: Dictionary containing scan results from BrillouinCalculator
|
|
92
|
+
plane_type: "HK", "HL", or "KL" - which plane to visualize
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
if not scan_results or not scan_results.get("success", False):
|
|
96
|
+
self.clear_plot()
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# Store scan results for trajectory
|
|
100
|
+
self.last_scan_results = scan_results
|
|
101
|
+
|
|
102
|
+
# Auto-detect ranges based on scan results
|
|
103
|
+
h_range, k_range, l_range = self._auto_detect_ranges(scan_results)
|
|
104
|
+
self.set_ranges(h_range, k_range, l_range)
|
|
105
|
+
|
|
106
|
+
# Extract deactivated index from scan results
|
|
107
|
+
deactivated_index = scan_results.get("deactivated_index", None)
|
|
108
|
+
|
|
109
|
+
# Map plane type to match deactivated index
|
|
110
|
+
if deactivated_index == "L":
|
|
111
|
+
plane_type = "HK" # L deactivated means HK plane
|
|
112
|
+
x_label = "H"
|
|
113
|
+
y_label = "K"
|
|
114
|
+
fixed_label = "L"
|
|
115
|
+
elif deactivated_index == "K":
|
|
116
|
+
plane_type = "HL" # K deactivated means HL plane
|
|
117
|
+
x_label = "H"
|
|
118
|
+
y_label = "L"
|
|
119
|
+
fixed_label = "K"
|
|
120
|
+
elif deactivated_index == "H":
|
|
121
|
+
plane_type = "KL" # H deactivated means KL plane
|
|
122
|
+
x_label = "K"
|
|
123
|
+
y_label = "L"
|
|
124
|
+
fixed_label = "H"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Plot only the trajectory points as scatter
|
|
128
|
+
success = self._plot_trajectory_only(scan_results, plane_type, x_label, y_label)
|
|
129
|
+
|
|
130
|
+
return success
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Error in visualize_results: {e}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _plot_trajectory_only(self, scan_results, plane_type, x_label, y_label):
|
|
137
|
+
"""Plot only the trajectory points as scatter without structure factor background.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
scan_results: Dictionary containing H, K, L arrays from scan
|
|
141
|
+
plane_type: "HK", "HL", or "KL"
|
|
142
|
+
x_label: X axis label ("H", "K", or "L")
|
|
143
|
+
y_label: Y axis label ("H", "K", or "L")
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
# Extract HKL coordinates from scan results
|
|
147
|
+
h_vals = np.array(scan_results.get("H", []), dtype=np.float64)
|
|
148
|
+
k_vals = np.array(scan_results.get("K", []), dtype=np.float64)
|
|
149
|
+
l_vals = np.array(scan_results.get("L", []), dtype=np.float64)
|
|
150
|
+
|
|
151
|
+
if len(h_vals) == 0:
|
|
152
|
+
self.clear_plot()
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
# Get the appropriate coordinates for the plane
|
|
156
|
+
if plane_type == "HK":
|
|
157
|
+
x_traj = h_vals
|
|
158
|
+
y_traj = k_vals
|
|
159
|
+
elif plane_type == "HL":
|
|
160
|
+
x_traj = h_vals
|
|
161
|
+
y_traj = l_vals
|
|
162
|
+
elif plane_type == "KL":
|
|
163
|
+
x_traj = k_vals
|
|
164
|
+
y_traj = l_vals
|
|
165
|
+
else:
|
|
166
|
+
self.clear_plot()
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
# Reset axes
|
|
170
|
+
if self._colorbar is not None:
|
|
171
|
+
self._colorbar.remove()
|
|
172
|
+
self._colorbar = None
|
|
173
|
+
self.fig.clear()
|
|
174
|
+
ax = self.fig.add_subplot(111)
|
|
175
|
+
self.axes = ax
|
|
176
|
+
|
|
177
|
+
# Plot trajectory as scatter points
|
|
178
|
+
# Use different colors for start, middle, and end points
|
|
179
|
+
if len(x_traj) > 0:
|
|
180
|
+
# Plot all points in blue
|
|
181
|
+
scatter = ax.scatter(x_traj, y_traj, c='dodgerblue', s=15, alpha=0.7,
|
|
182
|
+
label='Scan points', zorder=3)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
ax.set_xlabel(f"{x_label} (r.l.u.)")
|
|
188
|
+
ax.set_ylabel(f"{y_label} (r.l.u.)")
|
|
189
|
+
ax.set_title(f"{x_label}{y_label} plane")
|
|
190
|
+
|
|
191
|
+
# Set limits based on auto-detected ranges for the current plane
|
|
192
|
+
if plane_type == "HK":
|
|
193
|
+
x_min, x_max = self.h_range[0], self.h_range[1]
|
|
194
|
+
y_min, y_max = self.k_range[0], self.k_range[1]
|
|
195
|
+
elif plane_type == "HL":
|
|
196
|
+
x_min, x_max = self.h_range[0], self.h_range[1]
|
|
197
|
+
y_min, y_max = self.l_range[0], self.l_range[1]
|
|
198
|
+
elif plane_type == "KL":
|
|
199
|
+
x_min, x_max = self.k_range[0], self.k_range[1]
|
|
200
|
+
y_min, y_max = self.l_range[0], self.l_range[1]
|
|
201
|
+
else:
|
|
202
|
+
# Fallback to trajectory data range
|
|
203
|
+
x_min, x_max = x_traj.min(), x_traj.max()
|
|
204
|
+
y_min, y_max = y_traj.min(), y_traj.max()
|
|
205
|
+
|
|
206
|
+
ax.set_xlim(x_min, x_max)
|
|
207
|
+
ax.set_ylim(y_min, y_max)
|
|
208
|
+
|
|
209
|
+
# Add grid and legend
|
|
210
|
+
ax.grid(True, alpha=0.3)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
from matplotlib.ticker import MaxNLocator
|
|
214
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
215
|
+
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
self.draw()
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"Error in _plot_trajectory_only: {e}")
|
|
224
|
+
return False
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .coordinate_visualizer import CoordinateVisualizer
|
|
2
|
+
from .unitcell_visualizer import UnitcellVisualizer
|
|
3
|
+
from .scattering_visualizer import ScatteringVisualizer
|
|
4
|
+
from .structure_factor_visualizer import StructureFactorVisualizer3D
|
|
5
|
+
from .structure_factor_visualizer_2d import StructureFactorVisualizer2D
|
|
6
|
+
from .HKLScan2DVisualizer import HKLScan2DVisualizer
|
|
7
|
+
|
|
8
|
+
__all__ = ["CoordinateVisualizer", "UnitcellVisualizer", "ScatteringVisualizer", "StructureFactorVisualizer3D", "StructureFactorVisualizer2D", "HKLScan2DVisualizer"]
|