From ce3f2312765f46df524df1e24cd151239deff339 Mon Sep 17 00:00:00 2001 From: x01da Date: Thu, 30 Apr 2026 14:47:16 +0200 Subject: [PATCH] wip: digital twin --- .../widgets/digital_twin/digital_twin.py | 137 ++++++++++++------ .../widgets/digital_twin/x01da_parameters.py | 20 +++ debye_bec/bec_widgets/widgets/qt_widgets.py | 8 +- 3 files changed, 117 insertions(+), 48 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index 01a3796..608014b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -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"

{title}

") - 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_()) \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py index 1ddd98b..cb5db48 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py @@ -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]], +) diff --git a/debye_bec/bec_widgets/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/qt_widgets.py index 14d0fdd..2ccb69b 100644 --- a/debye_bec/bec_widgets/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/qt_widgets.py @@ -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."""