mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
fix: motor_config_validation changed to new monitor config structure
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationError
|
||||||
from pydantic_core import PydanticCustomError
|
from pydantic_core import PydanticCustomError
|
||||||
|
|
||||||
|
|
||||||
@ -68,37 +68,22 @@ class Signal(BaseModel):
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
class PlotAxis(BaseModel):
|
class AxisSignal(BaseModel):
|
||||||
"""
|
"""
|
||||||
Represents an axis (X or Y) in a plot configuration.
|
Configuration signal axis for a single plot.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
label (Optional[str]): The label for the axis.
|
x (list): Signal for the X axis.
|
||||||
signals (list[Signal]): A list of signals to be plotted on this axis.
|
y (list): Signals for the Y axis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
label: Optional[str]
|
x: list[Signal] = Field(default_factory=list)
|
||||||
signals: list[Signal] = Field(default_factory=list)
|
y: list[Signal] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class PlotConfig(BaseModel):
|
|
||||||
"""
|
|
||||||
Configuration for a single plot.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
plot_name (Optional[str]): Name of the plot.
|
|
||||||
x (PlotAxis): Configuration for the X axis.
|
|
||||||
y (PlotAxis): Configuration for the Y axis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
plot_name: Optional[str]
|
|
||||||
x: PlotAxis = Field(...)
|
|
||||||
y: PlotAxis = Field(...)
|
|
||||||
|
|
||||||
@field_validator("x")
|
@field_validator("x")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_x_signals(cls, v):
|
def validate_x_signals(cls, v):
|
||||||
if len(v.signals) != 1:
|
"""Ensure that there is only one signal for x-axis."""
|
||||||
|
if len(v) != 1:
|
||||||
raise PydanticCustomError(
|
raise PydanticCustomError(
|
||||||
"x_axis_multiple_signals",
|
"x_axis_multiple_signals",
|
||||||
'There must be exactly one signal for x axis. Number of x signals: "{wrong_value}"',
|
'There must be exactly one signal for x axis. Number of x signals: "{wrong_value}"',
|
||||||
@ -108,9 +93,92 @@ class PlotConfig(BaseModel):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class SourceHistoryValidator(BaseModel):
|
||||||
|
"""History source validator
|
||||||
|
Attributes:
|
||||||
|
type (str): type of source - history
|
||||||
|
scanID (str): Scan ID for history source.
|
||||||
|
signals (list): Signal for the source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: Literal["history"]
|
||||||
|
scanID: str # TODO can be validated if it is a valid scanID
|
||||||
|
signals: AxisSignal
|
||||||
|
|
||||||
|
|
||||||
|
class SourceSegmentValidator(BaseModel):
|
||||||
|
"""Scan Segment source validator
|
||||||
|
Attributes:
|
||||||
|
type (str): type of source - scan_segment
|
||||||
|
signals (AxisSignal): Signal for the source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: Literal["scan_segment"]
|
||||||
|
signals: AxisSignal
|
||||||
|
|
||||||
|
|
||||||
|
class Source(BaseModel): # TODO decide if it should stay for general Source validation
|
||||||
|
"""
|
||||||
|
General source validation, includes all Optional arguments of all other sources.
|
||||||
|
Attributes:
|
||||||
|
type (list): type of source (scan_segment, history)
|
||||||
|
scanID (Optional[str]): Scan ID for history source.
|
||||||
|
signals (Optional[AxisSignal]): Signal for the source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: Literal["scan_segment", "history"]
|
||||||
|
scanID: Optional[str] = None
|
||||||
|
signals: Optional[AxisSignal] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PlotConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Configuration for a single plot.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
plot_name (Optional[str]): Name of the plot.
|
||||||
|
x_label (Optional[str]): The label for the x-axis.
|
||||||
|
y_label (Optional[str]): The label for the y-axis.
|
||||||
|
sources (list): A list of sources to be plotted on this axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plot_name: Optional[str]
|
||||||
|
x_label: Optional[str]
|
||||||
|
y_label: Optional[str]
|
||||||
|
sources: list = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("sources")
|
||||||
|
@classmethod
|
||||||
|
def validate_sources(cls, values):
|
||||||
|
"""Validate the sources of the plot configuration, based on the type of source."""
|
||||||
|
validated_sources = []
|
||||||
|
for source in values:
|
||||||
|
Source(**source)
|
||||||
|
source_type = source.get("type", None)
|
||||||
|
|
||||||
|
# Check if source type provided
|
||||||
|
if source_type is None:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_source_type", "Source type must be provided", {"wrong_value": source}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if source type is supported
|
||||||
|
if source_type == "scan_segment":
|
||||||
|
validated_sources.append(SourceSegmentValidator(**source))
|
||||||
|
elif source_type == "history":
|
||||||
|
validated_sources.append(SourceHistoryValidator(**source))
|
||||||
|
else:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"unsupported_source_type",
|
||||||
|
"Unsupported source type: '{wrong_value}'",
|
||||||
|
{"wrong_value": source_type},
|
||||||
|
)
|
||||||
|
return validated_sources
|
||||||
|
|
||||||
|
|
||||||
class PlotSettings(BaseModel):
|
class PlotSettings(BaseModel):
|
||||||
"""
|
"""
|
||||||
Global settings for plotting.
|
Global settings for plotting affecting mostly visuals.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
background_color (str): Color of the plot background.
|
background_color (str): Color of the plot background.
|
||||||
|
@ -98,7 +98,6 @@ CONFIG_SIMPLE = {
|
|||||||
"plot_name": "BPM4i plots vs samx",
|
"plot_name": "BPM4i plots vs samx",
|
||||||
"x": {
|
"x": {
|
||||||
"label": "Motor Y",
|
"label": "Motor Y",
|
||||||
# "signals": [{"name": "samx", "entry": "samx"}],
|
|
||||||
"signals": [{"name": "samy"}],
|
"signals": [{"name": "samy"}],
|
||||||
},
|
},
|
||||||
"y": {"label": "bpm4i", "signals": [{"name": "bpm4i", "entry": "bpm4i"}]},
|
"y": {"label": "bpm4i", "signals": [{"name": "bpm4i", "entry": "bpm4i"}]},
|
||||||
@ -108,7 +107,6 @@ CONFIG_SIMPLE = {
|
|||||||
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
"y": {
|
"y": {
|
||||||
"label": "Gauss",
|
"label": "Gauss",
|
||||||
# "signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
|
||||||
"signals": [{"name": "gauss_bpm"}, {"name": "samy", "entry": "samy"}],
|
"signals": [{"name": "gauss_bpm"}, {"name": "samy", "entry": "samy"}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -202,20 +200,14 @@ CONFIG_SOURCE = {
|
|||||||
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# {
|
{
|
||||||
# "type": "history",
|
"type": "history",
|
||||||
# "scanID": "<scanID>",
|
"scanID": "<scanID>",
|
||||||
# "signals": {
|
"signals": {
|
||||||
# "y": [{"name": "bpm4i", "entry": "bpm4i_history_entry"}]
|
"x": [{"name": "samy"}],
|
||||||
# }
|
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
||||||
# },
|
},
|
||||||
# {
|
},
|
||||||
# "type": "redis",
|
|
||||||
# "endpoint": "endpoint1",
|
|
||||||
# "signals": {
|
|
||||||
# "y": [{"name": "bpm4i", "entry": "bpm4i_redis_entry"}]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -464,6 +456,7 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
plot.clear()
|
plot.clear()
|
||||||
|
|
||||||
for source in plot_config["sources"]:
|
for source in plot_config["sources"]:
|
||||||
|
source_type = source["type"][0]
|
||||||
y_signals = source["signals"].get("y", [])
|
y_signals = source["signals"].get("y", [])
|
||||||
colors_ys = Colors.golden_angle_color(
|
colors_ys = Colors.golden_angle_color(
|
||||||
colormap=self.plot_settings["colormap"], num=len(y_signals)
|
colormap=self.plot_settings["colormap"], num=len(y_signals)
|
||||||
@ -474,20 +467,8 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
y_name = y_signal["name"]
|
y_name = y_signal["name"]
|
||||||
y_entry = y_signal.get("entry", y_name)
|
y_entry = y_signal.get("entry", y_name)
|
||||||
|
|
||||||
user_color = self.user_colors.get((plot_name, y_name, y_entry), None)
|
curve_name = f"{y_name} ({y_entry})-{source_type.upper()}"
|
||||||
color_to_use = user_color if user_color else color
|
curve_data = self.create_curve(curve_name, color)
|
||||||
|
|
||||||
pen_curve = mkPen(color=color_to_use, width=2, style=QtCore.Qt.DashLine)
|
|
||||||
brush_curve = mkBrush(color=color_to_use)
|
|
||||||
|
|
||||||
curve_data = pg.PlotDataItem(
|
|
||||||
symbolSize=5,
|
|
||||||
symbolBrush=brush_curve,
|
|
||||||
pen=pen_curve,
|
|
||||||
skipFiniteCheck=True,
|
|
||||||
name=f"{y_name} ({y_entry})",
|
|
||||||
)
|
|
||||||
|
|
||||||
curve_list.append((y_name, y_entry, curve_data))
|
curve_list.append((y_name, y_entry, curve_data))
|
||||||
plot.addItem(curve_data)
|
plot.addItem(curve_data)
|
||||||
row_labels.append(f"{y_name} ({y_entry}) - {plot_name}")
|
row_labels.append(f"{y_name} ({y_entry}) - {plot_name}")
|
||||||
@ -498,6 +479,29 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
if self.enable_crosshair is True:
|
if self.enable_crosshair is True:
|
||||||
self.hook_crosshair()
|
self.hook_crosshair()
|
||||||
|
|
||||||
|
def create_curve(self, curve_name, color):
|
||||||
|
"""
|
||||||
|
Create
|
||||||
|
Args:
|
||||||
|
curve_name:
|
||||||
|
color:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
user_color = self.user_colors.get(curve_name, None)
|
||||||
|
color_to_use = user_color if user_color else color
|
||||||
|
pen_curve = mkPen(color=color_to_use, width=2, style=QtCore.Qt.DashLine)
|
||||||
|
brush_curve = mkBrush(color=color_to_use)
|
||||||
|
|
||||||
|
return pg.PlotDataItem(
|
||||||
|
symbolSize=5,
|
||||||
|
symbolBrush=brush_curve,
|
||||||
|
pen=pen_curve,
|
||||||
|
skipFiniteCheck=True,
|
||||||
|
name=curve_name,
|
||||||
|
)
|
||||||
|
|
||||||
def hook_crosshair(self) -> None:
|
def hook_crosshair(self) -> None:
|
||||||
"""Hook the crosshair to all plots."""
|
"""Hook the crosshair to all plots."""
|
||||||
# TODO can be extended to hook crosshair signal for mouse move/clicked
|
# TODO can be extended to hook crosshair signal for mouse move/clicked
|
||||||
@ -723,7 +727,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
monitor = BECMonitor(
|
monitor = BECMonitor(
|
||||||
config=config,
|
config=config,
|
||||||
gui_id=args.id,
|
gui_id=args.id,
|
||||||
skip_validation=True,
|
skip_validation=False,
|
||||||
)
|
)
|
||||||
monitor.show()
|
monitor.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
Reference in New Issue
Block a user