wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m4s
CI for debye_bec / test (push) Failing after 1m6s

This commit is contained in:
x01da
2026-04-30 14:47:16 +02:00
parent 274bb9154c
commit ce3f231276
3 changed files with 117 additions and 48 deletions
@@ -1,18 +1,19 @@
import sys
import os
import datetime
import numpy as np
from bec_lib import bec_logger
# pylint: disable=E0611
from qtpy.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QDoubleSpinBox, QGroupBox, QApplication, QLineEdit, QLayout
QApplication, QLayout
)
# pylint: disable=E0611
from qtpy.QtCore import QTimer, Qt
from qtpy.QtGui import QColor
from qtpy.QtCore import Qt
from qtpy.QtGui import QColor, QFont
import pyqtgraph as pg
from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
@@ -20,7 +21,6 @@ from debye_bec.bec_widgets.widgets.qt_widgets import InputNumberField, ComboBox,
from debye_bec.bec_widgets.widgets.digital_twin.calculate_positions import calc_positions
from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
logger = bec_logger.logger
@@ -33,7 +33,6 @@ class DigitalTwin(BECWidget, QWidget):
- A live plot that updates every second
"""
USER_ACCESS = ["set_a", "set_b"]
PLUGIN = True
ICON_NAME = "lightbulb"
@@ -41,23 +40,20 @@ class DigitalTwin(BECWidget, QWidget):
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
self.get_bec_shortcuts()
self._history = [] # stores (sum, product) over time
self._t = 0 # tick counter
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.plot_widget = PlotWidget(title='Plot title', chart_data = [])
self.input = InputPanel()
self.plot_widget = PlotWidget()
self.positions = PositionsPanel()
self.root_layout.addWidget(self.plot_widget, stretch=3)
self.root_layout.addWidget(self.input, stretch=1, alignment=Qt.AlignTop)
self.root_layout.addWidget(self.positions, stretch=1, alignment=Qt.AlignTop)
self.root_layout.addWidget(self.input, stretch=1, alignment=Qt.AlignTop) # type: ignore
self.root_layout.addWidget(self.plot_widget, stretch=1, alignment=Qt.AlignTop) # type: ignore
self.root_layout.addWidget(self.positions, stretch=1, alignment=Qt.AlignTop) # type: ignore
self.setLayout(self.root_layout)
self.setWindowTitle("Digital Twin")
self.resize(600, 500)
# self.resize(1500, 800)
self.input.energy.value_changed_connect(self.calc_bragg_angle)
self.input.sldi_hacc.value_changed_connect(self.calc_positions)
@@ -197,7 +193,7 @@ class InputPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# Energy
self.energy = InputNumberField('Energy [keV]', init=8979, decimals=0, single_step=100, ll=4000, hl=65000)
@@ -275,7 +271,7 @@ class PositionsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# FE Slits
self.sldi_gapx = NumberIndicator('GAPX', 'mm', decimals=3)
@@ -402,25 +398,31 @@ class PositionsPanel(QWidget):
class PlotWidget(QWidget):
"""Plot widget with two curves and legend."""
def __init__(self, title: str = "Title", chart_data = [], max_points=2000, parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self.chart_data = chart_data
self.max_points = max_points
self._layout = QVBoxLayout(self)
self._title = QLabel(f"<h2>{title}</h2>")
self._layout.addWidget(self._title)
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self.plot_widget = pg.PlotWidget(axisItems={'bottom': TimeAxis(orientation='bottom')})
self.plot_widget = pg.PlotWidget()
self.plot_widget.getAxis('bottom').enableAutoSIPrefix(False)
self.plot_widget.addLegend()
self.curves = []
colors = self.golden_angle_color(
colormap='plasma', num=max(10, len(self.curves) + 1), format="HEX"
colormap='plasma',
num=2,
format="HEX",
)
self.color_impenetrable = self.impenetrable_color()
for idx, element in enumerate(self.chart_data):
self.data = {
'assistant': [[0, 1000, 2000], [0, 20, 30]],
'reality': [[0, 1000, 2000], [0, 18, 36]],
}
self.pipes = []
self.walls = []
for idx, element in enumerate(self.data):
self.curves.append(
self.plot_widget.plot(
[],
@@ -430,10 +432,62 @@ class PlotWidget(QWidget):
)
)
self._layout.addWidget(self.plot_widget)
self.plot_group = Group(
'Side View',
[
self.plot_widget,
]
)
self.plot_widget.setLabel('left', 'Temperature [°C]')
self.plot_widget.setLabel('bottom', 'Time')
self.plot_widget.setLabel('left', 'Height [mm]')
self.plot_widget.setLabel('bottom', 'Distance [mm]')
self.plot_widget.setMouseEnabled(x=False, y=False)
self.plot_widget.setXRange(0, 25000, padding=0.1)
self.plot_widget.setYRange(-20, 120, padding=0.1)
self.plot_widget.setMenuEnabled(False)
self.plot_widget.hideButtons()
self._layout.addWidget(self.plot_group)
self._layout.addStretch()
self.plot_vacuum_pipes()
self.plot_walls()
self.update_curves()
def plot_vacuum_pipes(self):
for i, _ in enumerate(bl.vacuum_pipes.center):
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight
self.pipes.append(self.plot_widget.plot(
x=np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
y=np.array([top, top]),
pen=pg.mkPen(color=self.color_impenetrable, width=2),
))
self.pipes.append(self.plot_widget.plot(
x=np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
y=np.array([bottom, bottom]),
pen=pg.mkPen(color=self.color_impenetrable, width=2),
))
def plot_walls(self):
for i, _ in enumerate(bl.walls.start):
rect = pg.QtWidgets.QGraphicsRectItem( # pylint: disable=E1101
bl.walls.start[i],
bl.walls.height[i][0],
bl.walls.end[i] - bl.walls.start[i],
bl.walls.height[i][1] - bl.walls.height[i][0],
)
rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
self.plot_widget.addItem(rect)
def impenetrable_color(self):
app = QApplication.instance()
theme = app.theme.theme # type: ignore
if theme == "light":
return (30, 30, 30)
else:
return (220, 220, 220)
def golden_angle_color(
self,
@@ -470,10 +524,10 @@ class PlotWidget(QWidget):
positions = min_pos + positions * (max_pos - min_pos)
# Sample colors from the colormap at the calculated positions
colors = cmap.map(positions, mode="float")
colors = cmap.map(positions, mode="float") # type: ignore
color_list = []
for color in colors:
for color in colors: # type: ignore
if format.upper() == "HEX":
color_list.append(QColor.fromRgbF(*color).name())
elif format.upper() == "RGB":
@@ -505,7 +559,7 @@ class PlotWidget(QWidget):
if theme is None:
app = QApplication.instance()
if hasattr(app, "theme"):
theme = app.theme.theme
theme = app.theme.theme # type: ignore
if theme == "light":
min_pos = 0.0
@@ -516,17 +570,12 @@ class PlotWidget(QWidget):
return min_pos, max_pos
def update_curves(self, timestamps: list[str], data: list[float]):
x = timestamps.copy()
y = data.copy()
min_len = min([min([len(i) for i in y]), len(x)])
x_float = [t.timestamp() for t in x]
for idx, element in enumerate(y):
self.curves[idx].setData(x=np.array(x_float)[0:min_len], y=np.array(element)[0:min_len])
class TimeAxis(pg.AxisItem):
def tickStrings(self, values, scale, spacing):
return [datetime.fromtimestamp(value).strftime("%H:%M:%S") for value in values]
def update_curves(self):
for idx, element in enumerate(self.data):
self.curves[idx].setData(
x=np.array(self.data[element][0]),
y=np.array(self.data[element][1]),
)
# --------------------------------------------------------- Standalone run ---
@@ -537,10 +586,10 @@ if __name__ == "__main__":
from bec_widgets.utils.colors import apply_theme
app = QApplication(sys.argv)
apply_theme("dark")
apply_theme("light")
dispatcher = BECDispatcher(gui_id="digital_twin")
win = DigitalTwin()
win.resize(1000, 800)
# win.resize(1000, 800)
win.show()
sys.exit(app.exec_())
@@ -289,3 +289,23 @@ smpl = sample(
smpl2 = sample(
name='EH-SMPL2',
center=[0, 27500, sourceHeight],)
# Vacuum pipes
# DN40CF ID = 35 mm oder 37 mm
# DN50CF ID = 47.5 mm
# DN63CF ID = 60.2 mm oder 66 mm
# DN100CF ID = 97.4 mm oder 104 mm
pipe = namedtuple('pipes', ['center', 'diameter', 'start', 'end'])
vacuum_pipes = pipe(
center= [27.5, (37.5+27.5)/2, 37.5, 62.5, 72.5],
diameter=[97.4, 97.4, 97.4, 97.4, 97.4],
start= [10952.88, 11750+250, mo2.center[1]+250, 14000, fm.center[1]],
end= [11750-250, mo2.center[1]-250, 14000, fm.center[1], ehWindow.center[1]],
)
Walls = namedtuple('walls', ['start', 'end', 'height'])
walls = Walls(
start= [13999.30],
end= [13999+75.5+30],
height= [[-20, 25]],
)
+4 -4
View File
@@ -10,9 +10,9 @@ from qtpy.QtGui import QFont
class Group(QGroupBox):
def __init__(self, label, widgets):
super().__init__(label)
self.layout = QVBoxLayout(self)
self.layout = QVBoxLayout(self) # type: ignore
for widget in widgets:
self.layout.addWidget(widget)
self.layout.addWidget(widget) # type: ignore
# class TextIndicator(QWidget):
# def __init__(self, label, unit=None, highlight=False):
@@ -94,8 +94,8 @@ class InputTextField(QWidget):
def has_focus(self) -> bool:
return self.val.hasFocus()
def value(self) -> float:
return self.val.val()
def text(self) -> str:
return self.val.text()
def set_on_return(self, func):
"""Connect a function to the Enter/Return key press."""