bec-widgets 1.25.1__py3-none-any.whl → 2.0.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 (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +631 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +186 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1563 +0,0 @@
1
- # pylint: disable=too_many_lines
2
- from __future__ import annotations
3
-
4
- from collections import defaultdict
5
- from typing import Any, Literal, Optional
6
-
7
- import numpy as np
8
- import pyqtgraph as pg
9
- from bec_lib import messages
10
- from bec_lib.device import ReadoutPriority
11
- from bec_lib.endpoints import MessageEndpoints
12
- from bec_lib.logger import bec_logger
13
- from pydantic import Field, ValidationError, field_validator
14
- from pyqtgraph.exporters import MatplotlibExporter
15
- from qtpy.QtCore import Signal as pyqtSignal
16
- from qtpy.QtWidgets import QWidget
17
-
18
- from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
19
- from bec_widgets.utils import Colors, EntryValidator
20
- from bec_widgets.utils.colors import get_accent_colors
21
- from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
22
- from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
23
- from bec_widgets.widgets.containers.figure.plots.waveform.waveform_curve import (
24
- BECCurve,
25
- CurveConfig,
26
- Signal,
27
- SignalData,
28
- )
29
-
30
- logger = bec_logger.logger
31
-
32
-
33
- class Waveform1DConfig(SubplotConfig):
34
- color_palette: Optional[str] = Field(
35
- "magma", description="The color palette of the figure widget.", validate_default=True
36
- )
37
- curves: dict[str, CurveConfig] = Field(
38
- {}, description="The list of curves to be added to the 1D waveform widget."
39
- )
40
-
41
- model_config: dict = {"validate_assignment": True}
42
- _validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map)
43
-
44
-
45
- class BECWaveform(BECPlotBase):
46
- READOUT_PRIORITY_HANDLER = {
47
- ReadoutPriority.ON_REQUEST: "on_request",
48
- ReadoutPriority.BASELINE: "baseline",
49
- ReadoutPriority.MONITORED: "monitored",
50
- ReadoutPriority.ASYNC: "async",
51
- ReadoutPriority.CONTINUOUS: "continuous",
52
- }
53
- USER_ACCESS = [
54
- "_rpc_id",
55
- "_config_dict",
56
- "plot",
57
- "add_dap",
58
- "get_dap_params",
59
- "set_x",
60
- "remove_curve",
61
- "scan_history",
62
- "curves",
63
- "get_curve",
64
- "get_all_data",
65
- "set",
66
- "set_title",
67
- "set_x_label",
68
- "set_y_label",
69
- "set_x_scale",
70
- "set_y_scale",
71
- "set_x_lim",
72
- "set_y_lim",
73
- "set_grid",
74
- "set_colormap",
75
- "enable_scatter",
76
- "enable_fps_monitor",
77
- "lock_aspect_ratio",
78
- "export",
79
- "remove",
80
- "clear_all",
81
- "set_legend_label_size",
82
- "toggle_roi",
83
- "select_roi",
84
- ]
85
- scan_signal_update = pyqtSignal()
86
- async_signal_update = pyqtSignal()
87
- dap_params_update = pyqtSignal(dict, dict)
88
- dap_summary_update = pyqtSignal(dict, dict)
89
- autorange_signal = pyqtSignal()
90
- new_scan = pyqtSignal()
91
- roi_changed = pyqtSignal(tuple)
92
- roi_active = pyqtSignal(bool)
93
- request_dap_refresh = pyqtSignal()
94
-
95
- def __init__(
96
- self,
97
- parent: Optional[QWidget] = None,
98
- parent_figure=None,
99
- config: Optional[Waveform1DConfig] = None,
100
- client=None,
101
- gui_id: Optional[str] = None,
102
- **kwargs,
103
- ):
104
- if config is None:
105
- config = Waveform1DConfig(widget_class=self.__class__.__name__)
106
- super().__init__(
107
- parent=parent,
108
- parent_figure=parent_figure,
109
- config=config,
110
- client=client,
111
- gui_id=gui_id,
112
- **kwargs,
113
- )
114
-
115
- self._curves_data = defaultdict(dict)
116
- self.old_scan_id = None
117
- self.scan_id = None
118
- self.scan_item = None
119
- self._roi_region = None
120
- self.roi_select = None
121
- self._accent_colors = get_accent_colors()
122
- self._x_axis_mode = {
123
- "name": None,
124
- "entry": None,
125
- "readout_priority": None,
126
- "label_suffix": "",
127
- }
128
-
129
- self._slice_index = None
130
-
131
- # Scan segment update proxy
132
- self.proxy_update_plot = pg.SignalProxy(
133
- self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves
134
- )
135
- self.proxy_update_dap = pg.SignalProxy(
136
- self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
137
- )
138
- self.async_signal_update.connect(self.replot_async_curve)
139
- self.autorange_signal.connect(self.auto_range)
140
-
141
- # Get bec shortcuts dev, scans, queue, scan_storage, dap
142
- self.get_bec_shortcuts()
143
-
144
- # Connect dispatcher signals
145
- self.bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
146
- # TODO disabled -> scan_status is SET_AND_PUBLISH -> do not work in combination with autoupdate from CLI
147
- # self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status())
148
-
149
- self.entry_validator = EntryValidator(self.dev)
150
-
151
- self.add_legend()
152
- self.apply_config(self.config)
153
-
154
- @Slot(bool)
155
- def toggle_roi(self, toggled: bool) -> None:
156
- """Toggle the linear region selector on the plot.
157
-
158
- Args:
159
- toggled(bool): If True, enable the linear region selector.
160
- """
161
- if toggled:
162
- return self._hook_roi()
163
- return self._unhook_roi()
164
-
165
- @Slot(tuple)
166
- def select_roi(self, region: tuple[float, float]):
167
- """Set the fit region of the plot widget. At the moment only a single region is supported.
168
- To remove the roi region again, use toggle_roi_region
169
-
170
- Args:
171
- region(tuple[float, float]): The fit region.
172
- """
173
- if self.roi_region == (None, None):
174
- self.toggle_roi(True)
175
- try:
176
- self.roi_select.linear_region_selector.setRegion(region)
177
- except Exception as e:
178
- logger.error(f"Error setting region {tuple}; Exception raised: {e}")
179
- raise ValueError(f"Error setting region {tuple}; Exception raised: {e}") from e
180
-
181
- def _hook_roi(self):
182
- """Hook the linear region selector to the plot."""
183
- color = self._accent_colors.default
184
- color.setAlpha(int(0.2 * 255))
185
- hover_color = self._accent_colors.default
186
- hover_color.setAlpha(int(0.35 * 255))
187
- if self.roi_select is None:
188
- self.roi_select = LinearRegionWrapper(
189
- self.plot_item, color=color, hover_color=hover_color, parent=self
190
- )
191
- self.roi_select.add_region_selector()
192
- self.roi_select.region_changed.connect(self.roi_changed)
193
- self.roi_select.region_changed.connect(self.set_roi_region)
194
- self.request_dap_refresh.connect(self.refresh_dap)
195
- self._emit_roi_region()
196
- self.roi_active.emit(True)
197
-
198
- def _unhook_roi(self):
199
- """Unhook the linear region selector from the plot."""
200
- if self.roi_select is not None:
201
- self.roi_select.region_changed.disconnect(self.roi_changed)
202
- self.roi_select.region_changed.disconnect(self.set_roi_region)
203
- self.request_dap_refresh.disconnect(self.refresh_dap)
204
- self.roi_active.emit(False)
205
- self.roi_region = None
206
- self.refresh_dap()
207
- self.roi_select.cleanup()
208
- self.roi_select.deleteLater()
209
- self.roi_select = None
210
-
211
- def apply_config(self, config: dict | SubplotConfig, replot_last_scan: bool = False):
212
- """
213
- Apply the configuration to the 1D waveform widget.
214
-
215
- Args:
216
- config(dict|SubplotConfig): Configuration settings.
217
- replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
218
- """
219
- if isinstance(config, dict):
220
- try:
221
- config = Waveform1DConfig(**config)
222
- except ValidationError as e:
223
- logger.error(f"Validation error when applying config to BECWaveform1D: {e}")
224
- return
225
-
226
- self.config = config
227
- self.plot_item.clear() # TODO not sure if on the plot or layout level
228
-
229
- self.apply_axis_config()
230
- # Reset curves
231
- self._curves_data = defaultdict(dict)
232
- self._curves = self.plot_item.curves
233
- for curve_config in self.config.curves.values():
234
- self.add_curve_by_config(curve_config)
235
- if replot_last_scan:
236
- self.scan_history(scan_index=-1)
237
-
238
- def change_gui_id(self, new_gui_id: str):
239
- """
240
- Change the GUI ID of the waveform widget and update the parent_id in all associated curves.
241
-
242
- Args:
243
- new_gui_id (str): The new GUI ID to be set for the waveform widget.
244
- """
245
- # Update the gui_id in the waveform widget itself
246
- self.gui_id = new_gui_id
247
- self.config.gui_id = new_gui_id
248
-
249
- for curve in self.curves:
250
- curve.config.parent_id = new_gui_id
251
-
252
- ###################################
253
- # Fit Range Properties
254
- ###################################
255
-
256
- @property
257
- def roi_region(self) -> tuple[float, float] | None:
258
- """
259
- Get the fit region of the plot widget.
260
-
261
- Returns:
262
- tuple: The fit region.
263
- """
264
- if self._roi_region is not None:
265
- return self._roi_region
266
- return None, None
267
-
268
- @roi_region.setter
269
- def roi_region(self, value: tuple[float, float] | None):
270
- """Set the fit region of the plot widget.
271
-
272
- Args:
273
- value(tuple[float, float]|None): The fit region.
274
- """
275
- self._roi_region = value
276
- if value is not None:
277
- self.request_dap_refresh.emit()
278
-
279
- @Slot(tuple)
280
- def set_roi_region(self, region: tuple[float, float]):
281
- """
282
- Set the fit region of the plot widget.
283
-
284
- Args:
285
- region(tuple[float, float]): The fit region.
286
- """
287
- self.roi_region = region
288
-
289
- def _emit_roi_region(self):
290
- """Emit the current ROI from selector the plot widget."""
291
- if self.roi_select is not None:
292
- self.set_roi_region(self.roi_select.linear_region_selector.getRegion())
293
-
294
- ###################################
295
- # Waveform Properties
296
- ###################################
297
-
298
- @property
299
- def curves(self) -> list[BECCurve]:
300
- """
301
- Get the curves of the plot widget as a list
302
- Returns:
303
- list: List of curves.
304
- """
305
- return self._curves
306
-
307
- @curves.setter
308
- def curves(self, value: list[BECCurve]):
309
- self._curves = value
310
-
311
- @property
312
- def x_axis_mode(self) -> dict:
313
- """
314
- Get the x axis mode of the plot widget.
315
-
316
- Returns:
317
- dict: The x axis mode.
318
- """
319
- return self._x_axis_mode
320
-
321
- @x_axis_mode.setter
322
- def x_axis_mode(self, value: dict):
323
- self._x_axis_mode = value
324
-
325
- ###################################
326
- # Adding and Removing Curves
327
- ###################################
328
-
329
- def add_curve_by_config(self, curve_config: CurveConfig | dict) -> BECCurve:
330
- """
331
- Add a curve to the plot widget by its configuration.
332
-
333
- Args:
334
- curve_config(CurveConfig|dict): Configuration of the curve to be added.
335
-
336
- Returns:
337
- BECCurve: The curve object.
338
- """
339
- if isinstance(curve_config, dict):
340
- curve_config = CurveConfig(**curve_config)
341
- curve = self._add_curve_object(
342
- name=curve_config.label, source=curve_config.source, config=curve_config
343
- )
344
- return curve
345
-
346
- def get_curve_config(self, curve_id: str, dict_output: bool = True) -> CurveConfig | dict:
347
- """
348
- Get the configuration of a curve by its ID.
349
-
350
- Args:
351
- curve_id(str): ID of the curve.
352
-
353
- Returns:
354
- CurveConfig|dict: Configuration of the curve.
355
- """
356
- for curves in self._curves_data.values():
357
- if curve_id in curves:
358
- if dict_output:
359
- return curves[curve_id].config.model_dump()
360
- else:
361
- return curves[curve_id].config
362
-
363
- def get_curve(self, identifier) -> BECCurve:
364
- """
365
- Get the curve by its index or ID.
366
-
367
- Args:
368
- identifier(int|str): Identifier of the curve. Can be either an integer (index) or a string (curve_id).
369
-
370
- Returns:
371
- BECCurve: The curve object.
372
- """
373
- if isinstance(identifier, int):
374
- return self.plot_item.curves[identifier]
375
- elif isinstance(identifier, str):
376
- for curves in self._curves_data.values():
377
- if identifier in curves:
378
- return curves[identifier]
379
- raise ValueError(f"Curve with ID '{identifier}' not found.")
380
- else:
381
- raise ValueError("Identifier must be either an integer (index) or a string (curve_id).")
382
-
383
- def enable_scatter(self, enable: bool):
384
- """
385
- Enable/Disable scatter plot on all curves.
386
-
387
- Args:
388
- enable(bool): If True, enable scatter markers; if False, disable them.
389
- """
390
- for curve in self.curves:
391
- if isinstance(curve, BECCurve):
392
- if enable:
393
- curve.set_symbol("o") # You can choose any symbol you like
394
- else:
395
- curve.set_symbol(None)
396
-
397
- def plot(
398
- self,
399
- arg1: list | np.ndarray | str | None = None,
400
- y: list | np.ndarray | None = None,
401
- x: list | np.ndarray | None = None,
402
- x_name: str | None = None,
403
- y_name: str | None = None,
404
- z_name: str | None = None,
405
- x_entry: str | None = None,
406
- y_entry: str | None = None,
407
- z_entry: str | None = None,
408
- color: str | None = None,
409
- color_map_z: str | None = "magma",
410
- label: str | None = None,
411
- validate: bool = True,
412
- dap: str | None = None, # TODO add dap custom curve wrapper
413
- **kwargs,
414
- ) -> BECCurve:
415
- """
416
- Plot a curve to the plot widget.
417
-
418
- Args:
419
- arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
420
- y(list | np.ndarray): Custom y data to plot.
421
- x(list | np.ndarray): Custom y data to plot.
422
- x_name(str): Name of the x signal.
423
- - "best_effort": Use the best effort signal.
424
- - "timestamp": Use the timestamp signal.
425
- - "index": Use the index signal.
426
- - Custom signal name of device from BEC.
427
- y_name(str): The name of the device for the y-axis.
428
- z_name(str): The name of the device for the z-axis.
429
- x_entry(str): The name of the entry for the x-axis.
430
- y_entry(str): The name of the entry for the y-axis.
431
- z_entry(str): The name of the entry for the z-axis.
432
- color(str): The color of the curve.
433
- color_map_z(str): The color map to use for the z-axis.
434
- label(str): The label of the curve.
435
- validate(bool): If True, validate the device names and entries.
436
- dap(str): The dap model to use for the curve, only available for sync devices. If not specified, none will be added.
437
-
438
- Returns:
439
- BECCurve: The curve object.
440
- """
441
- if x is not None and y is not None:
442
- return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
443
-
444
- if isinstance(arg1, str):
445
- y_name = arg1
446
- elif isinstance(arg1, list):
447
- if isinstance(y, list):
448
- return self.add_curve_custom(x=arg1, y=y, label=label, color=color, **kwargs)
449
- if y is None:
450
- x = np.arange(len(arg1))
451
- return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs)
452
- elif isinstance(arg1, np.ndarray) and y is None:
453
- if arg1.ndim == 1:
454
- x = np.arange(arg1.size)
455
- return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs)
456
- if arg1.ndim == 2:
457
- x = arg1[:, 0]
458
- y = arg1[:, 1]
459
- return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
460
- if y_name is None:
461
- raise ValueError("y_name must be provided.")
462
- if dap:
463
- self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
464
- curve = self.add_curve_bec(
465
- x_name=x_name,
466
- y_name=y_name,
467
- z_name=z_name,
468
- x_entry=x_entry,
469
- y_entry=y_entry,
470
- z_entry=z_entry,
471
- color=color,
472
- color_map_z=color_map_z,
473
- label=label,
474
- validate_bec=validate,
475
- **kwargs,
476
- )
477
- self.scan_signal_update.emit()
478
- self.async_signal_update.emit()
479
-
480
- return curve
481
-
482
- def set_x(self, x_name: str, x_entry: str | None = None):
483
- """
484
- Change the x axis of the plot widget.
485
-
486
- Args:
487
- x_name(str): Name of the x signal.
488
- - "best_effort": Use the best effort signal.
489
- - "timestamp": Use the timestamp signal.
490
- - "index": Use the index signal.
491
- - Custom signal name of device from BEC.
492
- x_entry(str): Entry of the x signal.
493
- """
494
- if not x_name:
495
- # this can happen, if executed by a signal from a widget
496
- return
497
-
498
- curve_configs = self.config.curves
499
- curve_ids = list(curve_configs.keys())
500
- curve_configs = list(curve_configs.values())
501
- self.set_auto_range(True, "xy")
502
-
503
- x_entry, _, _ = self._validate_signal_entries(
504
- x_name, None, None, x_entry, None, None, validate_bec=True
505
- )
506
-
507
- readout_priority_x = None
508
- if x_name not in ["best_effort", "timestamp", "index"]:
509
- readout_priority_x = self._get_device_readout_priority(x_name)
510
-
511
- self.x_axis_mode = {
512
- "name": x_name,
513
- "entry": x_entry,
514
- "readout_priority": readout_priority_x,
515
- }
516
-
517
- if len(self.curves) > 0:
518
- # validate all curves
519
- for curve in self.curves:
520
- if not isinstance(curve, BECCurve):
521
- continue
522
- if curve.config.source == "custom":
523
- continue
524
- self._validate_x_axis_behaviour(curve.config.signals.y.name, x_name, x_entry, False)
525
- self._switch_x_axis_item(
526
- f"{x_name}-{x_entry}"
527
- if x_name not in ["best_effort", "timestamp", "index"]
528
- else x_name
529
- )
530
- for curve_id, curve_config in zip(curve_ids, curve_configs):
531
- if curve_config.signals is None:
532
- continue
533
- if curve_config.signals.x is None:
534
- continue
535
- curve_config.signals.x.name = x_name
536
- curve_config.signals.x.entry = x_entry
537
- self.remove_curve(curve_id)
538
- self.add_curve_by_config(curve_config)
539
-
540
- self.async_signal_update.emit()
541
- self.scan_signal_update.emit()
542
-
543
- @Slot()
544
- def auto_range(self):
545
- """Manually set auto range of the plotitem"""
546
- self.plot_item.autoRange()
547
-
548
- def set_auto_range(self, enabled: bool, axis: str = "xy"):
549
- """
550
- Set the auto range of the plot widget.
551
-
552
- Args:
553
- enabled(bool): If True, enable the auto range.
554
- axis(str, optional): The axis to enable the auto range.
555
- - "xy": Enable auto range for both x and y axis.
556
- - "x": Enable auto range for x axis.
557
- - "y": Enable auto range for y axis.
558
- """
559
- self.plot_item.enableAutoRange(axis, enabled)
560
-
561
- def add_curve_custom(
562
- self,
563
- x: list | np.ndarray,
564
- y: list | np.ndarray,
565
- label: str = None,
566
- color: str = None,
567
- curve_source: str = "custom",
568
- **kwargs,
569
- ) -> BECCurve:
570
- """
571
- Add a custom data curve to the plot widget.
572
-
573
- Args:
574
- x(list|np.ndarray): X data of the curve.
575
- y(list|np.ndarray): Y data of the curve.
576
- label(str, optional): Label of the curve. Defaults to None.
577
- color(str, optional): Color of the curve. Defaults to None.
578
- curve_source(str, optional): Tag for source of the curve. Defaults to "custom".
579
- **kwargs: Additional keyword arguments for the curve configuration.
580
-
581
- Returns:
582
- BECCurve: The curve object.
583
- """
584
- curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
585
-
586
- curve_exits = self._check_curve_id(curve_id, self._curves_data)
587
- if curve_exits:
588
- raise ValueError(
589
- f"Curve with ID '{curve_id}' already exists in widget '{self.gui_id}'."
590
- )
591
-
592
- color = (
593
- color
594
- or Colors.golden_angle_color(
595
- colormap=self.config.color_palette,
596
- num=max(10, len(self.plot_item.curves) + 1),
597
- format="HEX",
598
- )[len(self.plot_item.curves)]
599
- )
600
-
601
- # Create curve by config
602
- curve_config = CurveConfig(
603
- widget_class="BECCurve",
604
- parent_id=self.gui_id,
605
- label=curve_id,
606
- color=color,
607
- source=curve_source,
608
- **kwargs,
609
- )
610
-
611
- curve = self._add_curve_object(
612
- name=curve_id, source=curve_source, config=curve_config, data=(x, y)
613
- )
614
- return curve
615
-
616
- def add_curve_bec(
617
- self,
618
- x_name: str | None = None,
619
- y_name: str | None = None,
620
- z_name: str | None = None,
621
- x_entry: str | None = None,
622
- y_entry: str | None = None,
623
- z_entry: str | None = None,
624
- color: str | None = None,
625
- color_map_z: str | None = "magma",
626
- label: str | None = None,
627
- validate_bec: bool = True,
628
- dap: str | None = None,
629
- source: str | None = None,
630
- **kwargs,
631
- ) -> BECCurve:
632
- """
633
- Add a curve to the plot widget from the scan segment. #TODO adapt docs to DAP
634
-
635
- Args:
636
- x_name(str): Name of the x signal.
637
- x_entry(str): Entry of the x signal.
638
- y_name(str): Name of the y signal.
639
- y_entry(str): Entry of the y signal.
640
- z_name(str): Name of the z signal.
641
- z_entry(str): Entry of the z signal.
642
- color(str, optional): Color of the curve. Defaults to None.
643
- color_map_z(str): The color map to use for the z-axis.
644
- label(str, optional): Label of the curve. Defaults to None.
645
- validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
646
- dap(str, optional): The dap model to use for the curve. Defaults to None.
647
- **kwargs: Additional keyword arguments for the curve configuration.
648
-
649
- Returns:
650
- BECCurve: The curve object.
651
- """
652
- # 1. Check - y_name must be provided
653
- if y_name is None:
654
- raise ValueError("y_name must be provided.")
655
-
656
- # 2. Check - check if there is already a x axis signal
657
- if x_name is None:
658
- mode = self.x_axis_mode["name"]
659
- x_name = mode if mode is not None else "best_effort"
660
- self.x_axis_mode["name"] = x_name
661
-
662
- if not x_name or not y_name:
663
- # can happen if executed from a signal from a widget ;
664
- # the code above has to be executed to set some other
665
- # variables, but it cannot continue if both names are
666
- # not set properly -> exit here
667
- return
668
-
669
- # 3. Check - Get entry if not provided and validate
670
- x_entry, y_entry, z_entry = self._validate_signal_entries(
671
- x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
672
- )
673
-
674
- # 4. Check - get source of the device
675
- if source is None:
676
- if validate_bec is True:
677
- source = self._validate_device_source_compatibity(y_name)
678
- else:
679
- source = "scan_segment"
680
-
681
- if z_name is not None and z_entry is not None:
682
- label = label or f"{z_name}-{z_entry}"
683
- else:
684
- label = label or f"{y_name}-{y_entry}"
685
-
686
- # 5. Check - Check if curve already exists
687
- curve_exits = self._check_curve_id(label, self._curves_data)
688
- if curve_exits:
689
- raise ValueError(f"Curve with ID '{label}' already exists in widget '{self.gui_id}'.")
690
-
691
- # Validate or define x axis behaviour and compatibility with y_name readoutPriority
692
- if validate_bec is True:
693
- self._validate_x_axis_behaviour(y_name, x_name, x_entry)
694
-
695
- # Create color if not specified
696
- color = (
697
- color
698
- or Colors.golden_angle_color(
699
- colormap=self.config.color_palette,
700
- num=max(10, len(self.plot_item.curves) + 1),
701
- format="HEX",
702
- )[len(self.plot_item.curves)]
703
- )
704
- logger.info(f"Color: {color}")
705
-
706
- # Create curve by config
707
- curve_config = CurveConfig(
708
- widget_class="BECCurve",
709
- parent_id=self.gui_id,
710
- label=label,
711
- color=color,
712
- color_map_z=color_map_z,
713
- source=source,
714
- signals=Signal(
715
- source=source,
716
- x=SignalData(name=x_name, entry=x_entry) if x_name else None,
717
- y=SignalData(name=y_name, entry=y_entry),
718
- z=SignalData(name=z_name, entry=z_entry) if z_name else None,
719
- dap=dap,
720
- ),
721
- **kwargs,
722
- )
723
-
724
- curve = self._add_curve_object(name=label, source=source, config=curve_config)
725
- return curve
726
-
727
- def add_dap(
728
- self,
729
- x_name: str | None = None,
730
- y_name: str | None = None,
731
- x_entry: Optional[str] = None,
732
- y_entry: Optional[str] = None,
733
- color: Optional[str] = None,
734
- dap: str = "GaussianModel",
735
- validate_bec: bool = True,
736
- **kwargs,
737
- ) -> BECCurve:
738
- """
739
- Add LMFIT dap model curve to the plot widget.
740
-
741
- Args:
742
- x_name(str): Name of the x signal.
743
- x_entry(str): Entry of the x signal.
744
- y_name(str): Name of the y signal.
745
- y_entry(str): Entry of the y signal.
746
- color(str, optional): Color of the curve. Defaults to None.
747
- dap(str): The dap model to use for the curve.
748
- validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
749
- **kwargs: Additional keyword arguments for the curve configuration.
750
-
751
- Returns:
752
- BECCurve: The curve object.
753
- """
754
- if x_name is None:
755
- x_name = self.x_axis_mode["name"]
756
- x_entry = self.x_axis_mode["entry"]
757
- if x_name == "timestamp" or x_name == "index":
758
- raise ValueError(
759
- f"Cannot use x axis '{x_name}' for DAP curve. Please provide a custom x axis signal or switch to 'best_effort' signal mode."
760
- )
761
-
762
- if self.x_axis_mode["readout_priority"] == "async":
763
- raise ValueError(
764
- "Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
765
- )
766
-
767
- if validate_bec is True:
768
- x_entry, y_entry, _ = self._validate_signal_entries(
769
- x_name, y_name, None, x_entry, y_entry, None
770
- )
771
- label = f"{y_name}-{y_entry}-{dap}"
772
- curve = self.add_curve_bec(
773
- x_name=x_name,
774
- y_name=y_name,
775
- x_entry=x_entry,
776
- y_entry=y_entry,
777
- color=color,
778
- label=label,
779
- source="DAP",
780
- dap=dap,
781
- symbol="star",
782
- **kwargs,
783
- )
784
-
785
- self.setup_dap(self.old_scan_id, self.scan_id)
786
- self.refresh_dap()
787
- return curve
788
-
789
- @Slot()
790
- def get_dap_params(self) -> dict:
791
- """
792
- Get the DAP parameters of all DAP curves.
793
-
794
- Returns:
795
- dict: DAP parameters of all DAP curves.
796
- """
797
- params = {}
798
- for curve_id, curve in self._curves_data["DAP"].items():
799
- params[curve_id] = curve.dap_params
800
- return params
801
-
802
- @Slot()
803
- def get_dap_summary(self) -> dict:
804
- """
805
- Get the DAP summary of all DAP curves.
806
-
807
- Returns:
808
- dict: DAP summary of all DAP curves.
809
- """
810
- summary = {}
811
- for curve_id, curve in self._curves_data["DAP"].items():
812
- summary[curve_id] = curve.dap_summary
813
- return summary
814
-
815
- def _add_curve_object(
816
- self,
817
- name: str,
818
- source: str,
819
- config: CurveConfig,
820
- data: tuple[list | np.ndarray, list | np.ndarray] = None,
821
- ) -> BECCurve:
822
- """
823
- Add a curve object to the plot widget.
824
-
825
- Args:
826
- name(str): ID of the curve.
827
- source(str): Source of the curve.
828
- config(CurveConfig): Configuration of the curve.
829
- data(tuple[list|np.ndarray,list|np.ndarray], optional): Data (x,y) to be plotted. Defaults to None.
830
-
831
- Returns:
832
- BECCurve: The curve object.
833
- """
834
- curve = BECCurve(config=config, name=name, parent_item=self)
835
- self._curves_data[source][name] = curve
836
- self.plot_item.addItem(curve)
837
- self.config.curves[name] = curve.config
838
- if data is not None:
839
- curve.setData(data[0], data[1])
840
- self.set_legend_label_size()
841
- return curve
842
-
843
- def _validate_device_source_compatibity(self, name: str):
844
- readout_priority_y = self._get_device_readout_priority(name)
845
- if readout_priority_y == "monitored" or readout_priority_y == "baseline":
846
- source = "scan_segment"
847
- elif readout_priority_y == "async":
848
- source = "async"
849
- else:
850
- raise ValueError(
851
- f"Readout priority '{readout_priority_y}' of device '{name}' is not supported for y signal."
852
- )
853
- return source
854
-
855
- def _validate_x_axis_behaviour(
856
- self, y_name: str, x_name: str | None = None, x_entry: str | None = None, auto_switch=True
857
- ) -> None:
858
- """
859
- Validate the x axis behaviour and consistency for the plot item.
860
-
861
- Args:
862
- source(str): Source of updating device. Can be either "scan_segment" or "async".
863
- x_name(str): Name of the x signal.
864
- - "best_effort": Use the best effort signal.
865
- - "timestamp": Use the timestamp signal.
866
- - "index": Use the index signal.
867
- - Custom signal name of device from BEC.
868
- x_entry(str): Entry of the x signal.
869
- """
870
-
871
- readout_priority_y = self._get_device_readout_priority(y_name)
872
-
873
- # Check if the x axis behaviour is already set
874
- if self._x_axis_mode["name"] is not None:
875
- # Case 1: The same x axis signal is used, check if source is compatible with the device
876
- if x_name != self._x_axis_mode["name"] and x_entry != self._x_axis_mode["entry"]:
877
- # A different x axis signal is used, raise an exception
878
- raise ValueError(
879
- f"All curves must have the same x axis.\n"
880
- f" Current valid x axis: '{self._x_axis_mode['name']}'\n"
881
- f" Attempted to add curve with x axis: '{x_name}'\n"
882
- f"If you want to change the x-axis of the curve, please remove previous curves or change the x axis of the plot widget with '.set_x({x_name})'."
883
- )
884
-
885
- # If x_axis_mode["name"] is None, determine the mode based on x_name
886
- # With async the best effort is always "index"
887
- # Setting mode to either "best_effort", "timestamp", "index", or a custom one
888
- if x_name is None and readout_priority_y == "async":
889
- x_name = "index"
890
- x_entry = "index"
891
- if x_name in ["best_effort", "timestamp", "index"]:
892
- self._x_axis_mode["name"] = x_name
893
- self._x_axis_mode["entry"] = x_entry
894
- else:
895
- self._x_axis_mode["name"] = x_name
896
- self._x_axis_mode["entry"] = x_entry
897
- if readout_priority_y == "async":
898
- raise ValueError(
899
- f"Async devices '{y_name}' cannot be used with custom x signal '{x_name}-{x_entry}'.\n"
900
- f"Please use mode 'best_effort', 'timestamp', or 'index' signal for x axis."
901
- f"You can change the x axis mode with '.set_x(mode)'"
902
- )
903
-
904
- if auto_switch is True:
905
- # Switch the x axis mode accordingly
906
- self._switch_x_axis_item(
907
- f"{x_name}-{x_entry}"
908
- if x_name not in ["best_effort", "timestamp", "index"]
909
- else x_name
910
- )
911
-
912
- def _get_device_readout_priority(self, name: str):
913
- """
914
- Get the type of device from the entry_validator.
915
-
916
- Args:
917
- name(str): Name of the device.
918
- entry(str): Entry of the device.
919
-
920
- Returns:
921
- str: Type of the device.
922
- """
923
- return self.READOUT_PRIORITY_HANDLER[self.dev[name].readout_priority]
924
-
925
- def _switch_x_axis_item(self, mode: str):
926
- """
927
- Switch the x-axis mode between timestamp, index, the best effort and custom signal.
928
-
929
- Args:
930
- mode(str): Mode of the x-axis.
931
- - "timestamp": Use the timestamp signal.
932
- - "index": Use the index signal.
933
- - "best_effort": Use the best effort signal.
934
- - Custom signal name of device from BEC.
935
- """
936
- current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
937
- date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation="bottom")
938
- default_axis = pg.AxisItem(orientation="bottom")
939
- self._x_axis_mode["label_suffix"] = f" [{mode}]"
940
-
941
- if mode == "timestamp":
942
- self.plot_item.setAxisItems({"bottom": date_axis})
943
- self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
944
- elif mode == "index":
945
- self.plot_item.setAxisItems({"bottom": default_axis})
946
- self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
947
- else:
948
- self.plot_item.setAxisItems({"bottom": default_axis})
949
- self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
950
-
951
- def _validate_signal_entries(
952
- self,
953
- x_name: str | None,
954
- y_name: str | None,
955
- z_name: str | None,
956
- x_entry: str | None,
957
- y_entry: str | None,
958
- z_entry: str | None,
959
- validate_bec: bool = True,
960
- ) -> tuple[str, str, str | None]:
961
- """
962
- Validate the signal name and entry.
963
-
964
- Args:
965
- x_name(str): Name of the x signal.
966
- y_name(str): Name of the y signal.
967
- z_name(str): Name of the z signal.
968
- x_entry(str|None): Entry of the x signal.
969
- y_entry(str|None): Entry of the y signal.
970
- z_entry(str|None): Entry of the z signal.
971
- validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
972
-
973
- Returns:
974
- tuple[str,str,str|None]: Validated x, y, z entries.
975
- """
976
- if validate_bec:
977
- if x_name is None:
978
- x_name = "best_effort"
979
- x_entry = "best_effort"
980
- if x_name:
981
- if x_name == "index" or x_name == "timestamp" or x_name == "best_effort":
982
- x_entry = x_name
983
- else:
984
- x_entry = self.entry_validator.validate_signal(x_name, x_entry)
985
- if y_name:
986
- y_entry = self.entry_validator.validate_signal(y_name, y_entry)
987
- if z_name:
988
- z_entry = self.entry_validator.validate_signal(z_name, z_entry)
989
- else:
990
- x_entry = x_name if x_entry is None else x_entry
991
- y_entry = y_name if y_entry is None else y_entry
992
- z_entry = z_name if z_entry is None else z_entry
993
- return x_entry, y_entry, z_entry
994
-
995
- def _check_curve_id(self, val: Any, dict_to_check: dict) -> bool:
996
- """
997
- Check if val is in the values of the dict_to_check or in the values of the nested dictionaries.
998
-
999
- Args:
1000
- val(Any): Value to check.
1001
- dict_to_check(dict): Dictionary to check.
1002
-
1003
- Returns:
1004
- bool: True if val is in the values of the dict_to_check or in the values of the nested dictionaries, False otherwise.
1005
- """
1006
- if val in dict_to_check.keys():
1007
- return True
1008
- for key in dict_to_check:
1009
- if isinstance(dict_to_check[key], dict):
1010
- if self._check_curve_id(val, dict_to_check[key]):
1011
- return True
1012
- return False
1013
-
1014
- def remove_curve(self, *identifiers):
1015
- """
1016
- Remove a curve from the plot widget.
1017
-
1018
- Args:
1019
- *identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id).
1020
- """
1021
- for identifier in identifiers:
1022
- if isinstance(identifier, int):
1023
- self._remove_curve_by_order(identifier)
1024
- elif isinstance(identifier, str):
1025
- self._remove_curve_by_id(identifier)
1026
- else:
1027
- raise ValueError(
1028
- "Each identifier must be either an integer (index) or a string (curve_id)."
1029
- )
1030
-
1031
- def _remove_curve_by_id(self, curve_id):
1032
- """
1033
- Remove a curve by its ID from the plot widget.
1034
-
1035
- Args:
1036
- curve_id(str): ID of the curve to be removed.
1037
- """
1038
- for curves in self._curves_data.values():
1039
- if curve_id in curves:
1040
- curve = curves.pop(curve_id)
1041
- self.plot_item.removeItem(curve)
1042
- del self.config.curves[curve_id]
1043
- if curve in self.plot_item.curves:
1044
- self.plot_item.curves.remove(curve)
1045
- return
1046
- raise KeyError(f"Curve with ID '{curve_id}' not found.")
1047
-
1048
- def _remove_curve_by_order(self, N):
1049
- """
1050
- Remove a curve by its order from the plot widget.
1051
-
1052
- Args:
1053
- N(int): Order of the curve to be removed.
1054
- """
1055
- if N < len(self.plot_item.curves):
1056
- curve = self.plot_item.curves[N]
1057
- curve_id = curve.name() # Assuming curve's name is used as its ID
1058
- self.plot_item.removeItem(curve)
1059
- del self.config.curves[curve_id]
1060
- # Remove from self.curve_data
1061
- for curves in self._curves_data.values():
1062
- if curve_id in curves:
1063
- del curves[curve_id]
1064
- break
1065
- else:
1066
- raise IndexError(f"Curve order {N} out of range.")
1067
-
1068
- @Slot(dict)
1069
- def on_scan_status(self, msg):
1070
- """
1071
- Handle the scan status message.
1072
-
1073
- Args:
1074
- msg(dict): Message received with scan status.
1075
- """
1076
-
1077
- current_scan_id = msg.get("scan_id", None)
1078
- if current_scan_id is None:
1079
- return
1080
-
1081
- if current_scan_id != self.scan_id:
1082
- self.reset()
1083
- self.new_scan.emit()
1084
- self.set_auto_range(True, "xy")
1085
- self.old_scan_id = self.scan_id
1086
- self.scan_id = current_scan_id
1087
- self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
1088
- if self._curves_data["DAP"]:
1089
- self.setup_dap(self.old_scan_id, self.scan_id)
1090
- if self._curves_data["async"]:
1091
- for curve in self._curves_data["async"].values():
1092
- self.setup_async(
1093
- name=curve.config.signals.y.name, entry=curve.config.signals.y.entry
1094
- )
1095
-
1096
- @Slot(dict, dict)
1097
- def on_scan_segment(self, msg: dict, metadata: dict):
1098
- """
1099
- Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
1100
- Used only for triggering scan segment update from the BECClient scan storage.
1101
-
1102
- Args:
1103
- msg (dict): Message received with scan data.
1104
- metadata (dict): Metadata of the scan.
1105
- """
1106
- self.on_scan_status(msg)
1107
- self.scan_signal_update.emit()
1108
- # self.autorange_timer.start(100)
1109
-
1110
- def set_x_label(self, label: str, size: int = None):
1111
- """
1112
- Set the label of the x-axis.
1113
-
1114
- Args:
1115
- label(str): Label of the x-axis.
1116
- size(int): Font size of the label.
1117
- """
1118
- super().set_x_label(label, size)
1119
- current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1120
- self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
1121
-
1122
- def set_colormap(self, colormap: str | None = None):
1123
- """
1124
- Set the colormap of the plot widget.
1125
-
1126
- Args:
1127
- colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
1128
- """
1129
- if colormap is not None:
1130
- self.config.color_palette = colormap
1131
-
1132
- colors = Colors.golden_angle_color(
1133
- colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX"
1134
- )
1135
- for curve, color in zip(self.curves, colors):
1136
- curve.set_color(color)
1137
-
1138
- def setup_dap(self, old_scan_id: str | None, new_scan_id: str | None):
1139
- """
1140
- Setup DAP for the new scan.
1141
-
1142
- Args:
1143
- old_scan_id(str): old_scan_id, used to disconnect the previous dispatcher connection.
1144
- new_scan_id(str): new_scan_id, used to connect the new dispatcher connection.
1145
-
1146
- """
1147
- self.bec_dispatcher.disconnect_slot(
1148
- self.update_dap, MessageEndpoints.dap_response(f"{old_scan_id}-{self.gui_id}")
1149
- )
1150
- if len(self._curves_data["DAP"]) > 0:
1151
- self.bec_dispatcher.connect_slot(
1152
- self.update_dap, MessageEndpoints.dap_response(f"{new_scan_id}-{self.gui_id}")
1153
- )
1154
-
1155
- @Slot(str)
1156
- def setup_async(self, name: str, entry: str):
1157
- self.bec_dispatcher.disconnect_slot(
1158
- self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, name)
1159
- )
1160
- try:
1161
- self._curves_data["async"][f"{name}-{entry}"].clear_data()
1162
- except KeyError:
1163
- pass
1164
- if len(self._curves_data["async"]) > 0:
1165
- self.bec_dispatcher.connect_slot(
1166
- self.on_async_readback,
1167
- MessageEndpoints.device_async_readback(self.scan_id, name),
1168
- from_start=True,
1169
- )
1170
-
1171
- @Slot()
1172
- def refresh_dap(self, _=None):
1173
- """
1174
- Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
1175
- """
1176
- for curve_id, curve in self._curves_data["DAP"].items():
1177
- if len(self._curves_data["async"]) > 0:
1178
- curve.remove()
1179
- raise ValueError(
1180
- f"Cannot refresh DAP curve '{curve_id}' while async curves are present. Removing {curve_id} from display."
1181
- )
1182
- if self._x_axis_mode["name"] == "best_effort":
1183
- try:
1184
- x_name = self.scan_item.status_message.info["scan_report_devices"][0]
1185
- x_entry = self.entry_validator.validate_signal(x_name, None)
1186
- except AttributeError:
1187
- return
1188
- elif curve.config.signals.x is not None:
1189
- x_name = curve.config.signals.x.name
1190
- x_entry = curve.config.signals.x.entry
1191
- if (
1192
- x_name == "timestamp" or x_name == "index"
1193
- ): # timestamp and index not supported by DAP
1194
- return
1195
- try: # to prevent DAP update if the x axis is not the same as the current scan
1196
- current_x_names = self.scan_item.status_message.info["scan_report_devices"]
1197
- if x_name not in current_x_names:
1198
- return
1199
- except AttributeError:
1200
- return
1201
-
1202
- y_name = curve.config.signals.y.name
1203
- y_entry = curve.config.signals.y.entry
1204
- model_name = curve.config.signals.dap
1205
- model = getattr(self.dap, model_name)
1206
- x_min, x_max = self.roi_region
1207
-
1208
- msg = messages.DAPRequestMessage(
1209
- dap_cls="LmfitService1D",
1210
- dap_type="on_demand",
1211
- config={
1212
- "args": [self.scan_id, x_name, x_entry, y_name, y_entry],
1213
- "kwargs": {"x_min": x_min, "x_max": x_max},
1214
- "class_args": model._plugin_info["class_args"],
1215
- "class_kwargs": model._plugin_info["class_kwargs"],
1216
- },
1217
- metadata={"RID": f"{self.scan_id}-{self.gui_id}"},
1218
- )
1219
- self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
1220
-
1221
- @Slot(dict, dict)
1222
- def update_dap(self, msg, metadata):
1223
- """Callback for DAP response message."""
1224
-
1225
- # pylint: disable=unused-variable
1226
- scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
1227
- model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
1228
-
1229
- curve_id_request = f"{y_name}-{y_entry}-{model}"
1230
-
1231
- for curve_id, curve in self._curves_data["DAP"].items():
1232
- if curve_id == curve_id_request:
1233
- if msg["data"] is not None:
1234
- x = msg["data"][0]["x"]
1235
- y = msg["data"][0]["y"]
1236
- curve.setData(x, y)
1237
- curve.dap_params = msg["data"][1]["fit_parameters"]
1238
- curve.dap_summary = msg["data"][1]["fit_summary"]
1239
- metadata.update({"curve_id": curve_id_request})
1240
- self.dap_params_update.emit(curve.dap_params, metadata)
1241
- self.dap_summary_update.emit(curve.dap_summary, metadata)
1242
- break
1243
-
1244
- @Slot(dict, dict)
1245
- def on_async_readback(self, msg, metadata):
1246
- """
1247
- Get async data readback.
1248
-
1249
- Args:
1250
- msg(dict): Message with the async data.
1251
- metadata(dict): Metadata of the message.
1252
- """
1253
- y_data = None
1254
- x_data = None
1255
- instruction = metadata.get("async_update", {}).get("type")
1256
- max_shape = metadata.get("async_update", {}).get("max_shape", [])
1257
- all_async_curves = self._curves_data["async"].values()
1258
- # for curve in self._curves_data["async"].values():
1259
- for curve in all_async_curves:
1260
- y_entry = curve.config.signals.y.entry
1261
- x_name = self._x_axis_mode["name"]
1262
- for device, async_data in msg["signals"].items():
1263
- if device == y_entry:
1264
- data_plot = async_data["value"]
1265
- if instruction == "add":
1266
- if len(max_shape) > 1:
1267
- if len(data_plot.shape) > 1:
1268
- data_plot = data_plot[-1, :]
1269
- else:
1270
- x_data, y_data = curve.get_data()
1271
- if y_data is not None:
1272
- new_data = np.hstack((y_data, data_plot))
1273
- else:
1274
- new_data = data_plot
1275
- if x_name == "timestamp":
1276
- if x_data is not None:
1277
- x_data = np.hstack((x_data, async_data["timestamp"]))
1278
- else:
1279
- x_data = async_data["timestamp"]
1280
- curve.setData(x_data, new_data)
1281
- else:
1282
- curve.setData(new_data)
1283
- elif instruction == "add_slice":
1284
- current_slice_id = metadata.get("async_update", {}).get("index")
1285
- data_plot = async_data["value"]
1286
- if current_slice_id != self._slice_index:
1287
- self._slice_index = current_slice_id
1288
- new_data = data_plot
1289
- else:
1290
- x_data, y_data = curve.get_data()
1291
- new_data = np.hstack((y_data, data_plot))
1292
-
1293
- curve.setData(new_data)
1294
-
1295
- elif instruction == "replace":
1296
- if x_name == "timestamp":
1297
- x_data = async_data["timestamp"]
1298
- curve.setData(x_data, data_plot)
1299
- else:
1300
- curve.setData(data_plot)
1301
-
1302
- @Slot()
1303
- def replot_async_curve(self):
1304
- try:
1305
- data = self.scan_item.async_data
1306
- except AttributeError:
1307
- return
1308
- for curve_id, curve in self._curves_data["async"].items():
1309
- y_name = curve.config.signals.y.name
1310
- y_entry = curve.config.signals.y.entry
1311
- x_name = None
1312
-
1313
- if curve.config.signals.x:
1314
- x_name = curve.config.signals.x.name
1315
-
1316
- if x_name == "timestamp":
1317
- data_x = data[y_name][y_entry]["timestamp"]
1318
- else:
1319
- data_x = None
1320
- data_y = data[y_name][y_entry]["value"]
1321
-
1322
- if data_x is None:
1323
- curve.setData(data_y)
1324
- else:
1325
- curve.setData(data_x, data_y)
1326
-
1327
- @Slot()
1328
- def _update_scan_curves(self, _=None):
1329
- """
1330
- Update the scan curves with the data from the scan segment.
1331
- """
1332
- try:
1333
- data = (
1334
- self.scan_item.live_data
1335
- if hasattr(self.scan_item, "live_data") # backward compatibility
1336
- else self.scan_item.data
1337
- )
1338
- except AttributeError:
1339
- return
1340
-
1341
- data_x = None
1342
- data_y = None
1343
- data_z = None
1344
-
1345
- for curve_id, curve in self._curves_data["scan_segment"].items():
1346
-
1347
- y_name = curve.config.signals.y.name
1348
- y_entry = curve.config.signals.y.entry
1349
- if curve.config.signals.z:
1350
- z_name = curve.config.signals.z.name
1351
- z_entry = curve.config.signals.z.entry
1352
-
1353
- data_x = self._get_x_data(curve, y_name, y_entry)
1354
- if len(data) == 0: # case if the data is empty because motor is not scanned
1355
- return
1356
-
1357
- try:
1358
- data_y = data[y_name][y_entry].val
1359
- if curve.config.signals.z:
1360
- data_z = data[z_name][z_entry].val
1361
- color_z = self._make_z_gradient(data_z, curve.config.color_map_z)
1362
- except TypeError:
1363
- continue
1364
-
1365
- if data_z is not None and color_z is not None:
1366
- try:
1367
- curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
1368
- except:
1369
- return
1370
- if data_x is None:
1371
- curve.setData(data_y)
1372
- else:
1373
- curve.setData(data_x, data_y)
1374
-
1375
- def _get_x_data(self, curve: BECCurve, y_name: str, y_entry: str) -> list | np.ndarray | None:
1376
- """
1377
- Get the x data for the curve with the decision logic based on the curve configuration:
1378
- - If x is called 'timestamp', use the timestamp data from the scan item.
1379
- - If x is called 'index', use the rolling index.
1380
- - If x is a custom signal, use the data from the scan item.
1381
- - If x is not specified, use the first device from the scan report.
1382
-
1383
- Args:
1384
- curve(BECCurve): The curve object.
1385
-
1386
- Returns:
1387
- list|np.ndarray|None: X data for the curve.
1388
- """
1389
- x_data = None
1390
- live_data = (
1391
- self.scan_item.live_data
1392
- if hasattr(self.scan_item, "live_data")
1393
- else self.scan_item.data
1394
- )
1395
- if self._x_axis_mode["name"] == "timestamp":
1396
-
1397
- timestamps = live_data[y_name][y_entry].timestamps
1398
-
1399
- x_data = timestamps
1400
- return x_data
1401
- if self._x_axis_mode["name"] == "index":
1402
- x_data = None
1403
- return x_data
1404
-
1405
- if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort":
1406
- if len(self._curves_data["async"]) > 0:
1407
- x_data = None
1408
- self._x_axis_mode["label_suffix"] = " [auto: index]"
1409
- current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1410
- self.plot_item.setLabel(
1411
- "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
1412
- )
1413
- return x_data
1414
- else:
1415
- x_name = self.scan_item.status_message.info["scan_report_devices"][0]
1416
- x_entry = self.entry_validator.validate_signal(x_name, None)
1417
- x_data = live_data[x_name][x_entry].val
1418
- self._x_axis_mode["label_suffix"] = f" [auto: {x_name}-{x_entry}]"
1419
- current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1420
- self.plot_item.setLabel(
1421
- "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
1422
- )
1423
-
1424
- else:
1425
- x_name = curve.config.signals.x.name
1426
- x_entry = curve.config.signals.x.entry
1427
- try:
1428
- x_data = live_data[x_name][x_entry].val
1429
- except TypeError:
1430
- x_data = []
1431
- return x_data
1432
-
1433
- def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
1434
- """
1435
- Make a gradient color for the z values.
1436
-
1437
- Args:
1438
- data_z(list|np.ndarray): Z values.
1439
- colormap(str): Colormap for the gradient color.
1440
-
1441
- Returns:
1442
- list: List of colors for the z values.
1443
- """
1444
- # Normalize z_values for color mapping
1445
- z_min, z_max = np.min(data_z), np.max(data_z)
1446
-
1447
- if z_max != z_min: # Ensure that there is a range in the z values
1448
- z_values_norm = (data_z - z_min) / (z_max - z_min)
1449
- colormap = pg.colormap.get(colormap) # using colormap from global settings
1450
- colors = [colormap.map(z, mode="qcolor") for z in z_values_norm]
1451
- return colors
1452
- else:
1453
- return None
1454
-
1455
- def scan_history(self, scan_index: int = None, scan_id: str = None):
1456
- """
1457
- Update the scan curves with the data from the scan storage.
1458
- Provide only one of scan_id or scan_index.
1459
-
1460
- Args:
1461
- scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
1462
- scan_index(int, optional): Index of the scan to be updated. Defaults to None.
1463
- """
1464
- if scan_index is not None and scan_id is not None:
1465
- raise ValueError("Only one of scan_id or scan_index can be provided.")
1466
-
1467
- # Reset DAP connector
1468
- self.bec_dispatcher.disconnect_slot(
1469
- self.update_dap, MessageEndpoints.dap_response(self.scan_id)
1470
- )
1471
- if scan_index is not None:
1472
- try:
1473
- self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
1474
- except IndexError:
1475
- logger.error(f"Scan index {scan_index} out of range.")
1476
- return
1477
- elif scan_id is not None:
1478
- self.scan_id = scan_id
1479
-
1480
- self.setup_dap(self.old_scan_id, self.scan_id)
1481
- self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
1482
- self.scan_signal_update.emit()
1483
- self.async_signal_update.emit()
1484
-
1485
- def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame:
1486
- """
1487
- Extract all curve data into a dictionary or a pandas DataFrame.
1488
-
1489
- Args:
1490
- output (Literal["dict", "pandas"]): Format of the output data.
1491
-
1492
- Returns:
1493
- dict | pd.DataFrame: Data of all curves in the specified format.
1494
- """
1495
- data = {}
1496
- try:
1497
- import pandas as pd
1498
- except ImportError:
1499
- pd = None
1500
- if output == "pandas":
1501
- logger.warning(
1502
- "Pandas is not installed. "
1503
- "Please install pandas using 'pip install pandas'."
1504
- "Output will be dictionary instead."
1505
- )
1506
- output = "dict"
1507
-
1508
- for curve in self.plot_item.curves:
1509
- x_data, y_data = curve.get_data()
1510
- if x_data is not None or y_data is not None:
1511
- if output == "dict":
1512
- data[curve.name()] = {"x": x_data.tolist(), "y": y_data.tolist()}
1513
- elif output == "pandas" and pd is not None:
1514
- data[curve.name()] = pd.DataFrame({"x": x_data, "y": y_data})
1515
-
1516
- if output == "pandas" and pd is not None:
1517
- combined_data = pd.concat(
1518
- [data[curve.name()] for curve in self.plot_item.curves],
1519
- axis=1,
1520
- keys=[curve.name() for curve in self.plot_item.curves],
1521
- )
1522
- return combined_data
1523
- return data
1524
-
1525
- def export_to_matplotlib(self):
1526
- """
1527
- Export current waveform to matplotlib gui. Available only if matplotlib is installed in the enviroment.
1528
-
1529
- """
1530
- MatplotlibExporter(self.plot_item).export()
1531
-
1532
- def clear_source(self, source: Literal["DAP", "async", "scan_segment", "custom"]):
1533
- """Clear speicific source from self._curves_data.
1534
-
1535
- Args:
1536
- source (Literal["DAP", "async", "scan_segment", "custom"]): Source to be cleared.
1537
- """
1538
- curves_data = self._curves_data
1539
- curve_ids_to_remove = list(curves_data[source].keys())
1540
- for curve_id in curve_ids_to_remove:
1541
- self.remove_curve(curve_id)
1542
-
1543
- def reset(self):
1544
- self._slice_index = None
1545
- super().reset()
1546
-
1547
- def clear_all(self):
1548
- sources = list(self._curves_data.keys())
1549
- for source in sources:
1550
- self.clear_source(source)
1551
-
1552
- def cleanup(self):
1553
- """Cleanup the widget connection from BECDispatcher."""
1554
- self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
1555
- self.bec_dispatcher.disconnect_slot(
1556
- self.update_dap, MessageEndpoints.dap_response(self.scan_id)
1557
- )
1558
- for curve_id in self._curves_data["async"]:
1559
- self.bec_dispatcher.disconnect_slot(
1560
- self.on_async_readback,
1561
- MessageEndpoints.device_async_readback(self.scan_id, curve_id),
1562
- )
1563
- self.curves.clear()