bec-widgets 0.83.1__py3-none-any.whl → 0.84.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.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
3
4
  import time
4
5
  from collections import defaultdict
5
6
  from typing import Any, Literal, Optional
@@ -7,8 +8,8 @@ from typing import Any, Literal, Optional
7
8
  import numpy as np
8
9
  import pyqtgraph as pg
9
10
  from bec_lib import messages
11
+ from bec_lib.device import ReadoutPriority
10
12
  from bec_lib.endpoints import MessageEndpoints
11
- from bec_lib.scan_data import ScanData
12
13
  from pydantic import Field, ValidationError
13
14
  from qtpy.QtCore import Signal as pyqtSignal
14
15
  from qtpy.QtCore import Slot as pyqtSlot
@@ -27,18 +28,26 @@ from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
27
28
  class Waveform1DConfig(SubplotConfig):
28
29
  color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
29
30
  "plasma", description="The color palette of the figure widget."
30
- ) # TODO can be extended to all colormaps from current pyqtgraph session
31
+ )
31
32
  curves: dict[str, CurveConfig] = Field(
32
33
  {}, description="The list of curves to be added to the 1D waveform widget."
33
34
  )
34
35
 
35
36
 
36
37
  class BECWaveform(BECPlotBase):
38
+ READOUT_PRIORITY_HANDLER = {
39
+ ReadoutPriority.ON_REQUEST: "on_request",
40
+ ReadoutPriority.BASELINE: "baseline",
41
+ ReadoutPriority.MONITORED: "monitored",
42
+ ReadoutPriority.ASYNC: "async",
43
+ ReadoutPriority.CONTINUOUS: "continuous",
44
+ }
37
45
  USER_ACCESS = [
38
46
  "_rpc_id",
39
47
  "_config_dict",
40
48
  "plot",
41
49
  "add_dap",
50
+ "set_x",
42
51
  "get_dap_params",
43
52
  "remove_curve",
44
53
  "scan_history",
@@ -56,10 +65,13 @@ class BECWaveform(BECPlotBase):
56
65
  "set_grid",
57
66
  "lock_aspect_ratio",
58
67
  "remove",
68
+ "clear_all",
59
69
  "set_legend_label_size",
60
70
  ]
61
71
  scan_signal_update = pyqtSignal()
72
+ async_signal_update = pyqtSignal()
62
73
  dap_params_update = pyqtSignal(dict)
74
+ autorange_signal = pyqtSignal()
63
75
 
64
76
  def __init__(
65
77
  self,
@@ -78,20 +90,31 @@ class BECWaveform(BECPlotBase):
78
90
  self._curves_data = defaultdict(dict)
79
91
  self.old_scan_id = None
80
92
  self.scan_id = None
93
+ self.scan_item = None
94
+ self._x_axis_mode = {
95
+ "name": None,
96
+ "entry": None,
97
+ "readout_priority": None,
98
+ "label_suffix": "",
99
+ }
81
100
 
82
101
  # Scan segment update proxy
83
102
  self.proxy_update_plot = pg.SignalProxy(
84
- self.scan_signal_update, rateLimit=25, slot=self._update_scan_segment_plot
103
+ self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves
85
104
  )
86
-
87
105
  self.proxy_update_dap = pg.SignalProxy(
88
106
  self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
89
107
  )
108
+ self.async_signal_update.connect(self.replot_async_curve)
109
+ self.autorange_signal.connect(self.auto_range)
110
+
90
111
  # Get bec shortcuts dev, scans, queue, scan_storage, dap
91
112
  self.get_bec_shortcuts()
92
113
 
93
114
  # Connect dispatcher signals
94
115
  self.bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
116
+ # TODO disabled -> scan_status is SET_AND_PUBLISH -> do not work in combination with autoupdate from CLI
117
+ # self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status())
95
118
 
96
119
  self.entry_validator = EntryValidator(self.dev)
97
120
 
@@ -139,6 +162,41 @@ class BECWaveform(BECPlotBase):
139
162
  for curve in self.curves:
140
163
  curve.config.parent_id = new_gui_id
141
164
 
165
+ ###################################
166
+ # Waveform Properties
167
+ ###################################
168
+
169
+ @property
170
+ def curves(self) -> list[BECCurve]:
171
+ """
172
+ Get the curves of the plot widget as a list
173
+ Returns:
174
+ list: List of curves.
175
+ """
176
+ return self._curves
177
+
178
+ @curves.setter
179
+ def curves(self, value: list[BECCurve]):
180
+ self._curves = value
181
+
182
+ @property
183
+ def x_axis_mode(self) -> dict:
184
+ """
185
+ Get the x axis mode of the plot widget.
186
+
187
+ Returns:
188
+ dict: The x axis mode.
189
+ """
190
+ return self._x_axis_mode
191
+
192
+ @x_axis_mode.setter
193
+ def x_axis_mode(self, value: dict):
194
+ self._x_axis_mode = value
195
+
196
+ ###################################
197
+ # Adding and Removing Curves
198
+ ###################################
199
+
142
200
  def add_curve_by_config(self, curve_config: CurveConfig | dict) -> BECCurve:
143
201
  """
144
202
  Add a curve to the plot widget by its configuration.
@@ -173,19 +231,6 @@ class BECWaveform(BECPlotBase):
173
231
  else:
174
232
  return curves[curve_id].config
175
233
 
176
- @property
177
- def curves(self) -> list[BECCurve]:
178
- """
179
- Get the curves of the plot widget as a list
180
- Returns:
181
- list: List of curves.
182
- """
183
- return self._curves
184
-
185
- @curves.setter
186
- def curves(self, value: list[BECCurve]):
187
- self._curves = value
188
-
189
234
  def get_curve(self, identifier) -> BECCurve:
190
235
  """
191
236
  Get the curve by its index or ID.
@@ -208,8 +253,9 @@ class BECWaveform(BECPlotBase):
208
253
 
209
254
  def plot(
210
255
  self,
211
- x: list | np.ndarray | None = None,
256
+ arg1: list | np.ndarray | str | None = None,
212
257
  y: list | np.ndarray | None = None,
258
+ x: list | np.ndarray | None = None,
213
259
  x_name: str | None = None,
214
260
  y_name: str | None = None,
215
261
  z_name: str | None = None,
@@ -225,10 +271,16 @@ class BECWaveform(BECPlotBase):
225
271
  ) -> BECCurve:
226
272
  """
227
273
  Plot a curve to the plot widget.
274
+
228
275
  Args:
229
- x(list | np.ndarray): Custom x data to plot.
276
+ arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
230
277
  y(list | np.ndarray): Custom y data to plot.
231
- x_name(str): The name of the device for the x-axis.
278
+ x(list | np.ndarray): Custom y data to plot.
279
+ x_name(str): Name of the x signal.
280
+ - "best_effort": Use the best effort signal.
281
+ - "timestamp": Use the timestamp signal.
282
+ - "index": Use the index signal.
283
+ - Custom signal name of device from BEC.
232
284
  y_name(str): The name of the device for the y-axis.
233
285
  z_name(str): The name of the device for the z-axis.
234
286
  x_entry(str): The name of the entry for the x-axis.
@@ -238,30 +290,136 @@ class BECWaveform(BECPlotBase):
238
290
  color_map_z(str): The color map to use for the z-axis.
239
291
  label(str): The label of the curve.
240
292
  validate(bool): If True, validate the device names and entries.
241
- dap(str): The dap model to use for the curve. If not specified, none will be added.
293
+ dap(str): The dap model to use for the curve, only available for sync devices. If not specified, none will be added.
242
294
 
243
295
  Returns:
244
296
  BECCurve: The curve object.
245
297
  """
246
-
247
298
  if x is not None and y is not None:
248
299
  return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
249
- else:
250
- if dap:
251
- self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
252
- return self.add_curve_scan(
253
- x_name=x_name,
254
- y_name=y_name,
255
- z_name=z_name,
256
- x_entry=x_entry,
257
- y_entry=y_entry,
258
- z_entry=z_entry,
259
- color=color,
260
- color_map_z=color_map_z,
261
- label=label,
262
- validate_bec=validate,
263
- **kwargs,
300
+
301
+ if isinstance(arg1, str):
302
+ y_name = arg1
303
+ elif isinstance(arg1, list):
304
+ if isinstance(y, list):
305
+ return self.add_curve_custom(x=arg1, y=y, label=label, color=color, **kwargs)
306
+ if y is None:
307
+ x = np.arange(len(arg1))
308
+ return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs)
309
+ elif isinstance(arg1, np.ndarray) and y is None:
310
+ if arg1.ndim == 1:
311
+ x = np.arange(arg1.size)
312
+ return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs)
313
+ if arg1.ndim == 2:
314
+ x = arg1[:, 0]
315
+ y = arg1[:, 1]
316
+ return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
317
+ if y_name is None:
318
+ raise ValueError("y_name must be provided.")
319
+ if dap:
320
+ self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
321
+ curve = self.add_curve_bec(
322
+ x_name=x_name,
323
+ y_name=y_name,
324
+ z_name=z_name,
325
+ x_entry=x_entry,
326
+ y_entry=y_entry,
327
+ z_entry=z_entry,
328
+ color=color,
329
+ color_map_z=color_map_z,
330
+ label=label,
331
+ validate_bec=validate,
332
+ **kwargs,
333
+ )
334
+ self.scan_signal_update.emit()
335
+ self.async_signal_update.emit()
336
+
337
+ return curve
338
+
339
+ def set_x(self, x_name: str, x_entry: str | None = None):
340
+ """
341
+ Change the x axis of the plot widget.
342
+
343
+ Args:
344
+ x_name(str): Name of the x signal.
345
+ - "best_effort": Use the best effort signal.
346
+ - "timestamp": Use the timestamp signal.
347
+ - "index": Use the index signal.
348
+ - Custom signal name of device from BEC.
349
+ x_entry(str): Entry of the x signal.
350
+ """
351
+ curve_configs = self.config.curves
352
+ curve_ids = list(curve_configs.keys())
353
+ curve_configs = list(curve_configs.values())
354
+ self.set_auto_range(True, "xy")
355
+
356
+ x_entry, _, _ = self._validate_signal_entries(
357
+ x_name, None, None, x_entry, None, None, validate_bec=True
358
+ )
359
+
360
+ readout_priority_x = None
361
+ if x_name not in ["best_effort", "timestamp", "index"]:
362
+ readout_priority_x = self._get_device_readout_priority(x_name)
363
+
364
+ self.x_axis_mode = {
365
+ "name": x_name,
366
+ "entry": x_entry,
367
+ readout_priority_x: readout_priority_x,
368
+ }
369
+
370
+ if len(self.curves) > 0:
371
+ # validate all curves
372
+ for curve in self.curves:
373
+ self._validate_x_axis_behaviour(curve.config.signals.y.name, x_name, x_entry, False)
374
+ self._switch_x_axis_item(
375
+ f"{x_name}-{x_entry}"
376
+ if x_name not in ["best_effort", "timestamp", "index"]
377
+ else x_name
264
378
  )
379
+ for curve_id, curve_config in zip(curve_ids, curve_configs):
380
+ if curve_config.signals.x:
381
+ curve_config.signals.x.name = x_name
382
+ curve_config.signals.x.entry = x_entry
383
+ self.remove_curve(curve_id)
384
+ self.add_curve_by_config(curve_config)
385
+
386
+ self.async_signal_update.emit()
387
+ self.scan_signal_update.emit()
388
+ # self.autorange_timer.start(200)
389
+
390
+ @pyqtSlot()
391
+ def auto_range(self):
392
+ self.plot_item.autoRange()
393
+
394
+ def set_auto_range(self, enabled: bool, axis: str = "xy"):
395
+ """
396
+ Set the auto range of the plot widget.
397
+
398
+ Args:
399
+ enabled(bool): If True, enable the auto range.
400
+ axis(str, optional): The axis to enable the auto range.
401
+ - "xy": Enable auto range for both x and y axis.
402
+ - "x": Enable auto range for x axis.
403
+ - "y": Enable auto range for y axis.
404
+ """
405
+ self.plot_item.enableAutoRange(axis, enabled)
406
+
407
+ @pyqtSlot()
408
+ def auto_range(self):
409
+ self.plot_item.autoRange()
410
+
411
+ def set_auto_range(self, enabled: bool, axis: str = "xy"):
412
+ """
413
+ Set the auto range of the plot widget.
414
+
415
+ Args:
416
+ enabled(bool): If True, enable the auto range.
417
+ axis(str, optional): The axis to enable the auto range.
418
+ - "xy": Enable auto range for both x and y axis.
419
+ - "x": Enable auto range for x axis.
420
+ - "y": Enable auto range for y axis.
421
+ """
422
+ self.plot_item.enableAutoRange(axis, enabled)
265
423
 
266
424
  def add_curve_custom(
267
425
  self,
@@ -317,20 +475,20 @@ class BECWaveform(BECPlotBase):
317
475
  )
318
476
  return curve
319
477
 
320
- def add_curve_scan(
478
+ def add_curve_bec(
321
479
  self,
322
- x_name: str,
323
- y_name: str,
324
- z_name: Optional[str] = None,
325
- x_entry: Optional[str] = None,
326
- y_entry: Optional[str] = None,
327
- z_entry: Optional[str] = None,
328
- color: Optional[str] = None,
329
- color_map_z: Optional[str] = "plasma",
330
- label: Optional[str] = None,
480
+ x_name: str | None = None,
481
+ y_name: str | None = None,
482
+ z_name: str | None = None,
483
+ x_entry: str | None = None,
484
+ y_entry: str | None = None,
485
+ z_entry: str | None = None,
486
+ color: str | None = None,
487
+ color_map_z: str | None = "plasma",
488
+ label: str | None = None,
331
489
  validate_bec: bool = True,
332
- source: str = "scan_segment",
333
- dap: Optional[str] = None,
490
+ dap: str | None = None,
491
+ source: str | None = None,
334
492
  **kwargs,
335
493
  ) -> BECCurve:
336
494
  """
@@ -346,28 +504,50 @@ class BECWaveform(BECPlotBase):
346
504
  color(str, optional): Color of the curve. Defaults to None.
347
505
  color_map_z(str): The color map to use for the z-axis.
348
506
  label(str, optional): Label of the curve. Defaults to None.
507
+ validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
508
+ dap(str, optional): The dap model to use for the curve. Defaults to None.
349
509
  **kwargs: Additional keyword arguments for the curve configuration.
350
510
 
351
511
  Returns:
352
512
  BECCurve: The curve object.
353
513
  """
354
- # Check if curve already exists
355
- curve_source = source
514
+ # 1. Check - y_name must be provided
515
+ if y_name is None:
516
+ raise ValueError("y_name must be provided.")
517
+
518
+ # 2. Check - check if there is already a x axis signal
519
+ if x_name is None:
520
+ mode = self.x_axis_mode["name"]
521
+ x_name = mode if mode is not None else "best_effort"
522
+ self.x_axis_mode["name"] = x_name
356
523
 
357
- # Get entry if not provided and validate
524
+ # 3. Check - Get entry if not provided and validate
358
525
  x_entry, y_entry, z_entry = self._validate_signal_entries(
359
526
  x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
360
527
  )
361
528
 
529
+ # 4. Check - get source of the device
530
+ if source is None:
531
+ if validate_bec is True:
532
+ source = self._validate_device_source_compatibity(y_name)
533
+ else:
534
+ source = "scan_segment"
535
+
362
536
  if z_name is not None and z_entry is not None:
363
537
  label = label or f"{z_name}-{z_entry}"
364
538
  else:
365
539
  label = label or f"{y_name}-{y_entry}"
366
540
 
541
+ # 5. Check - Check if curve already exists
367
542
  curve_exits = self._check_curve_id(label, self._curves_data)
368
543
  if curve_exits:
369
544
  raise ValueError(f"Curve with ID '{label}' already exists in widget '{self.gui_id}'.")
370
545
 
546
+ # Validate or define x axis behaviour and compatibility with y_name readoutPriority
547
+ if validate_bec is True:
548
+ self._validate_x_axis_behaviour(y_name, x_name, x_entry)
549
+
550
+ # Create color if not specified
371
551
  color = (
372
552
  color
373
553
  or Colors.golden_angle_color(
@@ -382,27 +562,29 @@ class BECWaveform(BECPlotBase):
382
562
  label=label,
383
563
  color=color,
384
564
  color_map_z=color_map_z,
385
- source=curve_source,
565
+ source=source,
386
566
  signals=Signal(
387
- source=curve_source,
388
- x=SignalData(name=x_name, entry=x_entry),
567
+ source=source,
568
+ x=SignalData(name=x_name, entry=x_entry) if x_name else None,
389
569
  y=SignalData(name=y_name, entry=y_entry),
390
570
  z=SignalData(name=z_name, entry=z_entry) if z_name else None,
391
571
  dap=dap,
392
572
  ),
393
573
  **kwargs,
394
574
  )
395
- curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
575
+
576
+ curve = self._add_curve_object(name=label, source=source, config=curve_config)
396
577
  return curve
397
578
 
398
579
  def add_dap(
399
580
  self,
400
- x_name: str,
401
- y_name: str,
581
+ x_name: str | None = None,
582
+ y_name: str | None = None,
402
583
  x_entry: Optional[str] = None,
403
584
  y_entry: Optional[str] = None,
404
585
  color: Optional[str] = None,
405
586
  dap: str = "GaussianModel",
587
+ validate_bec: bool = True,
406
588
  **kwargs,
407
589
  ) -> BECCurve:
408
590
  """
@@ -417,16 +599,31 @@ class BECWaveform(BECPlotBase):
417
599
  color_map_z(str): The color map to use for the z-axis.
418
600
  label(str, optional): Label of the curve. Defaults to None.
419
601
  dap(str): The dap model to use for the curve.
602
+ validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
420
603
  **kwargs: Additional keyword arguments for the curve configuration.
421
604
 
422
605
  Returns:
423
606
  BECCurve: The curve object.
424
607
  """
425
- x_entry, y_entry, _ = self._validate_signal_entries(
426
- x_name, y_name, None, x_entry, y_entry, None
427
- )
608
+ if x_name is None:
609
+ x_name = self.x_axis_mode["name"]
610
+ x_entry = self.x_axis_mode["entry"]
611
+ if x_name == "timestamp" or x_name == "index":
612
+ raise ValueError(
613
+ f"Cannot use x axis '{x_name}' for DAP curve. Please provide a custom x axis signal or switch to 'best_effort' signal mode."
614
+ )
615
+
616
+ if self.x_axis_mode["readout_priority"] == "async":
617
+ raise ValueError(
618
+ f"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
619
+ )
620
+
621
+ if validate_bec is True:
622
+ x_entry, y_entry, _ = self._validate_signal_entries(
623
+ x_name, y_name, None, x_entry, y_entry, None
624
+ )
428
625
  label = f"{y_name}-{y_entry}-{dap}"
429
- curve = self.add_curve_scan(
626
+ curve = self.add_curve_bec(
430
627
  x_name=x_name,
431
628
  y_name=y_name,
432
629
  x_entry=x_entry,
@@ -444,6 +641,7 @@ class BECWaveform(BECPlotBase):
444
641
  self.refresh_dap()
445
642
  return curve
446
643
 
644
+ @pyqtSlot()
447
645
  def get_dap_params(self) -> dict:
448
646
  """
449
647
  Get the DAP parameters of all DAP curves.
@@ -484,10 +682,118 @@ class BECWaveform(BECPlotBase):
484
682
  self.set_legend_label_size()
485
683
  return curve
486
684
 
685
+ def _validate_device_source_compatibity(self, name: str):
686
+ readout_priority_y = self._get_device_readout_priority(name)
687
+ if readout_priority_y == "monitored" or readout_priority_y == "baseline":
688
+ source = "scan_segment"
689
+ elif readout_priority_y == "async":
690
+ source = "async"
691
+ else:
692
+ raise ValueError(
693
+ f"Readout priority '{readout_priority_y}' of device '{name}' is not supported for y signal."
694
+ )
695
+ return source
696
+
697
+ def _validate_x_axis_behaviour(
698
+ self, y_name: str, x_name: str | None = None, x_entry: str | None = None, auto_switch=True
699
+ ) -> None:
700
+ """
701
+ Validate the x axis behaviour and consistency for the plot item.
702
+
703
+ Args:
704
+ source(str): Source of updating device. Can be either "scan_segment" or "async".
705
+ x_name(str): Name of the x signal.
706
+ - "best_effort": Use the best effort signal.
707
+ - "timestamp": Use the timestamp signal.
708
+ - "index": Use the index signal.
709
+ - Custom signal name of device from BEC.
710
+ x_entry(str): Entry of the x signal.
711
+ """
712
+
713
+ readout_priority_y = self._get_device_readout_priority(y_name)
714
+
715
+ # Check if the x axis behaviour is already set
716
+ if self._x_axis_mode["name"] is not None:
717
+ # Case 1: The same x axis signal is used, check if source is compatible with the device
718
+ if x_name != self._x_axis_mode["name"] and x_entry != self._x_axis_mode["entry"]:
719
+ # A different x axis signal is used, raise an exception
720
+ raise ValueError(
721
+ f"All curves must have the same x axis.\n"
722
+ f" Current valid x axis: '{self._x_axis_mode['name']}'\n"
723
+ f" Attempted to add curve with x axis: '{x_name}'\n"
724
+ 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})'."
725
+ )
726
+
727
+ # If x_axis_mode["name"] is None, determine the mode based on x_name
728
+ # With async the best effort is always "index"
729
+ # Setting mode to either "best_effort", "timestamp", "index", or a custom one
730
+ if x_name is None and readout_priority_y == "async":
731
+ x_name = "index"
732
+ x_entry = "index"
733
+ if x_name in ["best_effort", "timestamp", "index"]:
734
+ self._x_axis_mode["name"] = x_name
735
+ self._x_axis_mode["entry"] = x_entry
736
+ else:
737
+ self._x_axis_mode["name"] = x_name
738
+ self._x_axis_mode["entry"] = x_entry
739
+ if readout_priority_y == "async":
740
+ raise ValueError(
741
+ f"Async devices '{y_name}' cannot be used with custom x signal '{x_name}-{x_entry}'.\n"
742
+ f"Please use mode 'best_effort', 'timestamp', or 'index' signal for x axis."
743
+ f"You can change the x axis mode with '.set_x(mode)'"
744
+ )
745
+
746
+ if auto_switch is True:
747
+ # Switch the x axis mode accordingly
748
+ self._switch_x_axis_item(
749
+ f"{x_name}-{x_entry}"
750
+ if x_name not in ["best_effort", "timestamp", "index"]
751
+ else x_name
752
+ )
753
+
754
+ def _get_device_readout_priority(self, name: str):
755
+ """
756
+ Get the type of device from the entry_validator.
757
+
758
+ Args:
759
+ name(str): Name of the device.
760
+ entry(str): Entry of the device.
761
+
762
+ Returns:
763
+ str: Type of the device.
764
+ """
765
+ return self.READOUT_PRIORITY_HANDLER[self.dev[name].readout_priority]
766
+
767
+ def _switch_x_axis_item(self, mode: str):
768
+ """
769
+ Switch the x-axis mode between timestamp, index, the best effort and custom signal.
770
+
771
+ Args:
772
+ mode(str): Mode of the x-axis.
773
+ - "timestamp": Use the timestamp signal.
774
+ - "index": Use the index signal.
775
+ - "best_effort": Use the best effort signal.
776
+ - Custom signal name of device from BEC.
777
+ """
778
+ current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
779
+ date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation="bottom")
780
+ default_axis = pg.AxisItem(orientation="bottom")
781
+ self._x_axis_mode["label_suffix"] = f" [{mode}]"
782
+
783
+ if mode == "timestamp":
784
+ self.plot_item.setAxisItems({"bottom": date_axis})
785
+ self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
786
+ elif mode == "index":
787
+ self.plot_item.setAxisItems({"bottom": default_axis})
788
+ self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
789
+ else:
790
+ self.plot_item.setAxisItems({"bottom": default_axis})
791
+ self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
792
+
487
793
  def _validate_signal_entries(
488
794
  self,
489
- x_name: str,
490
- y_name: str,
795
+ x_name: str | None,
796
+ y_name: str | None,
491
797
  z_name: str | None,
492
798
  x_entry: str | None,
493
799
  y_entry: str | None,
@@ -510,8 +816,16 @@ class BECWaveform(BECPlotBase):
510
816
  tuple[str,str,str|None]: Validated x, y, z entries.
511
817
  """
512
818
  if validate_bec:
513
- x_entry = self.entry_validator.validate_signal(x_name, x_entry)
514
- y_entry = self.entry_validator.validate_signal(y_name, y_entry)
819
+ if x_name is None:
820
+ x_name = "best_effort"
821
+ x_entry = "best_effort"
822
+ if x_name:
823
+ if x_name == "index" or x_name == "timestamp" or x_name == "best_effort":
824
+ x_entry = x_name
825
+ else:
826
+ x_entry = self.entry_validator.validate_signal(x_name, x_entry)
827
+ if y_name:
828
+ y_entry = self.entry_validator.validate_signal(y_name, y_entry)
515
829
  if z_name:
516
830
  z_entry = self.entry_validator.validate_signal(z_name, z_entry)
517
831
  else:
@@ -593,30 +907,57 @@ class BECWaveform(BECPlotBase):
593
907
  else:
594
908
  raise IndexError(f"Curve order {N} out of range.")
595
909
 
596
- @pyqtSlot(dict, dict)
597
- def on_scan_segment(self, msg: dict, metadata: dict):
910
+ @pyqtSlot(dict)
911
+ def on_scan_status(self, msg):
598
912
  """
599
- Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
913
+ Handle the scan status message.
600
914
 
601
915
  Args:
602
- msg (dict): Message received with scan data.
603
- metadata (dict): Metadata of the scan.
916
+ msg(dict): Message received with scan status.
604
917
  """
918
+
605
919
  current_scan_id = msg.get("scan_id", None)
606
920
  if current_scan_id is None:
607
921
  return
608
922
 
609
923
  if current_scan_id != self.scan_id:
924
+ self.set_auto_range(True, "xy")
610
925
  self.old_scan_id = self.scan_id
611
926
  self.scan_id = current_scan_id
612
- self.scan_segment_data = self.queue.scan_storage.find_scan_by_ID(
613
- self.scan_id
614
- ) # TODO do scan access through BECFigure
615
- self.setup_dap(self.old_scan_id, self.scan_id)
927
+ self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
928
+ if self._curves_data["DAP"]:
929
+ self.setup_dap(self.old_scan_id, self.scan_id)
930
+ if self._curves_data["async"]:
931
+ for curve_id, curve in self._curves_data["async"].items():
932
+ self.setup_async(curve.config.signals.y.name)
616
933
 
934
+ @pyqtSlot(dict, dict)
935
+ def on_scan_segment(self, msg: dict, metadata: dict):
936
+ """
937
+ Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
938
+ Used only for triggering scan segment update from the BECClient scan storage.
939
+
940
+ Args:
941
+ msg (dict): Message received with scan data.
942
+ metadata (dict): Metadata of the scan.
943
+ """
944
+ self.on_scan_status(msg)
617
945
  self.scan_signal_update.emit()
946
+ # self.autorange_timer.start(100)
947
+
948
+ def set_x_label(self, label: str, size: int = None):
949
+ """
950
+ Set the label of the x-axis.
951
+
952
+ Args:
953
+ label(str): Label of the x-axis.
954
+ size(int): Font size of the label.
955
+ """
956
+ super().set_x_label(label, size)
957
+ current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
958
+ self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}")
618
959
 
619
- def setup_dap(self, old_scan_id, new_scan_id):
960
+ def setup_dap(self, old_scan_id: str | None, new_scan_id: str | None):
620
961
  """
621
962
  Setup DAP for the new scan.
622
963
 
@@ -626,21 +967,61 @@ class BECWaveform(BECPlotBase):
626
967
 
627
968
  """
628
969
  self.bec_dispatcher.disconnect_slot(
629
- self.update_dap, MessageEndpoints.dap_response(old_scan_id)
970
+ self.update_dap, MessageEndpoints.dap_response(f"{old_scan_id}-{self.gui_id}")
630
971
  )
631
972
  if len(self._curves_data["DAP"]) > 0:
632
973
  self.bec_dispatcher.connect_slot(
633
- self.update_dap, MessageEndpoints.dap_response(new_scan_id)
974
+ self.update_dap, MessageEndpoints.dap_response(f"{new_scan_id}-{self.gui_id}")
975
+ )
976
+
977
+ @pyqtSlot(str)
978
+ def setup_async(self, device: str):
979
+ self.bec_dispatcher.disconnect_slot(
980
+ self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, device)
981
+ )
982
+ try:
983
+ self._curves_data["async"][f"{device}-{device}"].clear_data()
984
+ except KeyError:
985
+ pass
986
+ if len(self._curves_data["async"]) > 0:
987
+ self.bec_dispatcher.connect_slot(
988
+ self.on_async_readback,
989
+ MessageEndpoints.device_async_readback(self.scan_id, device),
990
+ from_start=True,
634
991
  )
635
992
 
993
+ @pyqtSlot()
636
994
  def refresh_dap(self):
637
995
  """
638
996
  Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
639
997
  """
640
998
  for curve_id, curve in self._curves_data["DAP"].items():
641
- x_name = curve.config.signals.x.name
999
+ if len(self._curves_data["async"]) > 0:
1000
+ curve.remove()
1001
+ raise ValueError(
1002
+ f"Cannot refresh DAP curve '{curve_id}' while async curves are present. Removing {curve_id} from display."
1003
+ )
1004
+ if self._x_axis_mode["name"] == "best_effort":
1005
+ try:
1006
+ x_name = self.scan_item.status_message.info["scan_report_devices"][0]
1007
+ x_entry = self.entry_validator.validate_signal(x_name, None)
1008
+ except AttributeError:
1009
+ return
1010
+ elif curve.config.signals.x is not None:
1011
+ x_name = curve.config.signals.x.name
1012
+ x_entry = curve.config.signals.x.entry
1013
+ if (
1014
+ x_name == "timestamp" or x_name == "index"
1015
+ ): # timestamp and index not supported by DAP
1016
+ return
1017
+ try: # to prevent DAP update if the x axis is not the same as the current scan
1018
+ current_x_names = self.scan_item.status_message.info["scan_report_devices"]
1019
+ if x_name not in current_x_names:
1020
+ return
1021
+ except AttributeError:
1022
+ return
1023
+
642
1024
  y_name = curve.config.signals.y.name
643
- x_entry = curve.config.signals.x.entry
644
1025
  y_entry = curve.config.signals.y.entry
645
1026
  model_name = curve.config.signals.dap
646
1027
  model = getattr(self.dap, model_name)
@@ -654,7 +1035,7 @@ class BECWaveform(BECPlotBase):
654
1035
  "class_args": model._plugin_info["class_args"],
655
1036
  "class_kwargs": model._plugin_info["class_kwargs"],
656
1037
  },
657
- metadata={"RID": self.scan_id},
1038
+ metadata={"RID": f"{self.scan_id}-{self.gui_id}"},
658
1039
  )
659
1040
  self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
660
1041
 
@@ -676,32 +1057,96 @@ class BECWaveform(BECPlotBase):
676
1057
  self.dap_params_update.emit(curve.dap_params)
677
1058
  break
678
1059
 
679
- def _update_scan_segment_plot(self):
680
- """Update the plot with the data from the scan segment."""
681
- data = self.scan_segment_data.data
682
- self._update_scan_curves(data)
683
-
684
- def _update_scan_curves(self, data: ScanData):
1060
+ @pyqtSlot(dict, dict)
1061
+ def on_async_readback(self, msg, metadata):
685
1062
  """
686
- Update the scan curves with the data from the scan segment.
1063
+ Get async data readback.
687
1064
 
688
1065
  Args:
689
- data(ScanData): Data from the scan segment.
1066
+ msg(dict): Message with the async data.
1067
+ metadata(dict): Metadata of the message.
1068
+ """
1069
+ instruction = metadata.get("async_update")
1070
+ for curve_id, curve in self._curves_data["async"].items():
1071
+ y_name = curve.config.signals.y.name
1072
+ y_entry = curve.config.signals.y.entry
1073
+ x_name = self._x_axis_mode["name"]
1074
+ for device, async_data in msg["signals"].items():
1075
+ if device == y_entry:
1076
+ data_plot = async_data["value"]
1077
+ if instruction == "extend":
1078
+ x_data, y_data = curve.get_data()
1079
+ if y_data is not None:
1080
+ new_data = np.hstack((y_data, data_plot))
1081
+ else:
1082
+ new_data = data_plot
1083
+ if x_name == "timestamp":
1084
+ if x_data is not None:
1085
+ x_data = np.hstack((x_data, async_data["timestamp"]))
1086
+ else:
1087
+ x_data = async_data["timestamp"]
1088
+ curve.setData(x_data, new_data)
1089
+ else:
1090
+ curve.setData(new_data)
1091
+ elif instruction == "replace":
1092
+ if x_name == "timestamp":
1093
+ x_data = async_data["timestamp"]
1094
+ curve.setData(x_data, data_plot)
1095
+ else:
1096
+ curve.setData(data_plot)
1097
+
1098
+ @pyqtSlot()
1099
+ def replot_async_curve(self):
1100
+ try:
1101
+ data = self.scan_item.async_data
1102
+ except AttributeError:
1103
+ return
1104
+ for curve_id, curve in self._curves_data["async"].items():
1105
+ y_name = curve.config.signals.y.name
1106
+ y_entry = curve.config.signals.y.entry
1107
+ x_name = None
1108
+
1109
+ if curve.config.signals.x:
1110
+ x_name = curve.config.signals.x.name
1111
+
1112
+ if x_name == "timestamp":
1113
+ data_x = data[y_name][y_entry]["timestamp"]
1114
+ else:
1115
+ data_x = None
1116
+ data_y = data[y_name][y_entry]["value"]
1117
+
1118
+ if data_x is None:
1119
+ curve.setData(data_y)
1120
+ else:
1121
+ curve.setData(data_x, data_y)
1122
+
1123
+ @pyqtSlot()
1124
+ def _update_scan_curves(self):
1125
+ """
1126
+ Update the scan curves with the data from the scan segment.
690
1127
  """
1128
+ try:
1129
+ data = self.scan_item.data
1130
+ except AttributeError:
1131
+ return
1132
+
691
1133
  data_x = None
692
1134
  data_y = None
693
1135
  data_z = None
1136
+
694
1137
  for curve_id, curve in self._curves_data["scan_segment"].items():
695
- x_name = curve.config.signals.x.name
696
- x_entry = curve.config.signals.x.entry
1138
+
697
1139
  y_name = curve.config.signals.y.name
698
1140
  y_entry = curve.config.signals.y.entry
699
1141
  if curve.config.signals.z:
700
1142
  z_name = curve.config.signals.z.name
701
1143
  z_entry = curve.config.signals.z.entry
702
1144
 
1145
+ data_x = self._get_x_data(curve, y_name, y_entry)
1146
+ if len(data) == 0: # case if the data is empty because motor is not scanned
1147
+ return
1148
+
703
1149
  try:
704
- data_x = data[x_name][x_entry].val
705
1150
  data_y = data[y_name][y_entry].val
706
1151
  if curve.config.signals.z:
707
1152
  data_z = data[z_name][z_entry].val
@@ -714,9 +1159,107 @@ class BECWaveform(BECPlotBase):
714
1159
  curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
715
1160
  except:
716
1161
  return
1162
+ if data_x is None:
1163
+ curve.setData(data_y)
717
1164
  else:
718
1165
  curve.setData(data_x, data_y)
719
1166
 
1167
+ def _get_x_data(self, curve: BECCurve, y_name: str, y_entry: str) -> list | np.ndarray | None:
1168
+ """
1169
+ Get the x data for the curve with the decision logic based on the curve configuration:
1170
+ - If x is called 'timestamp', use the timestamp data from the scan item.
1171
+ - If x is called 'index', use the rolling index.
1172
+ - If x is a custom signal, use the data from the scan item.
1173
+ - If x is not specified, use the first device from the scan report.
1174
+
1175
+ Args:
1176
+ curve(BECCurve): The curve object.
1177
+
1178
+ Returns:
1179
+ list|np.ndarray|None: X data for the curve.
1180
+ """
1181
+ x_data = None
1182
+ if self._x_axis_mode["name"] == "timestamp":
1183
+ timestamps = self.scan_item.data[y_name][y_entry].timestamps
1184
+
1185
+ x_data = timestamps
1186
+ print(x_data)
1187
+ return x_data
1188
+ if self._x_axis_mode["name"] == "index":
1189
+ x_data = None
1190
+ return x_data
1191
+
1192
+ if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort":
1193
+ if len(self._curves_data["async"]) > 0:
1194
+ x_data = None
1195
+ self._x_axis_mode["label_suffix"] = f" [auto: index]"
1196
+ current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1197
+ self.plot_item.setLabel(
1198
+ "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
1199
+ )
1200
+ return x_data
1201
+ else:
1202
+ x_name = self.scan_item.status_message.info["scan_report_devices"][0]
1203
+ x_entry = self.entry_validator.validate_signal(x_name, None)
1204
+ x_data = self.scan_item.data[x_name][x_entry].val
1205
+ self._x_axis_mode["label_suffix"] = f" [auto: {x_name}-{x_entry}]"
1206
+ current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1207
+ self.plot_item.setLabel(
1208
+ "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
1209
+ )
1210
+
1211
+ else:
1212
+ x_name = curve.config.signals.x.name
1213
+ x_entry = curve.config.signals.x.entry
1214
+ try:
1215
+ x_data = self.scan_item.data[x_name][x_entry].val
1216
+ except TypeError:
1217
+ x_data = []
1218
+ return x_data
1219
+
1220
+ # def _get_x_data(self, curve: BECCurve, y_name: str, y_entry: str) -> list | np.ndarray | None:
1221
+ # """
1222
+ # Get the x data for the curve with the decision logic based on the curve configuration:
1223
+ # - If x is called 'timestamp', use the timestamp data from the scan item.
1224
+ # - If x is called 'index', use the rolling index.
1225
+ # - If x is a custom signal, use the data from the scan item.
1226
+ # - If x is not specified, use the first device from the scan report.
1227
+ #
1228
+ # Args:
1229
+ # curve(BECCurve): The curve object.
1230
+ #
1231
+ # Returns:
1232
+ # list|np.ndarray|None: X data for the curve.
1233
+ # """
1234
+ # x_data = None
1235
+ # if curve.config.signals.x is not None:
1236
+ # if curve.config.signals.x.name == "timestamp":
1237
+ # timestamps = self.scan_item.data[y_name][y_entry].timestamps
1238
+ # x_data = self.convert_timestamps(timestamps)
1239
+ # elif curve.config.signals.x.name == "index":
1240
+ # x_data = None
1241
+ # else:
1242
+ # x_name = curve.config.signals.x.name
1243
+ # x_entry = curve.config.signals.x.entry
1244
+ # try:
1245
+ # x_data = self.scan_item.data[x_name][x_entry].val
1246
+ # except TypeError:
1247
+ # x_data = []
1248
+ # else:
1249
+ # if len(self._curves_data["async"]) > 0:
1250
+ # x_data = None
1251
+ # else:
1252
+ # x_name = self.scan_item.status_message.info["scan_report_devices"][0]
1253
+ # x_entry = self.entry_validator.validate_signal(x_name, None)
1254
+ # x_data = self.scan_item.data[x_name][x_entry].val
1255
+ # self._x_axis_mode["label_suffix"] = f" [auto: {x_name}-{x_entry}]"
1256
+ # current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1257
+ # self.plot_item.setLabel(
1258
+ # "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
1259
+ # )
1260
+ #
1261
+ # return x_data
1262
+
720
1263
  def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
721
1264
  """
722
1265
  Make a gradient color for the z values.
@@ -765,8 +1308,9 @@ class BECWaveform(BECPlotBase):
765
1308
  self.scan_id = scan_id
766
1309
 
767
1310
  self.setup_dap(self.old_scan_id, self.scan_id)
768
- data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
769
- self._update_scan_curves(data)
1311
+ self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
1312
+ self.scan_signal_update.emit()
1313
+ self.async_signal_update.emit()
770
1314
 
771
1315
  def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
772
1316
  """
@@ -808,12 +1352,25 @@ class BECWaveform(BECPlotBase):
808
1352
  return combined_data
809
1353
  return data
810
1354
 
1355
+ def clear_all(self):
1356
+ curves_data = self._curves_data
1357
+ sources = list(curves_data.keys())
1358
+ for source in sources:
1359
+ curve_ids_to_remove = list(curves_data[source].keys())
1360
+ for curve_id in curve_ids_to_remove:
1361
+ self.remove_curve(curve_id)
1362
+
811
1363
  def cleanup(self):
812
1364
  """Cleanup the widget connection from BECDispatcher."""
813
1365
  self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
814
1366
  self.bec_dispatcher.disconnect_slot(
815
1367
  self.update_dap, MessageEndpoints.dap_response(self.scan_id)
816
1368
  )
1369
+ for curve_id, curve in self._curves_data["async"].items():
1370
+ self.bec_dispatcher.disconnect_slot(
1371
+ self.on_async_readback,
1372
+ MessageEndpoints.device_async_readback(self.scan_id, curve_id),
1373
+ )
817
1374
  for curve in self.curves:
818
1375
  curve.cleanup()
819
1376
  super().cleanup()