From fe43dafac8739fd5a4c45ae10db408e57a15fc9d Mon Sep 17 00:00:00 2001 From: x01da Date: Thu, 7 May 2026 15:49:13 +0200 Subject: [PATCH] wip: digital twin --- .../widgets/digital_twin/digital_twin.py | 54 +++---- .../widgets/digital_twin/move_widget.py | 137 +----------------- 2 files changed, 32 insertions(+), 159 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 5c1ab1e..dea2df5 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -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() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py index 2067042..b3b9a40 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -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())