0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

fix(waveform): only one type of x axis allowed; x mode validated

This commit is contained in:
2024-07-10 14:01:36 +02:00
parent fc5a8bdd8b
commit 9d6ae87d0f

View File

@ -79,8 +79,7 @@ class BECWaveform(BECPlotBase):
self.old_scan_id = None
self.scan_id = None
self.scan_item = None
self.dap = None
self.scan_motors = None # TODO maybe not needed actually, can be fetched direclty from scan_item -> msg['info']['scan_report_devices']
self._x_axis_mode = {"name": None, "entry": None}
# Scan segment update proxy
self.proxy_update_plot = pg.SignalProxy(
@ -143,6 +142,37 @@ class BECWaveform(BECPlotBase):
for curve in self.curves:
curve.config.parent_id = new_gui_id
###################################
# Adding and Removing Curves
###################################
@property
def curves(self) -> list[BECCurve]:
"""
Get the curves of the plot widget as a list
Returns:
list: List of curves.
"""
return self._curves
@curves.setter
def curves(self, value: list[BECCurve]):
self._curves = value
@property
def x_axis_mode(self) -> dict:
"""
Get the x axis mode of the plot widget.
Returns:
dict: The x axis mode.
"""
return self._x_axis_mode
@x_axis_mode.setter
def x_axis_mode(self, value: dict):
self._x_axis_mode = value
def add_curve_by_config(self, curve_config: CurveConfig | dict) -> BECCurve:
"""
Add a curve to the plot widget by its configuration.
@ -177,19 +207,6 @@ class BECWaveform(BECPlotBase):
else:
return curves[curve_id].config
@property
def curves(self) -> list[BECCurve]:
"""
Get the curves of the plot widget as a list
Returns:
list: List of curves.
"""
return self._curves
@curves.setter
def curves(self, value: list[BECCurve]):
self._curves = value
def get_curve(self, identifier) -> BECCurve:
"""
Get the curve by its index or ID.
@ -253,7 +270,7 @@ class BECWaveform(BECPlotBase):
else:
if dap:
self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
return self.add_curve_scan(
curve = self.add_curve_scan(
x_name=x_name,
y_name=y_name,
z_name=z_name,
@ -266,6 +283,35 @@ class BECWaveform(BECPlotBase):
validate_bec=validate,
**kwargs,
)
self.scan_signal_update.emit()
return curve
def change_x_axis(self, x_name: str, x_entry: str | None = None):
"""
Change the x axis of the plot widget.
Args:
x_name(str): Name of the x signal.
x_entry(str): Entry of the x signal.
"""
curve_configs = self.config.curves
curve_ids = list(curve_configs.keys())
curve_configs = list(curve_configs.values())
x_entry, _, _ = self._validate_signal_entries(
x_name, None, None, x_entry, None, None, validate_bec=True
)
self.x_axis_mode = {"name": x_name, "entry": x_entry}
for curve_id, curve_config in zip(curve_ids, curve_configs):
if curve_config.signals.x:
curve_config.signals.x.name = x_name
curve_config.signals.x.entry = x_entry
self.remove_curve(curve_id)
self.add_curve_by_config(curve_config)
self.scan_signal_update.emit()
def add_curve_custom(
self,
@ -360,8 +406,9 @@ class BECWaveform(BECPlotBase):
"""
if y_name is None:
raise ValueError("y_name must be provided.")
# Check if curve already exists
curve_source = source
if x_name is None:
x_name = self.x_axis_mode["name"]
# Get entry if not provided and validate
x_entry, y_entry, z_entry = self._validate_signal_entries(
@ -373,10 +420,15 @@ class BECWaveform(BECPlotBase):
else:
label = label or f"{y_name}-{y_entry}"
# Check if curve already exists
curve_exits = self._check_curve_id(label, self._curves_data)
if curve_exits:
raise ValueError(f"Curve with ID '{label}' already exists in widget '{self.gui_id}'.")
# Validate or define x axis behaviour
self._validate_x_axis_behaviour(x_name, x_entry)
# Create color if not specified
color = (
color
or Colors.golden_angle_color(
@ -391,9 +443,9 @@ class BECWaveform(BECPlotBase):
label=label,
color=color,
color_map_z=color_map_z,
source=curve_source,
source=source,
signals=Signal(
source=curve_source,
source=source,
x=SignalData(name=x_name, entry=x_entry) if x_name else None,
y=SignalData(name=y_name, entry=y_entry),
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
@ -401,13 +453,14 @@ class BECWaveform(BECPlotBase):
),
**kwargs,
)
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
curve = self._add_curve_object(name=label, source=source, config=curve_config)
return curve
def add_dap(
self,
x_name: str,
y_name: str,
x_name: str | None = None,
y_name: str | None = None,
x_entry: Optional[str] = None,
y_entry: Optional[str] = None,
color: Optional[str] = None,
@ -433,8 +486,14 @@ class BECWaveform(BECPlotBase):
Returns:
BECCurve: The curve object.
"""
if validate_bec is True:
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
if x_name is None:
x_name = self.x_axis_mode["name"]
x_entry = self.x_axis_mode["entry"]
if x_name == "timestamp" or x_name == "index":
raise ValueError(
f"Cannot use x axis '{x_name}' for DAP curve. Please provide a custom x axis signal or switch to 'best_effort' signal mode."
)
if validate_bec is True: # TODO adapt dap for x axis global behaviour
x_entry, y_entry, _ = self._validate_signal_entries(
x_name, y_name, None, x_entry, y_entry, None
)
@ -497,6 +556,72 @@ class BECWaveform(BECPlotBase):
self.set_legend_label_size()
return curve
def _validate_x_axis_behaviour(
self, x_name: str | None = None, x_entry: str | None = None
) -> None:
"""
Validate the x axis behaviour and consistency for the plot item.
Args:
x_name(str): Name of the x signal.
- "best_effort": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of device from BEC.
x_entry(str): Entry of the x signal.
"""
# Check if the x axis behaviour is already set
if self._x_axis_mode["name"] is not None:
# Case 1: The same x axis signal is used, do nothing
if x_name == self._x_axis_mode["name"] and x_entry == self._x_axis_mode["entry"]:
return
# Case 2: A different x axis signal is used, raise an exception
raise ValueError(
f"All curves must have the same x axis.\n"
f" Current valid x axis: '{self._x_axis_mode['name']}'\n"
f" Attempted to add curve with x axis: '{x_name}'\n"
f"If you want to change the x-axis of the curve, please remove previous curves."
)
# If x_axis_mode["name"] is None, determine the mode based on x_name
# Setting mode to either "best_effort", "timestamp", "index", or a custom one
if x_name in ["best_effort", "timestamp", "index"]:
self._x_axis_mode["name"] = x_name
self._x_axis_mode["entry"] = x_entry
else:
self._x_axis_mode["name"] = x_name
self._x_axis_mode["entry"] = x_entry
# Switch the x axis mode accordingly
self._switch_x_axis_item(
f"{x_name}-{x_entry}" if x_name not in ["best_effort", "timestamp", "index"] else x_name
)
def _switch_x_axis_item(self, mode: str):
"""
Switch the x-axis mode between timestamp, index, the best effort and custom signal.
Args:
mode(str): Mode of the x-axis.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- "best_effort": Use the best effort signal.
- Custom signal name of device from BEC.
"""
current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation="bottom")
default_axis = pg.AxisItem(orientation="bottom")
if mode == "timestamp":
self.plot_item.setAxisItems({"bottom": date_axis})
self.plot_item.setLabel("bottom", f"{current_label} [timestamp]")
elif mode == "index":
self.plot_item.setAxisItems({"bottom": default_axis})
self.plot_item.setLabel("bottom", f"{current_label} [index]")
else:
self.plot_item.setAxisItems({"bottom": default_axis})
self.plot_item.setLabel("bottom", f"{current_label} [{mode}]")
def _validate_signal_entries(
self,
x_name: str | None,
@ -524,7 +649,7 @@ class BECWaveform(BECPlotBase):
"""
if validate_bec:
if x_name:
if x_name == "index" or x_name == "timestamp":
if x_name == "index" or x_name == "timestamp" or x_name == "best_effort":
x_entry = x_name
else:
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
@ -674,14 +799,32 @@ class BECWaveform(BECPlotBase):
self.update_dap, MessageEndpoints.device_async_readback(self.scan_id, device)
)
@pyqtSlot()
def refresh_dap(self):
"""
Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
"""
for curve_id, curve in self._curves_data["DAP"].items():
x_name = curve.config.signals.x.name
if curve.config.signals.x is not None:
x_name = curve.config.signals.x.name
x_entry = curve.config.signals.x.entry
if (
x_name == "timestamp" or x_name == "index"
): # timestamp and index not supported by DAP
return
try: # to prevent DAP update if the x axis is not the same as the current scan
current_x_names = self.scan_item.status_message.info["scan_report_devices"]
if x_name not in current_x_names:
return
except AttributeError:
return
else:
try:
x_name = self.scan_item.status_message.info["scan_report_devices"][0]
x_entry = self.entry_validator.validate_signal(x_name, None)
except AttributeError:
return
y_name = curve.config.signals.y.name
x_entry = curve.config.signals.x.entry
y_entry = curve.config.signals.y.entry
model_name = curve.config.signals.dap
model = getattr(self.dap, model_name)
@ -728,7 +871,10 @@ class BECWaveform(BECPlotBase):
"""
Update the scan curves with the data from the scan segment.
"""
data = self.scan_item.data
try:
data = self.scan_item.data
except AttributeError:
return
data_x = None
data_y = None
@ -743,6 +889,8 @@ class BECWaveform(BECPlotBase):
z_entry = curve.config.signals.z.entry
data_x = self._get_x_data(curve, y_name, y_entry)
if data_x == []: # case if the data is empty because motor is not scanned
return
try:
data_y = data[y_name][y_entry].val
@ -785,11 +933,15 @@ class BECWaveform(BECPlotBase):
else:
x_name = curve.config.signals.x.name
x_entry = curve.config.signals.x.entry
x_data = self.scan_item.data[x_name][x_entry].val
try:
x_data = self.scan_item.data[x_name][x_entry].val
except TypeError:
x_data = []
else:
x_name = self.scan_item.status_message.info["scan_report_devices"][0]
x_entry = self.entry_validator.validate_signal(x_name, None)
x_data = self.scan_item.data[x_name][x_entry].val
self.set_x_label(f"[auto: {x_name}-{x_entry}]")
return x_data
@ -899,7 +1051,11 @@ class BECWaveform(BECPlotBase):
datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S.%f")
for ts in timestamps
]
return human_readable_dates
data2float = [
time.mktime(datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S.%f").timetuple())
for date in human_readable_dates
]
return data2float
def cleanup(self):
"""Cleanup the widget connection from BECDispatcher."""