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

This commit is contained in:
x01da
2026-05-07 15:49:13 +02:00
parent 8493b60468
commit fe43dafac8
2 changed files with 32 additions and 159 deletions
@@ -661,11 +661,11 @@ class MoverPanel(QWidget):
# FE Slits
mot = self.dev.sldi_gapx
self.sldi_gapx = MoveWidget(motor=mot, label='GAPX', unit='mm', decimals=2)
self.sldi_gapx = MoveWidget(motor=mot, label='GAPX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.sldi_gapx)
mot = self.dev.sldi_gapy
self.sldi_gapy = MoveWidget(motor=mot, label='GAPY', unit='mm', decimals=2)
self.sldi_gapy = MoveWidget(motor=mot, label='GAPY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.sldi_gapy)
self.sldi_mov_group = Group(
@@ -678,19 +678,19 @@ class MoverPanel(QWidget):
# Collimating mirror
mot = self.dev.cm_trx
self.cm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2)
self.cm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.cm_trx)
mot = self.dev.cm_try
self.cm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.cm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.cm_try)
mot = self.dev.cm_bnd
self.cm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2)
self.cm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2, deadband=0.2)
self.mover_widgets.append(self.cm_bnd)
mot = self.dev.cm_rotx
self.cm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3)
self.cm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.cm_rotx)
self.cm_mov_group = Group(
@@ -705,15 +705,15 @@ class MoverPanel(QWidget):
# Monochromator
mot = self.dev.mo1_bragg
self.mo1_bragg_angle = MoveWidget(motor=mot, label='Bragg Angle', unit='deg', decimals=3)
self.mo1_bragg_angle = MoveWidget(motor=mot, label='Bragg Angle', unit='deg', decimals=3, deadband=0.01)
self.mover_widgets.append(self.mo1_bragg_angle)
mot = self.dev.mo1_trx
self.mo1_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2)
self.mo1_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.mo1_trx)
mot = self.dev.mo1_try
self.mo1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.mo1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.mo1_try)
self.mo1_mov_group = Group(
@@ -727,7 +727,7 @@ class MoverPanel(QWidget):
# OP Slits 1
mot = self.dev.sl1_centery
self.sl1_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2)
self.sl1_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl1_centery)
self.sl1_mov_group = Group(
@@ -739,7 +739,7 @@ class MoverPanel(QWidget):
# OP Beam Monitor 1
mot = self.dev.bm1_try
self.bm1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.bm1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.bm1_try)
self.bm1_mov_group = Group(
@@ -751,19 +751,19 @@ class MoverPanel(QWidget):
# Focusing Mirror
mot = self.dev.fm_trx
self.fm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2)
self.fm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.fm_trx)
mot = self.dev.fm_try
self.fm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.fm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.fm_try)
mot = self.dev.fm_bnd
self.fm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2)
self.fm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2, deadband=0.2)
self.mover_widgets.append(self.fm_bnd)
mot = self.dev.fm_rotx
self.fm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3)
self.fm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.fm_rotx)
self.fm_mov_group = Group(
@@ -778,7 +778,7 @@ class MoverPanel(QWidget):
# OP Slits 2
mot = self.dev.sl2_centery
self.sl2_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2)
self.sl2_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl2_centery)
self.sl2_mov_group = Group(
@@ -790,7 +790,7 @@ class MoverPanel(QWidget):
# OP Beam Monitor 2
mot = self.dev.bm2_try
self.bm2_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.bm2_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.bm2_try)
self.bm2_mov_group = Group(
@@ -802,15 +802,15 @@ class MoverPanel(QWidget):
# Optical Table
mot = self.dev.ot_try
self.ot_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2)
self.ot_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.2)
self.mover_widgets.append(self.ot_try)
mot = self.dev.ot_rotx
self.ot_rotx = MoveWidget(motor=mot, label='ROTX', unit='mrad', decimals=3)
self.ot_rotx = MoveWidget(motor=mot, label='ROTX', unit='mrad', decimals=3, deadband=0.05)
self.mover_widgets.append(self.ot_rotx)
mot = self.dev.ot_es1_trz
self.ot_es1_trz = MoveWidget(motor=mot, label='ES1 TRZ', unit='mm', decimals=0)
self.ot_es1_trz = MoveWidget(motor=mot, label='ES1 TRZ', unit='mm', decimals=0, deadband=5)
self.mover_widgets.append(self.ot_es1_trz)
self.ot_mov_group = Group(
@@ -950,7 +950,7 @@ class SurfacePlots(QWidget):
self.colors = [(79, 163, 224), (240, 128, 60)]
self.text_color = (255, 255, 255)
else: # dark theme
self.color_impenetrable = (220, 220, 220)
self.color_impenetrable = (180, 180, 180)
self.colors = [(26, 111, 173), (212, 83, 10)]
self.text_color = (0, 0, 0)
@@ -1091,26 +1091,26 @@ class SideviewPlot(QWidget):
self.colors = [(79, 163, 224), (240, 128, 60)]
self.text_color = (255, 255, 255)
else: # dark theme
self.color_impenetrable = (220, 220, 220)
self.color_impenetrable = (180, 180, 180)
self.colors = [(26, 111, 173), (212, 83, 10)]
self.text_color = (0, 0, 0)
for idx, scene in enumerate(self.data):
if scene in 'assistant':
brush = QBrush(QColor(*self.colors[idx], 255), Qt.DiagCrossPattern)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1, style=Qt.DashLine)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.DashLine)
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3)
self.plots[scene].setPen(pen)
self.plots[scene].setBrush(brush)
for wall in self.walls:
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101
for pipe in self.pipes:
pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
def plot_vacuum_pipes(self):
pipes = pipe_geometries()
@@ -1141,7 +1141,7 @@ if __name__ == "__main__":
from bec_widgets.utils.colors import apply_theme
app = QApplication(sys.argv)
apply_theme("light")
apply_theme("dark")
dispatcher = BECDispatcher(gui_id="digital_twin")
win = DigitalTwin()
win.show()
@@ -22,47 +22,6 @@ class Status:
MOVING = "moving" # blue mdi.loading (spinning)
ERROR = "error" # red mdi.alert-circle
# class StatusIcon(qta.IconWidget):
# """
# Displays a status icon using qtawesome Material Design Icons.
# Handles its own spin animation for the MOVING state via qta.Spin.
# """
# ICON_SIZE = 36
# # Map each status to an (icon_name, color) pair
# _ICON_MAP = {
# Status.IN_POSITION: ("mdi.check-circle-outline", "#27ae60"),
# Status.NOT_IN_POSITION: ("mdi.close-circle-outline", "#e6d922"),
# Status.ERROR: ("mdi.alert-outline", "#e74c3c"),
# Status.MOVING: ("mdi.gamepad-circle-outline", "#2980b9"),
# }
# def __init__(self, parent=None):
# super().__init__(parent=parent)
# self._status = None
# self._spin_anim = qta.Spin(self, autostart=True)
# self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
# self.set_status(Status.NOT_IN_POSITION)
# def set_status(self, status: str):
# if status == self._status:
# return
# self._status = status
# icon_name, color = self._ICON_MAP[status]
# if status == Status.MOVING:
# icon = qta.icon(icon_name, color=color, animation=self._spin_anim)
# self._spin_anim.start()
# else:
# self._spin_anim.stop()
# icon = qta.icon(icon_name, color=color)
# self.setIcon(icon)
# self.setIconSize(QSize(self.ICON_SIZE, self.ICON_SIZE))
class StatusIcon(QWidget):
"""
Displays a status icon using bec_qthemes Material Design Icons.
@@ -160,41 +119,6 @@ class MotionWorker(QObject):
# time.sleep(0.1)
self.finished.emit(True)
# def run(self):
# """Simulate motion: move in small steps with realistic deceleration."""
# distance = abs(self._target - self._current)
# direction = 1 if self._target > self._current else -1
# speed = max(0.5, distance / 3.0) # units/second (simulated)
# step_time = 0.05 # seconds per step
# pos = self._current
# while not self._stop_flag.is_set():
# remaining = abs(self._target - pos)
# if remaining < 0.01:
# pos = self._target
# self.position_changed.emit(round(pos, 4))
# self.finished.emit(True)
# return
# # Decelerate near the end
# effective_speed = speed * min(1.0, remaining / (distance * 0.2 + 0.001))
# effective_speed = max(0.05, effective_speed)
# step = effective_speed * step_time * direction
# if abs(step) > remaining:
# step = remaining * direction
# pos += step
# pos += random.gauss(0, 0.002) # tiny simulated encoder noise
# if pos > 20: # Simulated error if above 20 mm
# self.error.emit(True)
# return
# self.position_changed.emit(round(pos, 4))
# time.sleep(step_time)
# # Stopped by user
# self.position_changed.emit(round(pos, 4))
# self.finished.emit(False)
class MoveWidget(QWidget):
"""
One motor stage control group containing:
@@ -204,13 +128,12 @@ class MoveWidget(QWidget):
- Start / Stop button
"""
DEADBAND = 0.02 # mm — positions within this tolerance are "in position"
def __init__(self, motor, label: str = '', unit=None, decimals=3):
def __init__(self, motor, label: str = '', unit=None, decimals=3, deadband=0.0):
super().__init__()
self.fb = 0.0
self.target = 0
self.motor = motor
self.deadband = deadband
self._status = Status.IN_POSITION
self._thread: QThread | None = None
self._worker: MotionWorker | None = None
@@ -320,7 +243,7 @@ class MoveWidget(QWidget):
"""Re-evaluate in-position status whenever the target value changes."""
if self._status in (Status.ERROR, Status.MOVING):
return
if abs(self.fb - self.target) <= self.DEADBAND:
if abs(self.fb - self.target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
@@ -333,7 +256,7 @@ class MoveWidget(QWidget):
def _start_motion(self):
target = self.target
if abs(target - self.fb) <= self.DEADBAND:
if abs(target - self.fb) <= self.deadband:
self._set_status(Status.IN_POSITION)
return
@@ -373,7 +296,7 @@ class MoveWidget(QWidget):
def _on_motion_finished(self, reached: bool):
target = self.target
if abs(self.fb - target) <= self.DEADBAND:
if abs(self.fb - target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
@@ -396,53 +319,3 @@ class MoveWidget(QWidget):
if self._thread:
self._thread.quit()
self._thread.wait(2000) # max 2 s grace period
# # ---------------------------------------------------------------------------
# # Main window
# # ---------------------------------------------------------------------------
# class MainWindow(QMainWindow):
# def __init__(self):
# super().__init__()
# self.setWindowTitle("Motor Stage Controller")
# self.setMinimumWidth(620)
# central = QWidget()
# self.setCentralWidget(central)
# layout = QVBoxLayout(central)
# layout.setContentsMargins(16, 16, 16, 16)
# layout.setSpacing(12)
# # Title
# title = QLabel("Motor Stage Controller")
# layout.addWidget(title)
# sep = QFrame()
# sep.setFrameShape(QFrame.HLine)
# layout.addWidget(sep)
# # Three example stage groups
# self.stages: list[StageGroupWidget] = []
# for axis in ("X Axis", "Y Axis", "Z Axis"):
# stage = StageGroupWidget(axis)
# self.stages.append(stage)
# layout.addWidget(stage)
# # Set different initial positions for demo variety
# self.stages[0].spin_value.setValue(25.0)
# self.stages[1].spin_value.setValue(-10.5)
# self.stages[2].spin_value.setValue(5.75)
# def closeEvent(self, event):
# """Stop all motion threads before closing."""
# for stage in self.stages:
# stage.shutdown()
# event.accept()
# if __name__ == "__main__":
# app = QApplication(sys.argv)
# # app.setStyle("Fusion")
# window = MainWindow()
# window.show()
# sys.exit(app.exec())