wip: digital twin
This commit is contained in:
@@ -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]],
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user