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,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,6 @@
1
+ """Utility helpers for UI."""
2
+
3
+ from .readcif import readcif
4
+
5
+ __all__ = ["readcif"]
6
+
@@ -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"]