From 6b5ff49b046c2664ca7af51a9cb8ac09652f04cf Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 18 May 2026 10:38:01 +0200 Subject: [PATCH] refactoring --- .../widgets/digital_twin/digital_twin.py | 1334 ++++------------- .../widgets/digital_twin/input_panel.py | 174 +++ .../widgets/digital_twin/move_widget.py | 314 ++-- .../widgets/digital_twin/mover_panel.py | 220 +++ .../bec_widgets/widgets/digital_twin/plots.py | 303 ++++ .../widgets/digital_twin/settings_panel.py | 27 + 6 files changed, 1160 insertions(+), 1212 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/input_panel.py create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/plots.py create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py 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 7e2b163..6eb288d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -3,72 +3,54 @@ Digital Twin: Custom BEC widget to support the beamline alignment. """ import sys +from pathlib import Path + import numpy as np import yaml -from pathlib import Path from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints - -# pylint: disable=E0611 -from qtpy.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, - QApplication, - QLayout, - QMessageBox, - QLabel, - QDialog, - QPushButton, - QStyle, -) -# pylint: disable=E0611 -from qtpy.QtCore import ( - Qt, - QTimer, -) -from qtpy.QtGui import ( - QColor, - QBrush, - QCloseEvent, -) -import pyqtgraph as pg - from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.error_popups import SafeSlot -from debye_bec.bec_widgets.widgets.qt_widgets import ( - InputNumberField, - ComboBox, - Group, - NumberIndicator, - Button, +# pylint: disable=E0611 +from qtpy.QtCore import Qt, QTimer + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QApplication, + QDialog, + QHBoxLayout, + QLabel, + QPushButton, + QStyle, + QVBoxLayout, + QWidget, ) -from debye_bec.bec_widgets.widgets.digital_twin.move_widget import MoveWidget, AbsorberWidget + from debye_bec.bec_widgets.widgets.digital_twin.calc_positions import calc_positions from debye_bec.bec_widgets.widgets.digital_twin.calc_sideview import calc_sideview from debye_bec.bec_widgets.widgets.digital_twin.calc_surfaces import calc_surfaces from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( - sldi_gap_to_acc, - cm_trx_to_stripe, - fm_trx_to_stripe, - mo1_energy_resolution, - cm_reflectivity, - fm_reflectivity, - mo1_bragg_angle, - fm_ideal_pitch, cm_critical_angle, - mirror_surface_geometries, - mo_surface_geometries, - wall_geometries, - pipe_geometries, + cm_reflectivity, + cm_trx_to_stripe, + fm_ideal_pitch, + fm_reflectivity, + fm_trx_to_stripe, + mo1_bragg_angle, + mo1_energy_resolution, + sldi_gap_to_acc, ) -from debye_bec.devices.absorber import STATUS as ABS_STATUS +from debye_bec.bec_widgets.widgets.digital_twin.input_panel import InputPanel +from debye_bec.bec_widgets.widgets.digital_twin.mover_panel import MoverPanel +from debye_bec.bec_widgets.widgets.digital_twin.plots import SideviewPlot, SurfacePlots +from debye_bec.bec_widgets.widgets.digital_twin.settings_panel import SettingsPanel logger = bec_logger.logger OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/x01da_offsets.yaml" + class DigitalTwin(BECWidget, QWidget): """ Main widget of Digital Twin @@ -83,6 +65,7 @@ class DigitalTwin(BECWidget, QWidget): # Check if devices are all in config self.check_config() + self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update()) central = QWidget() self.root_layout = QHBoxLayout(central) @@ -91,22 +74,20 @@ class DigitalTwin(BECWidget, QWidget): self.input_layout = QVBoxLayout(self.input_widget) self.input = InputPanel() self.settings = SettingsPanel() - self.input_layout.addWidget(self.input) # type: ignore - self.input_layout.addWidget(self.settings) # type: ignore + self.input_layout.addWidget(self.input) # type: ignore + self.input_layout.addWidget(self.settings) # type: ignore self.plot_widget = QWidget() self.plot_layout = QVBoxLayout(self.plot_widget) self.sideview_plot = SideviewPlot() self.surface_plots = SurfacePlots() - self.plot_layout.addWidget(self.sideview_plot) # type: ignore - self.plot_layout.addWidget(self.surface_plots) # type: ignore + self.plot_layout.addWidget(self.sideview_plot) # type: ignore + self.plot_layout.addWidget(self.surface_plots) # type: ignore - self.positions = PositionsPanel() self.mover = MoverPanel(self.dev) - self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignTop) # type: ignore - self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignTop) # type: ignore - # self.root_layout.addWidget(self.positions, alignment=Qt.AlignTop) # type: ignore + self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignTop) # type: ignore + self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignTop) # type: ignore self.root_layout.addWidget(self.mover, alignment=Qt.AlignTop) self.setLayout(self.root_layout) @@ -137,7 +118,7 @@ class DigitalTwin(BECWidget, QWidget): # Initialize all values self.load_offsets(recalculate=False) - self.calc_assistant(identifier='init') + self.calc_assistant(identifier="init") # Timer: update plot every 1 second self._timer = QTimer(self) @@ -150,34 +131,38 @@ class DigitalTwin(BECWidget, QWidget): self.surface_plots.apply_theme(theme) self.mover.apply_theme(theme) - def check_config(self): + @SafeSlot() + def check_config(self, *args): + reload = (args[0] if args else {}).get("action") == "reload" + if reload: + self._timer.stop() devices = [ - 'abs', - 'sldi_gapx', - 'sldi_gapy', - 'cm_trx', - 'cm_try', - 'cm_bnd_radius', - 'cm_rotx', - 'mo1_bragg', - 'mo1_trx', - 'mo1_try', - 'sl1_centery', - 'sl1_gapy', - 'bm1_try', - 'fm_trx', - 'fm_try', - 'fm_bnd_radius', - 'fm_rotx', - 'fm_roty', - 'fm_rotz', - 'sl2_centery', - 'sl2_gapy', - 'bm2_try', - 'ot_try', - 'ot_rotx', - 'es0wi_try', - 'ot_es1_trz', + "abs", + "sldi_gapx", + "sldi_gapy", + "cm_trx", + "cm_try", + "cm_bnd_radius", + "cm_rotx", + "mo1_bragg", + "mo1_trx", + "mo1_try", + "sl1_centery", + "sl1_gapy", + "bm1_try", + "fm_trx", + "fm_try", + "fm_bnd_radius", + "fm_rotx", + "fm_roty", + "fm_rotz", + "sl2_centery", + "sl2_gapy", + "bm2_try", + "ot_try", + "ot_rotx", + "es0wi_try", + "ot_es1_trz", ] while True: missing = [d for d in devices if d not in self.dev] @@ -190,16 +175,16 @@ class DigitalTwin(BECWidget, QWidget): top = QHBoxLayout() icon = QLabel() - icon_pixmap = QApplication.style().standardIcon( - QStyle.SP_MessageBoxWarning - ).pixmap(48, 48) + icon_pixmap = ( + QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(48, 48) + ) icon.setPixmap(icon_pixmap) icon.setAlignment(Qt.AlignTop) top.addWidget(icon) text = QLabel( - "The current config does not include all required devices to run Digital Twin." + - "Reload the config with the correct devices." + "The current config does not include all required devices to run Digital Twin." + + "Reload the config with the correct devices." ) text.setWordWrap(True) text.setAlignment(Qt.AlignTop) @@ -214,7 +199,7 @@ class DigitalTwin(BECWidget, QWidget): buttons = QHBoxLayout() check_again = QPushButton("Check Again") - close_app = QPushButton("Close Application") + close_app = QPushButton("Close Application") check_again.clicked.connect(dialog.accept) close_app.clicked.connect(dialog.reject) buttons.addWidget(check_again) @@ -226,13 +211,15 @@ class DigitalTwin(BECWidget, QWidget): info.setMinimumHeight(info.heightForWidth(info.width())) if dialog.exec_() == QDialog.Rejected: QApplication.instance().exit(0) - sys.exit(0) + # sys.exit(0) + if reload: + self._timer.start() @SafeSlot() def calc_assistant(self, *args, **kwargs): - identifier = kwargs['identifier'] + identifier = kwargs["identifier"] match identifier: - case 'init': + case "init": self.update_mo1_mode() self.calc_mo1_bragg_angle() self.calc_cm_crit_pitch() @@ -242,206 +229,204 @@ class DigitalTwin(BECWidget, QWidget): self.calc_cm_fm_harm_suppr() self.calc_fm_ideal_pitch() self.calc_mo1_energy_resolution() - case 'energy': + case "energy": self.calc_mo1_bragg_angle() self.calc_cm_crit_pitch() self.calc_cm_reflectivity() self.calc_fm_reflectivity() self.calc_cm_fm_harm_suppr() self.calc_mo1_energy_resolution() - case 'cm_stripe': + case "cm_stripe": self.calc_cm_crit_pitch() self.calc_cm_reflectivity() self.calc_cm_fm_harm_suppr() - case 'cm_pitch': + case "cm_pitch": self.calc_cm_reflectivity() self.calc_cm_fm_harm_suppr() - case 'mo1_mode': + case "mo1_mode": self.update_mo1_mode() - case 'mo1_xtal': + case "mo1_xtal": self.calc_mo1_bragg_angle() self.calc_mo1_energy_resolution() - case 'fm_focus': + case "fm_focus": self.update_fm_mode() self.calc_fm_ideal_pitch() - case 'fm_focx': + case "fm_focx": self.calc_fm_ideal_pitch() - case 'fm_focy': + case "fm_focy": self.calc_fm_ideal_pitch() - case 'fm_stripe': + case "fm_stripe": self.calc_fm_reflectivity() self.calc_cm_fm_harm_suppr() self.calc_fm_ideal_pitch() - case 'smpl': + case "smpl": self.calc_fm_ideal_pitch() self.calc_positions() self.calc_assistant_sideview() self.calc_assistant_surfaces() def get_assistant_config(self): - fm_focus = self.input.fm_focus.currentText() - if fm_focus in 'Manual': + if fm_focus in "Manual": fm_rotx = self.input.fm_rotx.value() fm_qy = None - elif fm_focus in 'Focused': + elif fm_focus in "Focused": fm_rotx = self.input.fm_rotx_ideal.value() fm_qy = None - else: # Focused + else: # Focused fm_rotx = self.input.fm_rotx_ideal.value() fm_qy = self.qy - config = { # Config in SI units! - 'energy' : self.input.energy.value(), - 'h_acc' : self.input.sldi_hacc.value() * 1e-3, - 'v_acc' : self.input.sldi_vacc.value() * 1e-3, - 'cm_pitch' : -self.input.cm_pitch.value() * 1e-3, - 'cm_stripe' : self.input.cm_stripe.currentText(), - 'cm_trx' : None, - 'mo1_mode' : self.input.mo1_mode.currentText(), - 'mo1_xtal' : self.input.mo1_xtal.currentText(), - 'mo1_bragg' : self.bragg_angle, - 'fm_rotx' : -fm_rotx * 1e-3, - 'fm_stripe' : self.input.fm_stripe.currentText(), - 'fm_trx' : None, - 'fm_qy' : fm_qy, - 'fm_gain_height' : 1, - 'smpl' : self.input.smpl.value(), - } + config = { # Config in SI units! + "energy": self.input.energy.value(), + "h_acc": self.input.sldi_hacc.value() * 1e-3, + "v_acc": self.input.sldi_vacc.value() * 1e-3, + "cm_pitch": -self.input.cm_pitch.value() * 1e-3, + "cm_stripe": self.input.cm_stripe.currentText(), + "cm_trx": None, + "mo1_mode": self.input.mo1_mode.currentText(), + "mo1_xtal": self.input.mo1_xtal.currentText(), + "mo1_bragg": self.bragg_angle, + "fm_rotx": -fm_rotx * 1e-3, + "fm_stripe": self.input.fm_stripe.currentText(), + "fm_trx": None, + "fm_qy": fm_qy, + "fm_gain_height": 1, + "smpl": self.input.smpl.value(), + } # logger.info(f'Config created: {config}') return config def get_reality_config(self): - # Assure all devices are in the config - self.check_config() - mo1_trx = self.dev.mo1_trx.read(cached=True)['mo1_trx']['value'] + mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"] if abs(mo1_trx) > 5: - mo1_mode = 'Monochromatic' + mo1_mode = "Monochromatic" else: - mo1_mode = 'Pinkbeam' + mo1_mode = "Pinkbeam" mo1_bragg = self.dev.mo1_bragg.read(cached=True) - sldi_gapx = self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value'] - sldi_gapy = self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value'] + sldi_gapx = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"] + sldi_gapy = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"] h_acc, v_acc = sldi_gap_to_acc(sldi_gapx, sldi_gapy) - cm_trx = self.dev.cm_trx.read(cached=True)['cm_trx']['value'] + cm_trx = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"] cm_stripe = cm_trx_to_stripe(-cm_trx) - cm_pitch = self.dev.cm_rotx.read(cached=True)['cm_rotx']['value'] - fm_trx = self.dev.fm_trx.read(cached=True)['fm_trx']['value'] + cm_pitch = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] + fm_trx = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"] fm_stripe = fm_trx_to_stripe(-fm_trx) - fm_rotx = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value'] + fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"] fm_rotx_real = 2 * cm_pitch - fm_rotx - smpl = self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value'] - config = { # Config in SI units! - 'energy' : mo1_bragg['mo1_bragg']['value'], - 'h_acc' : h_acc, - 'v_acc' : v_acc, - 'cm_pitch' : -cm_pitch * 1e-3, - 'cm_stripe' : cm_stripe, - 'cm_trx' : -cm_trx, - 'mo1_mode' : mo1_mode, - 'mo1_xtal' : mo1_bragg['mo1_bragg_crystal_current_xtal_string']['value'], - 'mo1_bragg' : mo1_bragg['mo1_bragg_angle']['value']/180*np.pi, - 'fm_rotx' : -fm_rotx_real * 1e-3, - 'fm_stripe' : fm_stripe, - 'fm_trx' : -fm_trx, - 'fm_gain_height' : 1, - 'smpl' : smpl, - } + smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"] + config = { # Config in SI units! + "energy": mo1_bragg["mo1_bragg"]["value"], + "h_acc": h_acc, + "v_acc": v_acc, + "cm_pitch": -cm_pitch * 1e-3, + "cm_stripe": cm_stripe, + "cm_trx": -cm_trx, + "mo1_mode": mo1_mode, + "mo1_xtal": mo1_bragg["mo1_bragg_crystal_current_xtal_string"]["value"], + "mo1_bragg": mo1_bragg["mo1_bragg_angle"]["value"] / 180 * np.pi, + "fm_rotx": -fm_rotx_real * 1e-3, + "fm_stripe": fm_stripe, + "fm_trx": -fm_trx, + "fm_gain_height": 1, + "smpl": smpl, + } # logger.info(f'Config created: {config}') - abs_open = self.dev.abs.read(cached=True)['abs_status_string']['value'] == 'OPEN' + abs_open = self.dev.abs.read(cached=True)["abs_status_string"]["value"] == "OPEN" if not abs_open: ready = True for mover in self.mover.mover_widgets: - if mover.status in ('moving', 'error'): + if mover.status in ("moving", "error"): ready = False if ready: - self.mover.abs.enable_open(True) # Enable open button + self.mover.abs.enable_open(True) # Enable open button else: - self.mover.abs.enable_open(False) # Disable open button + self.mover.abs.enable_open(False) # Disable open button else: - self.mover.abs.enable_open(False) # Disable open button + self.mover.abs.enable_open(False) # Disable open button self.mover.sldi_gapx.set_feedback(sldi_gapx) self.mover.sldi_gapy.set_feedback(sldi_gapy) self.mover.cm_trx.set_feedback(cm_trx) - self.mover.cm_try.set_feedback(self.dev.cm_try.read(cached=True)['cm_try']['value']) - self.mover.cm_bnd.set_feedback(self.dev.cm_bnd_radius.read(cached=True)['cm_bnd_radius']['value']) + self.mover.cm_try.set_feedback(self.dev.cm_try.read(cached=True)["cm_try"]["value"]) + self.mover.cm_bnd.set_feedback( + self.dev.cm_bnd_radius.read(cached=True)["cm_bnd_radius"]["value"] + ) self.mover.cm_rotx.set_feedback(cm_pitch) - self.mover.mo1_bragg_angle.set_feedback(mo1_bragg['mo1_bragg_angle']['value']) + self.mover.mo1_bragg_angle.set_feedback(mo1_bragg["mo1_bragg_angle"]["value"]) self.mover.mo1_trx.set_feedback(mo1_trx) - self.mover.mo1_try.set_feedback(self.dev.mo1_try.read(cached=True)['mo1_try']['value']) - self.mover.sl1_centery.set_feedback(self.dev.sl1_centery.read(cached=True)['sl1_centery']['value']) - self.mover.sl1_gapy.set_feedback(self.dev.sl1_gapy.read(cached=True)['sl1_gapy']['value']) - self.mover.bm1_try.set_feedback(self.dev.bm1_try.read(cached=True)['bm1_try']['value']) + self.mover.mo1_try.set_feedback(self.dev.mo1_try.read(cached=True)["mo1_try"]["value"]) + self.mover.sl1_centery.set_feedback( + self.dev.sl1_centery.read(cached=True)["sl1_centery"]["value"] + ) + self.mover.sl1_gapy.set_feedback(self.dev.sl1_gapy.read(cached=True)["sl1_gapy"]["value"]) + self.mover.bm1_try.set_feedback(self.dev.bm1_try.read(cached=True)["bm1_try"]["value"]) self.mover.fm_trx.set_feedback(fm_trx) - self.mover.fm_try.set_feedback(self.dev.fm_try.read(cached=True)['fm_try']['value']) - self.mover.fm_bnd.set_feedback(self.dev.fm_bnd_radius.read(cached=True)['fm_bnd_radius']['value']) + self.mover.fm_try.set_feedback(self.dev.fm_try.read(cached=True)["fm_try"]["value"]) + self.mover.fm_bnd.set_feedback( + self.dev.fm_bnd_radius.read(cached=True)["fm_bnd_radius"]["value"] + ) self.mover.fm_rotx.set_feedback(fm_rotx) - self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)['fm_roty']['value']) - self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)['fm_rotz']['value']) - self.mover.sl2_centery.set_feedback(self.dev.sl2_centery.read(cached=True)['sl2_centery']['value']) - self.mover.sl2_gapy.set_feedback(self.dev.sl2_gapy.read(cached=True)['sl2_gapy']['value']) - self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)['bm2_try']['value']) - self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)['ot_try']['value']) - self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)['ot_rotx']['value']) + self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)["fm_roty"]["value"]) + self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)["fm_rotz"]["value"]) + self.mover.sl2_centery.set_feedback( + self.dev.sl2_centery.read(cached=True)["sl2_centery"]["value"] + ) + self.mover.sl2_gapy.set_feedback(self.dev.sl2_gapy.read(cached=True)["sl2_gapy"]["value"]) + self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)["bm2_try"]["value"]) + self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"]) + self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"]) self.mover.ot_es1_trz.set_feedback(smpl) - self.mover.es0wi_try.set_feedback(self.dev.es0wi_try.read(cached=True)['es0wi_try']['value']) + self.mover.es0wi_try.set_feedback( + self.dev.es0wi_try.read(cached=True)["es0wi_try"]["value"] + ) self.mover.abs.set_feedback(abs_open) return config - + def adapt_reality(self, *args): pos = {} - pos['sldi_gapx'] = self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value'] - pos['sldi_gapy'] = self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value'] - pos['cm_trx'] = self.dev.cm_trx.read(cached=True)['cm_trx']['value'] - pos['cm_rotx'] = self.dev.cm_rotx.read(cached=True)['cm_rotx']['value'] - pos['mo1_trx'] = self.dev.mo1_trx.read(cached=True)['mo1_trx']['value'] - pos['fm_trx'] = self.dev.fm_trx.read(cached=True)['fm_trx']['value'] - pos['fm_rotx'] = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value'] - pos['ot_es1_trz'] = self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value'] + pos["sldi_gapx"] = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"] + pos["sldi_gapy"] = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"] + pos["cm_trx"] = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"] + pos["cm_rotx"] = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] + pos["mo1_trx"] = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"] + pos["fm_trx"] = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"] + pos["fm_rotx"] = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"] + pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"] # Removing offsets for axis, value in pos.items(): if axis in self.offsets: axis_offsets = self.offsets[axis] - if 'modifier' in axis_offsets and 'offset' in axis_offsets: - for idx, rng in enumerate(axis_offsets['modifier']['range']): - if rng[0] < pos[axis_offsets['modifier']['axis']] < rng[1]: - pos[axis] -= axis_offsets['offset'][idx] + if "modifier" in axis_offsets and "offset" in axis_offsets: + for idx, rng in enumerate(axis_offsets["modifier"]["range"]): + if rng[0] < pos[axis_offsets["modifier"]["axis"]] < rng[1]: + pos[axis] -= axis_offsets["offset"][idx] break - elif 'offset' in axis_offsets: - pos[axis] -= axis_offsets['offset'] + elif "offset" in axis_offsets: + pos[axis] -= axis_offsets["offset"] - self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)['mo1_bragg']['value']) - h_acc, v_acc = sldi_gap_to_acc( - pos['sldi_gapx'], - pos['sldi_gapy'] - ) - self.input.sldi_hacc.set_number(h_acc*1e3) - self.input.sldi_vacc.set_number(v_acc*1e3) - self.input.cm_stripe.set_current_text( - cm_trx_to_stripe(-pos['cm_trx']) - ) - self.input.cm_pitch.set_number(pos['cm_rotx']) - if abs(pos['mo1_trx']) > 5: - mo1_mode = 'Monochromatic' + self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)["mo1_bragg"]["value"]) + h_acc, v_acc = sldi_gap_to_acc(pos["sldi_gapx"], pos["sldi_gapy"]) + self.input.sldi_hacc.set_number(h_acc * 1e3) + self.input.sldi_vacc.set_number(v_acc * 1e3) + self.input.cm_stripe.set_current_text(cm_trx_to_stripe(-pos["cm_trx"])) + self.input.cm_pitch.set_number(pos["cm_rotx"]) + if abs(pos["mo1_trx"]) > 5: + mo1_mode = "Monochromatic" else: - mo1_mode = 'Pinkbeam' + mo1_mode = "Pinkbeam" self.input.mo1_mode.set_current_text(mo1_mode) self.input.mo1_xtal.set_current_text( - self.dev.mo1_bragg.read(cached=True)['mo1_bragg_crystal_current_xtal_string']['value'] + self.dev.mo1_bragg.read(cached=True)["mo1_bragg_crystal_current_xtal_string"]["value"] ) - self.input.fm_stripe.set_current_text( - fm_trx_to_stripe(-pos['fm_trx']) - ) - self.input.fm_focus.set_current_text('Manual') - fm_rotx_real = 2 * pos['cm_rotx'] - pos['fm_rotx'] + self.input.fm_stripe.set_current_text(fm_trx_to_stripe(-pos["fm_trx"])) + self.input.fm_focus.set_current_text("Manual") + fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"] self.input.fm_rotx.set_number(fm_rotx_real) - self.input.smpl.set_number( - pos['ot_es1_trz'] - ) - self.calc_assistant(identifier='init') + self.input.smpl.set_number(pos["ot_es1_trz"]) + self.calc_assistant(identifier="init") def load_offsets(self, recalculate=True, *args): file = Path(OFFSET_FILE) @@ -457,44 +442,46 @@ class DigitalTwin(BECWidget, QWidget): self.offsets = data if recalculate: - self.calc_assistant(identifier='init') + self.calc_assistant(identifier="init") def unload_offsets(self, *args): self.offsets = {} - self.calc_assistant(identifier='init') + self.calc_assistant(identifier="init") def update_fm_mode(self): fm_focus = self.input.fm_focus.currentText() - if fm_focus in 'Manual': + if fm_focus in "Manual": self.input.fm_rotx.setVisible(True) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(False) self.input.fm_focy.setVisible(False) - self.input.fm_rotx_ideal.setLabel('Incidence Angle for focused beam') - elif fm_focus in 'Focused': + self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam") + elif fm_focus in "Focused": self.input.fm_rotx.setVisible(False) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(False) self.input.fm_focy.setVisible(False) - self.input.fm_rotx_ideal.setLabel('Incidence Angle for focused beam') - else: # Defocused + self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam") + else: # Defocused self.input.fm_rotx.setVisible(False) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(True) self.input.fm_focy.setVisible(True) - self.input.fm_rotx_ideal.setLabel('Incidence Angle for defocused beam') + self.input.fm_rotx_ideal.setLabel("Incidence Angle for defocused beam") def calc_reality(self): config = self.get_reality_config() beam = calc_sideview(config) - data = {'x': beam['x'], 'y': beam['y']} - self.sideview_plot.update_curves('reality', data) + data = {"x": beam["x"], "y": beam["y"]} + self.sideview_plot.update_curves("reality", data) # logger.info('Calc reality surfaces') surfaces = calc_surfaces(config) - self.surface_plots.update_surfaces(scene='reality', data=surfaces) + self.surface_plots.update_surfaces(scene="reality", data=surfaces) def calc_mo1_energy_resolution(self): - xtal = self.input.mo1_xtal.currentText().translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + xtal = self.input.mo1_xtal.currentText().translate( + str.maketrans("", "", "()") + ) # Remove brackets from xtal name to conform with parameters energy = self.input.energy.value() self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy)) @@ -504,36 +491,40 @@ class DigitalTwin(BECWidget, QWidget): energy = self.input.energy.value() self.input.cm_refl.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, energy)) self.input.cm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV") - self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3*energy)) + self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3 * energy)) self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_fm_reflectivity(self): fm_stripe = self.input.fm_stripe.currentText() fm_focus = self.input.fm_focus.currentText() - if fm_focus in 'Manual': + if fm_focus in "Manual": fm_rotx = -self.input.fm_rotx.value() * 1e-3 else: fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3 energy = self.input.energy.value() self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, energy)) self.input.fm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV") - self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3*energy)) + self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3 * energy)) self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_cm_fm_harm_suppr(self): - harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value()) + harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / ( + self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value() + ) self.input.cm_fm_harm_suppr.setValue(harm_suppr) - self.input.cm_fm_harm_suppr.setLabel(f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV") + self.input.cm_fm_harm_suppr.setLabel( + f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV" + ) def calc_assistant_sideview(self): beam = calc_sideview(self.get_assistant_config()) - data = {'x': beam['x'], 'y': beam['y']} - self.sideview_plot.update_curves('assistant', data) + data = {"x": beam["x"], "y": beam["y"]} + self.sideview_plot.update_curves("assistant", data) def calc_assistant_surfaces(self): # logger.info('Calc assistant surfaces') surfaces = calc_surfaces(self.get_assistant_config()) - self.surface_plots.update_surfaces(scene='assistant', data=surfaces) + self.surface_plots.update_surfaces(scene="assistant", data=surfaces) def calc_positions(self): out = calc_positions(self.get_assistant_config()) @@ -542,73 +533,56 @@ class DigitalTwin(BECWidget, QWidget): for axis, axis_data in out.items(): if axis in self.offsets: axis_offsets = self.offsets[axis] - if 'modifier' in axis_offsets and 'offset' in axis_offsets: - for idx, rng in enumerate(axis_offsets['modifier']['range']): - if rng[0] < out[axis_offsets['modifier']['axis']]['value'] < rng[1]: - axis_data['value'] += axis_offsets['offset'][idx] + if "modifier" in axis_offsets and "offset" in axis_offsets: + for idx, rng in enumerate(axis_offsets["modifier"]["range"]): + if rng[0] < out[axis_offsets["modifier"]["axis"]]["value"] < rng[1]: + axis_data["value"] += axis_offsets["offset"][idx] break - elif 'offset' in axis_offsets: - axis_data['value'] += axis_offsets['offset'] + elif "offset" in axis_offsets: + axis_data["value"] += axis_offsets["offset"] - self.positions.sldi_gapx.setValue(out['sldi_gapx']['value']) - self.positions.sldi_gapy.setValue(out['sldi_gapy']['value']) - self.positions.cm_trx.setValue(out['cm_trx']['value']) - self.positions.cm_try.setValue(out['cm_try']['value']) - self.positions.cm_bnd.setValue(out['cm_bnd_radius']['value']) - self.positions.cm_rotx.setValue(out['cm_rotx']['value']) - self.positions.mo1_bragg_angle.setValue(out['mo1_bragg_angle']['value']) - self.positions.mo1_trx.setValue(out['mo1_trx']['value']) - self.positions.mo1_try.setValue(out['mo1_try']['value']) - self.positions.sl1_centery.setValue(out['sl1_centery']['value']) - self.positions.bm1_try.setValue(out['bm1_try']['value']) - self.positions.fm_trx.setValue(out['fm_trx']['value']) - self.positions.fm_try.setValue(out['fm_try']['value']) - self.positions.fm_bnd.setValue(out['fm_bnd_radius']['value']) - self.positions.fm_rotx.setValue(out['fm_rotx']['value']) - self.positions.sl2_centery.setValue(out['sl2_centery']['value']) - self.positions.bm2_try.setValue(out['bm2_try']['value']) - self.positions.ot_try.setValue(out['ot_try']['value']) - self.positions.ot_rotx.setValue(out['ot_rotx']['value']) - self.positions.ot_es1_trz.setValue(out['ot_es1_trz']['value']) - - self.mover.sldi_gapx.set_target(out['sldi_gapx']['value']) - self.mover.sldi_gapy.set_target(out['sldi_gapy']['value']) - self.mover.cm_trx.set_target(out['cm_trx']['value']) - self.mover.cm_try.set_target(out['cm_try']['value']) - self.mover.cm_bnd.set_target(out['cm_bnd_radius']['value']) - self.mover.cm_rotx.set_target(out['cm_rotx']['value']) - self.mover.mo1_bragg_angle.set_target(out['mo1_bragg_angle']['value']) - self.mover.mo1_trx.set_target(out['mo1_trx']['value']) - self.mover.mo1_try.set_target(out['mo1_try']['value']) - self.mover.sl1_centery.set_target(out['sl1_centery']['value']) - self.mover.sl1_gapy.set_target(out['sl1_gapy']['value']) - self.mover.bm1_try.set_target(out['bm1_try']['value']) - self.mover.fm_trx.set_target(out['fm_trx']['value']) - self.mover.fm_try.set_target(out['fm_try']['value']) - self.mover.fm_bnd.set_target(out['fm_bnd_radius']['value']) - self.mover.fm_rotx.set_target(out['fm_rotx']['value']) - self.mover.fm_roty.set_target(out['fm_roty']['value']) - self.mover.fm_rotz.set_target(out['fm_rotz']['value']) - self.mover.sl2_centery.set_target(out['sl2_centery']['value']) - self.mover.sl2_gapy.set_target(out['sl2_gapy']['value']) - self.mover.bm2_try.set_target(out['bm2_try']['value']) - self.mover.ot_try.set_target(out['ot_try']['value']) - self.mover.ot_rotx.set_target(out['ot_rotx']['value']) - self.mover.ot_es1_trz.set_target(out['ot_es1_trz']['value']) - self.mover.es0wi_try.set_target(out['es0wi_try']['value']) + self.mover.sldi_gapx.set_target(out["sldi_gapx"]["value"]) + self.mover.sldi_gapy.set_target(out["sldi_gapy"]["value"]) + self.mover.cm_trx.set_target(out["cm_trx"]["value"]) + self.mover.cm_try.set_target(out["cm_try"]["value"]) + self.mover.cm_bnd.set_target(out["cm_bnd_radius"]["value"]) + self.mover.cm_rotx.set_target(out["cm_rotx"]["value"]) + self.mover.mo1_bragg_angle.set_target(out["mo1_bragg_angle"]["value"]) + self.mover.mo1_trx.set_target(out["mo1_trx"]["value"]) + self.mover.mo1_try.set_target(out["mo1_try"]["value"]) + self.mover.sl1_centery.set_target(out["sl1_centery"]["value"]) + self.mover.sl1_gapy.set_target(out["sl1_gapy"]["value"]) + self.mover.bm1_try.set_target(out["bm1_try"]["value"]) + self.mover.fm_trx.set_target(out["fm_trx"]["value"]) + self.mover.fm_try.set_target(out["fm_try"]["value"]) + self.mover.fm_bnd.set_target(out["fm_bnd_radius"]["value"]) + self.mover.fm_rotx.set_target(out["fm_rotx"]["value"]) + self.mover.fm_roty.set_target(out["fm_roty"]["value"]) + self.mover.fm_rotz.set_target(out["fm_rotz"]["value"]) + self.mover.sl2_centery.set_target(out["sl2_centery"]["value"]) + self.mover.sl2_gapy.set_target(out["sl2_gapy"]["value"]) + self.mover.bm2_try.set_target(out["bm2_try"]["value"]) + self.mover.ot_try.set_target(out["ot_try"]["value"]) + self.mover.ot_rotx.set_target(out["ot_rotx"]["value"]) + self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"]) + self.mover.es0wi_try.set_target(out["es0wi_try"]["value"]) def calc_mo1_bragg_angle(self): """ Calculates bragg angle in rad """ xtal = self.input.mo1_xtal.currentText() - if xtal in 'Si(111)': - d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)['mo1_bragg_crystal_d_spacing_si111']['value'] - elif xtal in 'Si(311)': - d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)['mo1_bragg_crystal_d_spacing_si311']['value'] + if xtal in "Si(111)": + d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)[ + "mo1_bragg_crystal_d_spacing_si111" + ]["value"] + elif xtal in "Si(311)": + d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)[ + "mo1_bragg_crystal_d_spacing_si311" + ]["value"] else: - raise Exception(f'Invalid xtal selection: {xtal}') - cm_pitch = -self.dev.cm_rotx.read(cached=True)['cm_rotx']['value'] * 1e-3 + raise Exception(f"Invalid xtal selection: {xtal}") + cm_pitch = -self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] * 1e-3 mo1_mode = self.input.mo1_mode.currentText() energy = self.input.energy.value() theta, theta_cor = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch) @@ -616,7 +590,7 @@ class DigitalTwin(BECWidget, QWidget): self.input.mo1_bragg_angle.setValue(theta / np.pi * 180) def update_mo1_mode(self): - if self.input.mo1_mode.currentText() in 'Monochromatic': + if self.input.mo1_mode.currentText() in "Monochromatic": self.input.mo1_xtal.setVisible(True) self.input.mo1_bragg_angle.setVisible(True) self.input.mo1_eres.setVisible(True) @@ -625,7 +599,7 @@ class DigitalTwin(BECWidget, QWidget): self.input.mo1_bragg_angle.setVisible(False) self.input.mo1_eres.setVisible(False) - def calc_fm_ideal_pitch(self): # TODO: What happens if the flats are selected? + def calc_fm_ideal_pitch(self): # TODO: What happens if the flats are selected? fm_focus = self.input.fm_focus.currentText() fm_stripe = self.input.fm_stripe.currentText() smpl = self.input.smpl.value() @@ -633,7 +607,9 @@ class DigitalTwin(BECWidget, QWidget): sldi_vacc = self.input.sldi_vacc.value() * 1e-3 fm_focx = self.input.fm_focx.value() fm_focy = self.input.fm_focy.value() - fm_rotx, qy = fm_ideal_pitch(fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy) + fm_rotx, qy = fm_ideal_pitch( + fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy + ) self.qy = qy self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3) @@ -642,778 +618,6 @@ class DigitalTwin(BECWidget, QWidget): energy = self.input.energy.value() self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3) -class InputPanel(QWidget): - """Right-side control panel: input field, indicator, send, recording.""" - - def __init__(self, parent=None): - super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore - - # Adapt to reality - self.adapt_reality = Button(label_button='Adapt to reality', enabled=True) - - # Energy - self.energy = InputNumberField('energy', 'Energy', unit='eV', init=8979, decimals=0, single_step=100, ll=4000, hl=65000) - - # FE Slits Acceptance - self.sldi_hacc = InputNumberField('h_acc', 'Horizontal', unit='mrad', prefix='±', init=0.25, decimals=3, single_step=0.01, ll=-0.1, hl=0.9) - self.sldi_vacc = InputNumberField('v_acc', 'Vertical', unit='mrad', prefix='±', init=0.1, decimals=3, single_step=0.01, ll=-0.1, hl=0.5) - self.sldi_ass_group = Group( - 'FE Slits Acceptance', - [ - self.sldi_hacc, - self.sldi_vacc, - ] - ) - - # Collimating mirror - self.cm_stripe = ComboBox('cm_stripe', 'Stripe', ['Si', 'Rh', 'Pt']) - self.cm_pitch = InputNumberField('cm_pitch', 'Pitch', unit='mrad', init=-2.391, decimals=3, single_step=0.01, ll=-4.6, hl=-1.2) - self.cm_pitch_critical = NumberIndicator('Critical Pitch', 'mrad', decimals=3) - self.cm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0) - self.cm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0) - self.cm_ass_group = Group( - 'Collimating Mirror', - [ - self.cm_stripe, - self.cm_pitch, - self.cm_pitch_critical, - self.cm_refl, - self.cm_refl_harm, - ] - ) - - # Monochromator - self.mo1_mode = ComboBox('mo1_mode', 'Mode', ['Monochromatic', 'Pinkbeam']) - self.mo1_xtal = ComboBox('mo1_xtal', 'Crystal', ['Si(111)', 'Si(311)']) - self.mo1_bragg_angle = NumberIndicator('Bragg Angle', 'deg', decimals=1) - self.mo1_eres = NumberIndicator('Energy Resolution', 'eV', decimals=2) - self.mo1_ass_group = Group( - 'Monochromator', - [ - self.mo1_mode, - self.mo1_xtal, - self.mo1_bragg_angle, - self.mo1_eres, - ] - ) - - # Focusing Mirror - self.fm_stripe = ComboBox('fm_stripe', 'Stripe', ['Rh (toroid)', 'Rh (flat)', 'Pt (toroid)', 'Pt (flat)']) - self.fm_focus = ComboBox('fm_focus', 'Focus Type', ['Manual', 'Focused', 'Defocused']) - self.fm_rotx = InputNumberField('fm_rotx', 'Incidence Angle', unit='mrad', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2) - self.fm_focx = InputNumberField('fm_focx', 'Beam Size Horizontal', unit='mm', init=1, decimals=1, single_step=0.1, ll=0, hl=30) - self.fm_focy = InputNumberField('fm_focy', 'Beam Size Vertical', unit='mm', init=1, decimals=1, single_step=0.1, ll=0, hl=10) - self.fm_rotx_ideal = NumberIndicator('Incidence Angle for focused beam', 'mrad', decimals=3) - self.fm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0) - self.fm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0) - self.fm_ass_group = Group( - 'Focusing Mirror', - [ - self.fm_stripe, - self.fm_focus, - self.fm_rotx, - self.fm_focx, - self.fm_focy, - self.fm_rotx_ideal, - self.fm_refl, - self.fm_refl_harm, - ] - ) - - # Sample - self.cm_fm_harm_suppr = NumberIndicator('Total Suppression Factor at x eV', '', decimals=0) - self.smpl = InputNumberField('smpl', 'Sample Position', unit='mm', init=23511, decimals=0, single_step=100, ll=23000, hl=30000) - - # Assemble complete assitant group - self.input_group = Group( - 'User Input', - [ - self.adapt_reality, - self.energy, - self.sldi_ass_group, - self.cm_ass_group, - self.mo1_ass_group, - self.fm_ass_group, - self.cm_fm_harm_suppr, - self.smpl, - ] - ) - - self._layout .addWidget(self.input_group) - self._layout .addStretch() - -class SettingsPanel(QWidget): - """Right-side control panel: input field, indicator, send, recording.""" - - def __init__(self, parent=None): - super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore - - # Reload offsets - self.reload_offsets = Button(label='Reload Offsets', label_button='Reload', enabled=True) - self.unload_offsets = Button(label='Unload Offsets', label_button='Unload', enabled=True) - - # Assemble complete offset group - self.offset_group = Group( - 'Axes Offsets', - [ - self.reload_offsets, - self.unload_offsets, - ] - ) - - self._layout .addWidget(self.offset_group) - self._layout .addStretch() - -class PositionsPanel(QWidget): - """Right-side control panel: input field, indicator, send, recording.""" - - def __init__(self, parent=None): - super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore - - # FE Slits - self.sldi_gapx = NumberIndicator('GAPX', 'mm', decimals=2) - self.sldi_gapy = NumberIndicator('GAPY', 'mm', decimals=2) - self.sldi_pos_group = Group( - 'FE Slits', - [ - self.sldi_gapx, - self.sldi_gapy, - ] - ) - - # Collimating mirror - self.cm_trx = NumberIndicator('TRX', 'mm', decimals=2) - self.cm_try = NumberIndicator('TRY', 'mm', decimals=2) - self.cm_bnd = NumberIndicator('BENDER', 'km', decimals=2) - self.cm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3) - self.cm_pos_group = Group( - 'Collimating Mirror', - [ - self.cm_trx, - self.cm_try, - self.cm_bnd, - self.cm_rotx, - ] - ) - - # Monochromator - self.mo1_bragg_angle = NumberIndicator('Bragg Angle', 'deg', decimals=3) - self.mo1_trx = NumberIndicator('TRX', 'mm', decimals=2) - self.mo1_try = NumberIndicator('TRY', 'mm', decimals=2) - self.mo1_pos_group = Group( - 'Monochromator', - [ - self.mo1_bragg_angle, - self.mo1_trx, - self.mo1_try, - ] - ) - - # OP Slits 1 - self.sl1_centery = NumberIndicator('CENTERY', 'mm', decimals=2) - self.sl1_pos_group = Group( - 'OP Slits 1', - [ - self.sl1_centery, - ] - ) - - # OP Beam Monitor 1 - self.bm1_try = NumberIndicator('TRY', 'mm', decimals=2) - self.bm1_pos_group = Group( - 'OP Beam Monitor 1', - [ - self.bm1_try, - ] - ) - - # Focusing Mirror - self.fm_trx = NumberIndicator('TRX', 'mm', decimals=2) - self.fm_try = NumberIndicator('TRY', 'mm', decimals=2) - self.fm_bnd = NumberIndicator('BENDER', 'km', decimals=2) - self.fm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3) - self.fm_pos_group = Group( - 'Focusing Mirror', - [ - self.fm_trx, - self.fm_try, - self.fm_bnd, - self.fm_rotx, - ] - ) - - # OP Slits 2 - self.sl2_centery = NumberIndicator('CENTERY', 'mm', decimals=2) - self.sl2_pos_group = Group( - 'OP Slits 2', - [ - self.sl2_centery, - ] - ) - - # OP Beam Monitor 2 - self.bm2_try = NumberIndicator('TRY', 'mm', decimals=2) - self.bm2_pos_group = Group( - 'OP Beam Monitor 2', - [ - self.bm2_try, - ] - ) - - # Optical Table - self.ot_try = NumberIndicator('TRY', 'mm', decimals=2) - self.ot_rotx = NumberIndicator('ROTX', 'mrad', decimals=3) - self.ot_es1_trz = NumberIndicator('ES1 TRZ', 'mm', decimals=0) - self.ot_pos_group = Group( - 'Optical Table', - [ - self.ot_try, - self.ot_rotx, - self.ot_es1_trz, - ] - ) - - # Assemble complete assitant group - self.position_group = Group( - 'Axes Positions Calculator', - [ - self.sldi_pos_group, - self.cm_pos_group, - self.mo1_pos_group, - self.sl1_pos_group, - self.bm1_pos_group, - self.fm_pos_group, - self.sl2_pos_group, - self.bm2_pos_group, - self.ot_pos_group, - ] - ) - - self._layout .addWidget(self.position_group) - self._layout .addStretch() - -class MoverPanel(QWidget): - - def __init__(self, dev, parent=None): - super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore - - self.mover_widgets = [] - - # FE Slits - self.sldi_gapx = MoveWidget(dev=dev, motor='sldi_gapx', label='GAPX', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.sldi_gapx) - - self.sldi_gapy = MoveWidget(dev=dev, motor='sldi_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.sldi_gapy) - - self.sldi_mov_group = Group( - 'FE Slits', - [ - self.sldi_gapx, - self.sldi_gapy, - ] - ) - - # Absorber - self.abs = AbsorberWidget(absorber=dev.abs, label='') - - self.abs_group = Group( - 'Absorber', - [ - self.abs, - ] - ) - - # Collimating mirror - self.cm_trx = MoveWidget(dev=dev, motor='cm_trx', label='TRX', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.cm_trx) - - self.cm_try = MoveWidget(dev=dev, motor='cm_try', label='TRY', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.cm_try) - - self.cm_bnd = MoveWidget(dev=dev, motor='cm_bnd', label='BENDER', unit='km', decimals=2, deadband=0.2) - self.mover_widgets.append(self.cm_bnd) - - self.cm_rotx = MoveWidget(dev=dev, motor='cm_rotx', label='PITCH', unit='mrad', decimals=3, deadband=0.01) - self.mover_widgets.append(self.cm_rotx) - - self.cm_mov_group = Group( - 'Collimating Mirror', - [ - self.cm_trx, - self.cm_try, - self.cm_bnd, - self.cm_rotx, - ] - ) - - # Monochromator - self.mo1_bragg_angle = MoveWidget(dev=dev, motor='mo1_bragg_angle', label='Bragg Angle', unit='deg', decimals=3, deadband=0.01) - self.mover_widgets.append(self.mo1_bragg_angle) - - self.mo1_trx = MoveWidget(dev=dev, motor='mo1_trx', label='TRX', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.mo1_trx) - - self.mo1_try = MoveWidget(dev=dev, motor='mo1_try', label='TRY', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.mo1_try) - - self.mo1_mov_group = Group( - 'Monochromator', - [ - self.mo1_bragg_angle, - self.mo1_trx, - self.mo1_try, - ] - ) - - # OP Slits 1 - self.sl1_centery = MoveWidget(dev=dev, motor='sl1_centery', label='CENTERY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.sl1_centery) - - self.sl1_gapy = MoveWidget(dev=dev, motor='sl1_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.sl1_gapy) - - self.sl1_mov_group = Group( - 'OP Slits 1', - [ - self.sl1_centery, - self.sl1_gapy, - ] - ) - - # OP Beam Monitor 1 - self.bm1_try = MoveWidget(dev=dev, motor='bm1_try', label='TRY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.bm1_try) - - self.bm1_mov_group = Group( - 'OP Beam Monitor 1', - [ - self.bm1_try, - ] - ) - - # Focusing Mirror - self.fm_trx = MoveWidget(dev=dev, motor='fm_trx', label='TRX', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.fm_trx) - - self.fm_try = MoveWidget(dev=dev, motor='fm_try', label='TRY', unit='mm', decimals=2, deadband=0.01) - self.mover_widgets.append(self.fm_try) - - self.fm_bnd = MoveWidget(dev=dev, motor='fm_bnd', label='BENDER', unit='km', decimals=2, deadband=0.2) - self.mover_widgets.append(self.fm_bnd) - - self.fm_rotx = MoveWidget(dev=dev, motor='fm_rotx', label='PITCH', unit='mrad', decimals=3, deadband=0.01) - self.mover_widgets.append(self.fm_rotx) - - self.fm_roty = MoveWidget(dev=dev, motor='fm_roty', label='YAW', unit='mrad', decimals=3, deadband=0.01) - self.mover_widgets.append(self.fm_roty) - - self.fm_rotz = MoveWidget(dev=dev, motor='fm_rotz', label='ROLL', unit='mrad', decimals=3, deadband=0.01) - self.mover_widgets.append(self.fm_rotz) - - self.fm_mov_group = Group( - 'Focusing Mirror', - [ - self.fm_trx, - self.fm_try, - self.fm_bnd, - self.fm_rotx, - self.fm_roty, - self.fm_rotz, - ] - ) - - # OP Slits 2 - self.sl2_centery = MoveWidget(dev=dev, motor='sl2_centery', label='CENTERY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.sl2_centery) - - self.sl2_gapy = MoveWidget(dev=dev, motor='sl2_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.sl2_gapy) - - self.sl2_mov_group = Group( - 'OP Slits 2', - [ - self.sl2_centery, - self.sl2_gapy, - ] - ) - - # OP Beam Monitor 2 - self.bm2_try = MoveWidget(dev=dev, motor='bm2_try', label='TRY', unit='mm', decimals=2, deadband=0.1) - self.mover_widgets.append(self.bm2_try) - - self.bm2_mov_group = Group( - 'OP Beam Monitor 2', - [ - self.bm2_try, - ] - ) - - # Optical Table - self.ot_try = MoveWidget(dev=dev, motor='ot_try', label='TRY', unit='mm', decimals=2, deadband=0.2) - self.mover_widgets.append(self.ot_try) - - self.ot_rotx = MoveWidget(dev=dev, motor='ot_rotx', label='ROTX', unit='mrad', decimals=3, deadband=0.05) - self.mover_widgets.append(self.ot_rotx) - - self.ot_mov_group = Group( - 'Optical Table', - [ - self.ot_try, - self.ot_rotx, - ] - ) - - # Experimental Station 0 - self.es0wi_try = MoveWidget(dev=dev, motor='es0wi_try', label='ES0 WI', unit='mm', decimals=0, deadband=0.1) - self.mover_widgets.append(self.es0wi_try) - - self.es0_mov_group = Group( - 'Expperimental Station 0', - [ - self.es0wi_try, - ] - ) - - # Experimental Station 1 - self.ot_es1_trz = MoveWidget(dev=dev, motor='ot_es1_trz', label='ES1 TRZ', unit='mm', decimals=0, deadband=5) - self.mover_widgets.append(self.ot_es1_trz) - - self.es1_mov_group = Group( - 'Expperimental Station 1', - [ - self.ot_es1_trz, - ] - ) - - # Assemble complete mover group - self.mover_group = Group( - 'Mover', - [ - self.sldi_mov_group, - self.abs_group, - self.cm_mov_group, - self.mo1_mov_group, - self.sl1_mov_group, - self.bm1_mov_group, - self.fm_mov_group, - self.sl2_mov_group, - self.bm2_mov_group, - self.ot_mov_group, - self.es0_mov_group, - self.es1_mov_group, - ] - ) - - self._layout .addWidget(self.mover_group) - self._layout .addStretch() - - def apply_theme(self, theme): - for widget in self.mover_widgets: - widget.apply_theme(theme) - -class SurfacePlots(QWidget): - """Plot widget with two curves and legend.""" - - def __init__(self, parent=None): - super().__init__(parent=parent) - self._layout = QHBoxLayout(self) - - self.surfaces = { - 'assistant': { - 'cm': {'x': [], 'y': []}, - 'mo1_1': {'x': [], 'y': []}, - 'mo1_2': {'x': [], 'y': []}, - 'fm': {'x': [], 'y': []}, - }, - 'reality': { - 'cm': {'x': [], 'y': []}, - 'mo1_1': {'x': [], 'y': []}, - 'mo1_2': {'x': [], 'y': []}, - 'fm': {'x': [], 'y': []}, - }, - } - - self.plots = { - 'fm': {}, - 'mo1_2': {}, - 'mo1_1': {}, - 'cm': {}, - } - - self.color_impenetrable = (0, 0, 0) - self.colors = [(255, 255, 0), (255, 0, 255)] - self.text_color = (255, 255, 255) - - # Create plot widgets - for name, widget in self.plots.items(): - plot_widget = pg.PlotWidget() - plot_widget.getAxis('bottom').enableAutoSIPrefix(False) - - plot_group = Group( - 'Surface ' + name, - [ - plot_widget, - ] - ) - - plot_widget.setLabel('left', 'Z [mm]') - plot_widget.setLabel('bottom', 'X [mm]') - plot_widget.setMouseEnabled(x=False, y=False) - plot_widget.setMenuEnabled(False) - plot_widget.hideButtons() - - widget['widget'] = plot_widget - self._layout.addWidget(plot_group) - - # Create surfaces - for idx, scene in enumerate(self.surfaces): - for name, _ in self.surfaces[scene].items(): - 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) - z_value = 2 - else: - brush = QBrush(QColor(*self.colors[idx], 255)) - pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1) - z_value = 1 - widget = self.plots[name] - self.plots[name][scene] = widget['widget'].plot( - [], - [], - pen=pen, - name=scene, - brush=brush, - fillLevel=0, - ) - self.plots[name][scene].setZValue(z_value) - - self.walls = [] - self.texts = [] - - self.plot_walls() - - self.apply_theme() - - def apply_theme(self, theme=None): - - if theme is None: - app = QApplication.instance() - theme = app.theme.theme # type: ignore - - bg_color = pg.getConfigOption("background") - fg_color = pg.getConfigOption("foreground") - for _, plot in self.plots.items(): - # Background - plot['widget'].setBackground(bg_color) - # Axes (tick marks, tick labels, axis line) - for axis in ["left", "bottom", "right", "top"]: - ax = plot['widget'].getAxis(axis) - ax.setPen(pg.mkPen(color=fg_color)) - ax.setTextPen(pg.mkPen(color=fg_color)) - - if theme == "light": - self.color_impenetrable = (30, 30, 30) - self.colors = [(79, 163, 224), (240, 128, 60)] - self.text_color = (255, 255, 255) - else: # dark theme - 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.surfaces): - for name, _ in self.surfaces[scene].items(): - 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) - else: - brush = QBrush(QColor(*self.colors[idx], 255)) - pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0) - self.plots[name][scene].setPen(pen) - self.plots[name][scene].setBrush(brush) - - for wall in self.walls: - wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) - wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 - - for text in self.texts: - text.setColor(self.text_color) - - def plot_walls(self): - - def plot_surface(widget, surfaces): - for name, surface in surfaces.items(): - rect = pg.QtWidgets.QGraphicsRectItem(*surface) # pylint: disable=E1101 - rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 - rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) - widget.addItem(rect) - text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5)) - widget.addItem(text) - text.setPos(surface[0]+surface[2]/2, surface[1]+surface[3]/2) - text.setZValue(10) - self.walls.append(rect) - self.texts.append(text) - - for name, plot in self.plots.items(): - if name in 'cm': - plot_surface(plot['widget'], mirror_surface_geometries('cm')) - elif name in 'mo1_1': - plot_surface(plot['widget'], mo_surface_geometries ('mo1', 0)) - elif name in 'mo1_2': - plot_surface(plot['widget'], mo_surface_geometries ('mo1', 1)) - elif name in 'fm': - plot_surface(plot['widget'], mirror_surface_geometries('fm_flat')) - plot_surface(plot['widget'], mirror_surface_geometries('fm_toroid')) - else: - raise Exception(f'Plot {name} not found!') - for name, plot in self.plots.items(): - plot['widget'].disableAutoRange() - - def update_surfaces(self, scene, data): - self.surfaces[scene] = data - for name, device in self.surfaces[scene].items(): - plot = self.plots[name][scene] - x = np.array(device['x'] + [device['x'][0]]) if len(device['x']) != 0 else np.array([]) - y = np.array(device['y'] + [device['y'][0]]) if len(device['y']) != 0 else np.array([]) - plot.setData(x=x, y=y) - -class SideviewPlot(QWidget): - """Plot widget with two curves and legend.""" - - def __init__(self, parent=None): - super().__init__(parent=parent) - self._layout = QVBoxLayout(self) - # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore - - self.plot_widget = pg.PlotWidget() - self.plot_widget.getAxis('bottom').enableAutoSIPrefix(False) - self.plot_widget.invertX(True) - self.plot_widget.addLegend() - - self.color_impenetrable = (0, 0, 0) - self.colors = [(255, 255, 0), (255, 0, 255)] - - self.data = { - 'assistant': {'x': [0, 1000, 2000], 'y': [0, 20, 30]}, - 'reality': {'x': [0, 1000, 2000], 'y': [0, 15, 50]}, - } - - self.plots = {} - - self.pipes = [] - self.walls = [] - - for idx, scene in enumerate(self.data.keys()): - if scene in "assistant": - pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DotLine) - z_value = 2 - else: - pen = pg.mkPen(color=self.colors[idx], width=2) - z_value = 1 - self.plots[scene] = self.plot_widget.plot( - [], - [], - pen=pen, - name=scene, - ) - self.plots[scene].setZValue(z_value) - - self.plot_group = Group( - 'Side View', - [ - self.plot_widget, - ] - ) - - 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.apply_theme() - - def apply_theme(self, theme=None): - - if theme is None: - app = QApplication.instance() - theme = app.theme.theme # type: ignore - - bg_color = pg.getConfigOption("background") - fg_color = pg.getConfigOption("foreground") - # Background - self.plot_widget.setBackground(bg_color) - # Axes (tick marks, tick labels, axis line) - for axis in ["left", "bottom", "right", "top"]: - ax = self.plot_widget.getAxis(axis) - ax.setPen(pg.mkPen(color=fg_color)) - ax.setTextPen(pg.mkPen(color=fg_color)) - - if theme == "light": - self.color_impenetrable = (30, 30, 30) - self.colors = [(79, 163, 224), (240, 128, 60)] - self.text_color = (255, 255, 255) - else: # dark theme - 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=3, style=Qt.DotLine) - else: - brush = QBrush(QColor(*self.colors[idx], 255)) - 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=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=3)) - - def plot_vacuum_pipes(self): - pipes = pipe_geometries() - for pipe in pipes: - self.pipes.append(self.plot_widget.plot( - x=pipe['x'], - y=pipe['y'], - pen=pg.mkPen(color=self.color_impenetrable, width=2), - )) - - def plot_walls(self): - walls = wall_geometries() - for wall in walls: - rect = pg.QtWidgets.QGraphicsRectItem(*wall) # pylint: disable=E1101 - 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) - self.walls.append(rect) - - def update_curves(self, scene, data): - self.data[scene] = data - plot = self.plots[scene] - plot.setData(x=self.data[scene]['x'], y=self.data[scene]['y']) - if __name__ == "__main__": from bec_widgets.utils.bec_dispatcher import BECDispatcher diff --git a/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py new file mode 100644 index 0000000..99a29be --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py @@ -0,0 +1,174 @@ +""" +Panel for user inputs of the digital twin widget +""" + +# pylint: disable=E0611 +from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget + +from debye_bec.bec_widgets.widgets.qt_widgets import ( + Button, + ComboBox, + Group, + InputNumberField, + NumberIndicator, +) + + +class InputPanel(QWidget): + """Right-side control panel: input field, indicator, send, recording.""" + + def __init__(self, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + # Adapt to reality + self.adapt_reality = Button(label_button="Adapt to reality", enabled=True) + + # Energy + self.energy = InputNumberField( + "energy", "Energy", unit="eV", init=8979, decimals=0, single_step=100, ll=4000, hl=65000 + ) + + # FE Slits Acceptance + self.sldi_hacc = InputNumberField( + "h_acc", + "Horizontal", + unit="mrad", + prefix="±", + init=0.25, + decimals=3, + single_step=0.01, + ll=-0.1, + hl=0.9, + ) + self.sldi_vacc = InputNumberField( + "v_acc", + "Vertical", + unit="mrad", + prefix="±", + init=0.1, + decimals=3, + single_step=0.01, + ll=-0.1, + hl=0.5, + ) + self.sldi_ass_group = Group("FE Slits Acceptance", [self.sldi_hacc, self.sldi_vacc]) + + # Collimating mirror + self.cm_stripe = ComboBox("cm_stripe", "Stripe", ["Si", "Rh", "Pt"]) + self.cm_pitch = InputNumberField( + "cm_pitch", + "Pitch", + unit="mrad", + init=-2.391, + decimals=3, + single_step=0.01, + ll=-4.6, + hl=-1.2, + ) + self.cm_pitch_critical = NumberIndicator("Critical Pitch", "mrad", decimals=3) + self.cm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0) + self.cm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0) + self.cm_ass_group = Group( + "Collimating Mirror", + [ + self.cm_stripe, + self.cm_pitch, + self.cm_pitch_critical, + self.cm_refl, + self.cm_refl_harm, + ], + ) + + # Monochromator + self.mo1_mode = ComboBox("mo1_mode", "Mode", ["Monochromatic", "Pinkbeam"]) + self.mo1_xtal = ComboBox("mo1_xtal", "Crystal", ["Si(111)", "Si(311)"]) + self.mo1_bragg_angle = NumberIndicator("Bragg Angle", "deg", decimals=1) + self.mo1_eres = NumberIndicator("Energy Resolution", "eV", decimals=2) + self.mo1_ass_group = Group( + "Monochromator", [self.mo1_mode, self.mo1_xtal, self.mo1_bragg_angle, self.mo1_eres] + ) + + # Focusing Mirror + self.fm_stripe = ComboBox( + "fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"] + ) + self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"]) + self.fm_rotx = InputNumberField( + "fm_rotx", + "Incidence Angle", + unit="mrad", + init=-2.391, + decimals=3, + single_step=0.01, + ll=-10, + hl=2, + ) + self.fm_focx = InputNumberField( + "fm_focx", + "Beam Size Horizontal", + unit="mm", + init=1, + decimals=1, + single_step=0.1, + ll=0, + hl=30, + ) + self.fm_focy = InputNumberField( + "fm_focy", + "Beam Size Vertical", + unit="mm", + init=1, + decimals=1, + single_step=0.1, + ll=0, + hl=10, + ) + self.fm_rotx_ideal = NumberIndicator("Incidence Angle for focused beam", "mrad", decimals=3) + self.fm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0) + self.fm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0) + self.fm_ass_group = Group( + "Focusing Mirror", + [ + self.fm_stripe, + self.fm_focus, + self.fm_rotx, + self.fm_focx, + self.fm_focy, + self.fm_rotx_ideal, + self.fm_refl, + self.fm_refl_harm, + ], + ) + + # Sample + self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0) + self.smpl = InputNumberField( + "smpl", + "Sample Position", + unit="mm", + init=23511, + decimals=0, + single_step=100, + ll=23000, + hl=30000, + ) + + # Assemble complete assitant group + self.input_group = Group( + "User Input", + [ + self.adapt_reality, + self.energy, + self.sldi_ass_group, + self.cm_ass_group, + self.mo1_ass_group, + self.fm_ass_group, + self.cm_fm_harm_suppr, + self.smpl, + ], + ) + + self._layout.addWidget(self.input_group) + self._layout.addStretch() 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 7834a29..01cd73e 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -1,28 +1,37 @@ -import time import random import threading +import time + +from bec_lib import bec_logger # import qtawesome as qta from bec_qthemes import material_icon from bec_widgets.utils.colors import get_accent_colors -from bec_lib import bec_logger +from qtpy.QtCore import Property, QObject, QPropertyAnimation, Qt, QThread, Signal +from qtpy.QtGui import QTransform +from qtpy.QtWidgets import ( + QApplication, + QDoubleSpinBox, + QFrame, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, +) from debye_bec.devices.absorber import STATUS as ABS_STATUS -from qtpy.QtCore import Qt, QThread, Signal, QObject, Property, QPropertyAnimation -from qtpy.QtWidgets import ( - QGroupBox, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, - QDoubleSpinBox, QFrame, QWidget, QApplication -) -from qtpy.QtGui import QTransform - logger = bec_logger.logger + class Status: - IN_POSITION = "in_position" # green mdi.check-circle + IN_POSITION = "in_position" # green mdi.check-circle NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle - MOVING = "moving" # blue mdi.loading (spinning) - ERROR = "error" # red mdi.alert-circle + MOVING = "moving" # blue mdi.loading (spinning) + ERROR = "error" # red mdi.alert-circle + class StatusIcon(QWidget): """ @@ -33,10 +42,10 @@ class StatusIcon(QWidget): ICON_SIZE = 20 _ICON_MAP = { - Status.IN_POSITION: ("check_circle", "#27ae60"), + Status.IN_POSITION: ("check_circle", "#27ae60"), Status.NOT_IN_POSITION: ("cancel", "#e6d922"), - Status.ERROR: ("warning", "#e74c3c"), - Status.MOVING: ("cycle", "#2980b9"), + Status.ERROR: ("warning", "#e74c3c"), + Status.MOVING: ("cycle", "#2980b9"), } def __init__(self, parent=None): @@ -76,7 +85,9 @@ class StatusIcon(QWidget): self._status = status icon_name, color = self._ICON_MAP[status] - icon = material_icon(icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True) + icon = material_icon( + icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True + ) self._current_pixmap_base = icon if status == Status.MOVING: @@ -85,14 +96,16 @@ class StatusIcon(QWidget): self._spin_anim.stop() self._label.setPixmap(icon) + class MotionWorker(QObject): """ Executes motion on the specified motor and includes some safety during motion for certain motors. """ + position_changed = Signal(float) - error = Signal(bool) # True = error - finished = Signal(bool) # True = reached target, False = stopped + error = Signal(bool) # True = error + finished = Signal(bool) # True = reached target, False = stopped def __init__(self, dev, motor, target_pos: float): super().__init__() @@ -104,101 +117,104 @@ class MotionWorker(QObject): def stop(self): self._stop_flag.set() - # def run(self): - # logger.info(f'Would run motor {self.motor}') - # simulated_run_time = 3 - # start = time.time() - # while (time.time() - start) < simulated_run_time: - # if self._stop_flag.is_set(): - # break - # time.sleep(0.01) - - # # self.motor.move(self._target, relative=False) - # # while self.motor.motor_is_moving.get(): - # # if self._stop_flag.is_set(): - # # self.motor.motor_stop() - # # self.position_changed.emit(self.motor.read[self.name]['value']) - # # time.sleep(0.1) - # self.finished.emit(True) - def run(self): match self.motor: - case 'sldi_gapx' | 'sldi_gapy' | 'sldi_centerx' | 'sldi_centery': + case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery": self.motion() - case 'cm_trx': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['cm_roty'], 'abs_tol': 0.05} - ]) - case 'cm_roty': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['cm_trx'], 'abs_tol': 0.05} - ]) - case 'cm_try': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['cm_rotx'], 'abs_tol': 0.05}, - {'device': self.dev['cm_rotz'], 'abs_tol': 0.05}, - ]) - case 'cm_rotx': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['cm_try'], 'abs_tol': 0.05}, - {'device': self.dev['cm_rotz'], 'abs_tol': 0.05}, - ]) - case 'cm_rotz': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['cm_try'], 'abs_tol': 0.05}, - {'device': self.dev['cm_rotx'], 'abs_tol': 0.05}, - ]) - case 'cm_bnd': - p1 = (1/(self.dev.cm_bnd_radius.read()['cm_bnd_radius']['value']*1e3) + 0.0284)/2e-6 - p2 = (1/(self._target*1e3) + 0.0284)/2e-6 - self._target = p2 - p1 - self.motion(relative=True, rb= - {'device': self.dev['cm_bnd_radius']} + case "cm_trx": + self.motion( + abs_closed=True, + surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}], ) - case 'mo1_try' | 'mo1_trx' | 'mo1_roty': + case "cm_roty": + self.motion( + abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}] + ) + case "cm_try": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["cm_rotx"], "abs_tol": 0.05}, + {"device": self.dev["cm_rotz"], "abs_tol": 0.05}, + ], + ) + case "cm_rotx": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["cm_try"], "abs_tol": 0.05}, + {"device": self.dev["cm_rotz"], "abs_tol": 0.05}, + ], + ) + case "cm_rotz": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["cm_try"], "abs_tol": 0.05}, + {"device": self.dev["cm_rotx"], "abs_tol": 0.05}, + ], + ) + case "cm_bnd": + p1 = ( + 1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284 + ) / 2e-6 + p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6 + self._target = p2 - p1 + self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]}) + case "mo1_try" | "mo1_trx" | "mo1_roty": self.motion(abs_closed=True) - case 'mo1_bragg_angle': + case "mo1_bragg_angle": self.motion() - case 'sl1_centery' | 'sl1_gapy' | 'bm1_try': + case "sl1_centery" | "sl1_gapy" | "bm1_try": self.motion() - case 'fm_trx': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['fm_roty'], 'abs_tol': 0.05} - ]) - case 'fm_roty': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['fm_trx'], 'abs_tol': 0.05} - ]) - case 'fm_try': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['fm_rotx'], 'abs_tol': 0.05}, - {'device': self.dev['fm_rotz'], 'abs_tol': 0.05}, - ]) - case 'fm_rotx': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['fm_try'], 'abs_tol': 0.05}, - {'device': self.dev['fm_rotz'], 'abs_tol': 0.05}, - ]) - case 'fm_rotz': - self.motion(abs_closed=True, surveyed_axes=[ - {'device': self.dev['fm_try'], 'abs_tol': 0.05}, - {'device': self.dev['fm_rotx'], 'abs_tol': 0.05}, - ]) - case 'fm_bnd': - p1 = (1/(self.dev.fm_bnd_radius.read()['fm_bnd_radius']['value']*1e3) + 4.28e-5)/1.84e-9 - p2 = (1/(self._target*1e3) + 4.28e-5)/1.84e-9 - self._target = p2 - p1 - self.motion(relative=True, rb= - {'device': self.dev['fm_bnd_radius']} + case "fm_trx": + self.motion( + abs_closed=True, + surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}], ) - case 'sl2_centery' | 'sl2_gapy' | 'bm2_try': + case "fm_roty": + self.motion( + abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}] + ) + case "fm_try": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["fm_rotx"], "abs_tol": 0.05}, + {"device": self.dev["fm_rotz"], "abs_tol": 0.05}, + ], + ) + case "fm_rotx": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["fm_try"], "abs_tol": 0.05}, + {"device": self.dev["fm_rotz"], "abs_tol": 0.05}, + ], + ) + case "fm_rotz": + self.motion( + abs_closed=True, + surveyed_axes=[ + {"device": self.dev["fm_try"], "abs_tol": 0.05}, + {"device": self.dev["fm_rotx"], "abs_tol": 0.05}, + ], + ) + case "fm_bnd": + p1 = ( + 1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3) + 4.28e-5 + ) / 1.84e-9 + p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9 + self._target = p2 - p1 + self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]}) + case "sl2_centery" | "sl2_gapy" | "bm2_try": self.motion() - case 'ot_try' | 'ot_rotx' | 'ot_es1_trz': + case "ot_try" | "ot_rotx" | "ot_es1_trz": self.motion() case _: - logger.warning(f'Motor {self.motor} not integrated in digital twin!') + logger.warning(f"Motor {self.motor} not integrated in digital twin!") - def motion(self, abs_closed=False, relative=False, rb=None, surveyed_axes = None): + def motion(self, abs_closed=False, relative=False, rb=None, surveyed_axes=None): """ Moves an axis while surverying a set of axes (if set). Example surveyed_axes: @@ -215,30 +231,37 @@ class MotionWorker(QObject): status.wait(timeout=5) if surveyed_axes is not None: for surv_ax in surveyed_axes: - surv_ax['name'] = surv_ax['device'].dotted_name - surv_ax['old_value'] = surv_ax['device'].read(cached=True)[surv_ax['name']]['value'] + surv_ax["name"] = surv_ax["device"].dotted_name + surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"] if rb is not None: - rb['name'] = rb['device'].dotted_name - self.dev[self.motor].move(self._target, relative=relative) - time.sleep(0.5) - while self.dev[self.motor].motor_is_moving.get(): + rb["name"] = rb["device"].dotted_name + status = self.dev[self.motor].move(self._target, relative=relative) + last_check = time.time() + update_interval = 0.1 + while status.status == "RUNNING": + now = time.time() + if time.time() - last_check < update_interval: + time.sleep(0.01) + last_check = now if self._stop_flag.is_set(): self.dev[self.motor].stop() self._stop_flag.clear() if rb is not None: - self.position_changed.emit(rb['device'].read(cached=True)[rb['name']]['value']) + self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"]) else: - self.position_changed.emit(self.dev[self.motor].read(cached=True)[self.motor]['value']) + self.position_changed.emit( + self.dev[self.motor].read(cached=True)[self.motor]["value"] + ) if surveyed_axes is not None: for surv_ax in surveyed_axes: - fb = surv_ax['device'].read(cached=True)[surv_ax['name']]['value'] - if abs(fb - surv_ax['old_value']) > surv_ax['abs_tol']: + fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"] + if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]: self.dev[self.motor].stop() self.error.emit(1) break - time.sleep(0.1) self.finished.emit(True) + class MoveWidget(QWidget): """ One motor stage control group containing: @@ -248,7 +271,7 @@ class MoveWidget(QWidget): - Start / Stop button """ - def __init__(self, dev, motor, label: str = '', unit=None, decimals=3, deadband=0.0): + def __init__(self, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0): super().__init__() self.fb = 0.0 self.target = 0 @@ -276,12 +299,12 @@ class MoveWidget(QWidget): layout.addWidget(self.label) # Target - self.target_label = QLabel('-') + self.target_label = QLabel("-") self.target_label.setFixedWidth(100) layout.addWidget(self.target_label) # Feedback - self.fb_label = QLabel('-') + self.fb_label = QLabel("-") self.fb_label.setFixedWidth(100) layout.addWidget(self.fb_label) @@ -297,7 +320,7 @@ class MoveWidget(QWidget): self.btn_action.setFixedHeight(20) self.btn_action.clicked.connect(self._on_button_clicked) layout.addWidget(self.btn_action) - self.btn_mode = 'start' + self.btn_mode = "start" self._apply_button_style("start") @@ -306,18 +329,18 @@ class MoveWidget(QWidget): def apply_theme(self, theme=None): if theme is None: app = QApplication.instance() - theme = app.theme.theme # type: ignore + theme = app.theme.theme # type: ignore if theme == "light": - self.text_color = {'target': (79, 163, 224), 'fb': (240, 128, 60)} - else: # dark theme - self.text_color = {'target': (26, 111, 173), 'fb': (212, 83, 10)} - r, g, b = self.text_color['target'] - self.target_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}') - r, g, b = self.text_color['fb'] - self.fb_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}') + self.text_color = {"target": (79, 163, 224), "fb": (240, 128, 60)} + else: # dark theme + self.text_color = {"target": (26, 111, 173), "fb": (212, 83, 10)} + r, g, b = self.text_color["target"] + self.target_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}") + r, g, b = self.text_color["fb"] + self.fb_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}") - if self.btn_mode == 'start': + if self.btn_mode == "start": self.btn_action.setStyleSheet( f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" ) @@ -328,18 +351,18 @@ class MoveWidget(QWidget): def set_target(self, target): self.target = target - text = f'{target:.{int(self.decimals)}f}' + text = f"{target:.{int(self.decimals)}f}" if self.unit is not None: - text = text + ' ' + self.unit + text = text + " " + self.unit self.target_label.setText(text) self._on_target_or_fb_changed() def set_feedback(self, fb): if self.status != Status.MOVING: self.fb = fb - text = f'{fb:.{int(self.decimals)}f}' + text = f"{fb:.{int(self.decimals)}f}" if self.unit is not None: - text = text + ' ' + self.unit + text = text + " " + self.unit self.fb_label.setText(text) self._on_target_or_fb_changed() @@ -348,13 +371,13 @@ class MoveWidget(QWidget): if mode == "start": self.btn_action.setText("Move") self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" - ) + f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" + ) else: # stop self.btn_action.setText("Stop") self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}" - ) + f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}" + ) def _set_status(self, status: str): self.status = status @@ -408,9 +431,9 @@ class MoveWidget(QWidget): def _on_position_changed(self, pos: float): self.fb = pos - text = f'{pos:.{int(self.decimals)}f}' + text = f"{pos:.{int(self.decimals)}f}" if self.unit is not None: - text = text + ' ' + self.unit + text = text + " " + self.unit self.fb_label.setText(text) def _on_motion_finished(self, reached: bool): @@ -437,12 +460,13 @@ class MoveWidget(QWidget): self._thread.quit() self._thread.wait(2000) # max 2 s grace period + class AbsorberWidget(QWidget): """ Control of the frontend absorber (only open) """ - def __init__(self, absorber, label: str = 'Absorber'): + def __init__(self, absorber, label: str = "Absorber"): super().__init__() self.absorber = absorber self.fb = False @@ -460,17 +484,17 @@ class AbsorberWidget(QWidget): layout.addWidget(self.label) # Blank - self.blank_label = QLabel('') + self.blank_label = QLabel("") self.blank_label.setFixedWidth(100) layout.addWidget(self.blank_label) # Feedback - self.fb_label = QLabel('-') + self.fb_label = QLabel("-") self.fb_label.setFixedWidth(100) layout.addWidget(self.fb_label) # Blank icon - self.blank_icon = QLabel('') + self.blank_icon = QLabel("") self.blank_icon.setFixedWidth(30) self.blank_icon.setContentsMargins(0, 0, 10, 0) layout.addWidget(self.blank_icon) @@ -485,15 +509,11 @@ class AbsorberWidget(QWidget): def set_feedback(self, fb: bool): self.fb = fb if fb: - self.fb_label.setText('Open') - self.fb_label.setStyleSheet( - f"QLabel {{color: {get_accent_colors().success.name()}}}" - ) + self.fb_label.setText("Open") + self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().success.name()}}}") else: - self.fb_label.setText('Closed') - self.fb_label.setStyleSheet( - f"QLabel {{color: {get_accent_colors().emergency.name()}}}" - ) + self.fb_label.setText("Closed") + self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().emergency.name()}}}") def enable_open(self, enable: bool = False): if enable: diff --git a/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py new file mode 100644 index 0000000..061ce27 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py @@ -0,0 +1,220 @@ +""" +Panel to move an axis to a certain position +""" + +# pylint: disable=E0611 +from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget + +from debye_bec.bec_widgets.widgets.digital_twin.move_widget import AbsorberWidget, MoveWidget +from debye_bec.bec_widgets.widgets.qt_widgets import Group + + +class MoverPanel(QWidget): + + def __init__(self, dev, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + self.mover_widgets = [] + + # FE Slits + self.sldi_gapx = MoveWidget( + dev=dev, motor="sldi_gapx", label="GAPX", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.sldi_gapx) + + self.sldi_gapy = MoveWidget( + dev=dev, motor="sldi_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.sldi_gapy) + + self.sldi_mov_group = Group("FE Slits", [self.sldi_gapx, self.sldi_gapy]) + + # Absorber + self.abs = AbsorberWidget(absorber=dev.abs, label="") + + self.abs_group = Group("Absorber", [self.abs]) + + # Collimating mirror + self.cm_trx = MoveWidget( + dev=dev, motor="cm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.cm_trx) + + self.cm_try = MoveWidget( + dev=dev, motor="cm_try", label="TRY", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.cm_try) + + self.cm_bnd = MoveWidget( + dev=dev, motor="cm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2 + ) + self.mover_widgets.append(self.cm_bnd) + + self.cm_rotx = MoveWidget( + dev=dev, motor="cm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01 + ) + self.mover_widgets.append(self.cm_rotx) + + self.cm_mov_group = Group( + "Collimating Mirror", [self.cm_trx, self.cm_try, self.cm_bnd, self.cm_rotx] + ) + + # Monochromator + self.mo1_bragg_angle = MoveWidget( + dev=dev, + motor="mo1_bragg_angle", + label="Bragg Angle", + unit="deg", + decimals=3, + deadband=0.01, + ) + self.mover_widgets.append(self.mo1_bragg_angle) + + self.mo1_trx = MoveWidget( + dev=dev, motor="mo1_trx", label="TRX", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.mo1_trx) + + self.mo1_try = MoveWidget( + dev=dev, motor="mo1_try", label="TRY", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.mo1_try) + + self.mo1_mov_group = Group( + "Monochromator", [self.mo1_bragg_angle, self.mo1_trx, self.mo1_try] + ) + + # OP Slits 1 + self.sl1_centery = MoveWidget( + dev=dev, motor="sl1_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.sl1_centery) + + self.sl1_gapy = MoveWidget( + dev=dev, motor="sl1_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.sl1_gapy) + + self.sl1_mov_group = Group("OP Slits 1", [self.sl1_centery, self.sl1_gapy]) + + # OP Beam Monitor 1 + self.bm1_try = MoveWidget( + dev=dev, motor="bm1_try", label="TRY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.bm1_try) + + self.bm1_mov_group = Group("OP Beam Monitor 1", [self.bm1_try]) + + # Focusing Mirror + self.fm_trx = MoveWidget( + dev=dev, motor="fm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.fm_trx) + + self.fm_try = MoveWidget( + dev=dev, motor="fm_try", label="TRY", unit="mm", decimals=2, deadband=0.01 + ) + self.mover_widgets.append(self.fm_try) + + self.fm_bnd = MoveWidget( + dev=dev, motor="fm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2 + ) + self.mover_widgets.append(self.fm_bnd) + + self.fm_rotx = MoveWidget( + dev=dev, motor="fm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01 + ) + self.mover_widgets.append(self.fm_rotx) + + self.fm_roty = MoveWidget( + dev=dev, motor="fm_roty", label="YAW", unit="mrad", decimals=3, deadband=0.01 + ) + self.mover_widgets.append(self.fm_roty) + + self.fm_rotz = MoveWidget( + dev=dev, motor="fm_rotz", label="ROLL", unit="mrad", decimals=3, deadband=0.01 + ) + self.mover_widgets.append(self.fm_rotz) + + self.fm_mov_group = Group( + "Focusing Mirror", + [self.fm_trx, self.fm_try, self.fm_bnd, self.fm_rotx, self.fm_roty, self.fm_rotz], + ) + + # OP Slits 2 + self.sl2_centery = MoveWidget( + dev=dev, motor="sl2_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.sl2_centery) + + self.sl2_gapy = MoveWidget( + dev=dev, motor="sl2_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.sl2_gapy) + + self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy]) + + # OP Beam Monitor 2 + self.bm2_try = MoveWidget( + dev=dev, motor="bm2_try", label="TRY", unit="mm", decimals=2, deadband=0.1 + ) + self.mover_widgets.append(self.bm2_try) + + self.bm2_mov_group = Group("OP Beam Monitor 2", [self.bm2_try]) + + # Optical Table + self.ot_try = MoveWidget( + dev=dev, motor="ot_try", label="TRY", unit="mm", decimals=2, deadband=0.2 + ) + self.mover_widgets.append(self.ot_try) + + self.ot_rotx = MoveWidget( + dev=dev, motor="ot_rotx", label="ROTX", unit="mrad", decimals=3, deadband=0.05 + ) + self.mover_widgets.append(self.ot_rotx) + + self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx]) + + # Experimental Station 0 + self.es0wi_try = MoveWidget( + dev=dev, motor="es0wi_try", label="ES0 WI", unit="mm", decimals=0, deadband=0.1 + ) + self.mover_widgets.append(self.es0wi_try) + + self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try]) + + # Experimental Station 1 + self.ot_es1_trz = MoveWidget( + dev=dev, motor="ot_es1_trz", label="ES1 TRZ", unit="mm", decimals=0, deadband=5 + ) + self.mover_widgets.append(self.ot_es1_trz) + + self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz]) + + # Assemble complete mover group + self.mover_group = Group( + "Mover", + [ + self.sldi_mov_group, + self.abs_group, + self.cm_mov_group, + self.mo1_mov_group, + self.sl1_mov_group, + self.bm1_mov_group, + self.fm_mov_group, + self.sl2_mov_group, + self.bm2_mov_group, + self.ot_mov_group, + self.es0_mov_group, + self.es1_mov_group, + ], + ) + + self._layout.addWidget(self.mover_group) + self._layout.addStretch() + + def apply_theme(self, theme): + for widget in self.mover_widgets: + widget.apply_theme(theme) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/plots.py new file mode 100644 index 0000000..5368389 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/plots.py @@ -0,0 +1,303 @@ +""" +Two plot classes to plot side-view and surface-view +""" + +import numpy as np +import pyqtgraph as pg +from bec_lib import bec_logger + +# pylint: disable=E0611 +from qtpy.QtCore import Qt +from qtpy.QtGui import QBrush, QColor + +# pylint: disable=E0611 +from qtpy.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget + +from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( + mirror_surface_geometries, + mo_surface_geometries, + pipe_geometries, + wall_geometries, +) +from debye_bec.bec_widgets.widgets.qt_widgets import Group + +logger = bec_logger.logger + + +class SurfacePlots(QWidget): + """Plot widget with two curves and legend.""" + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._layout = QHBoxLayout(self) + + self.surfaces = { + "assistant": { + "cm": {"x": [], "y": []}, + "mo1_1": {"x": [], "y": []}, + "mo1_2": {"x": [], "y": []}, + "fm": {"x": [], "y": []}, + }, + "reality": { + "cm": {"x": [], "y": []}, + "mo1_1": {"x": [], "y": []}, + "mo1_2": {"x": [], "y": []}, + "fm": {"x": [], "y": []}, + }, + } + + self.plots = {"fm": {}, "mo1_2": {}, "mo1_1": {}, "cm": {}} + + self.color_impenetrable = (0, 0, 0) + self.colors = [(255, 255, 0), (255, 0, 255)] + self.text_color = (255, 255, 255) + + # Create plot widgets + for name, widget in self.plots.items(): + plot_widget = pg.PlotWidget() + plot_widget.getAxis("bottom").enableAutoSIPrefix(False) + + plot_group = Group("Surface " + name, [plot_widget]) + + plot_widget.setLabel("left", "Z [mm]") + plot_widget.setLabel("bottom", "X [mm]") + plot_widget.setMouseEnabled(x=False, y=False) + plot_widget.setMenuEnabled(False) + plot_widget.hideButtons() + + widget["widget"] = plot_widget + self._layout.addWidget(plot_group) + + # Create surfaces + for idx, scene in enumerate(self.surfaces): + for name, _ in self.surfaces[scene].items(): + 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) + z_value = 2 + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1) + z_value = 1 + widget = self.plots[name] + self.plots[name][scene] = widget["widget"].plot( + [], [], pen=pen, name=scene, brush=brush, fillLevel=0 + ) + self.plots[name][scene].setZValue(z_value) + + self.walls = [] + self.texts = [] + + self.plot_walls() + + self.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + for _, plot in self.plots.items(): + # Background + plot["widget"].setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = plot["widget"].getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + if theme == "light": + self.color_impenetrable = (30, 30, 30) + self.colors = [(79, 163, 224), (240, 128, 60)] + self.text_color = (255, 255, 255) + else: # dark theme + 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.surfaces): + for name, _ in self.surfaces[scene].items(): + 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) + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0) + self.plots[name][scene].setPen(pen) + self.plots[name][scene].setBrush(brush) + + for wall in self.walls: + wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + wall.setBrush( + pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable)) + ) # pylint: disable=E1101 + + for text in self.texts: + text.setColor(self.text_color) + + def plot_walls(self): + + def plot_surface(widget, surfaces): + for name, surface in surfaces.items(): + rect = pg.QtWidgets.QGraphicsRectItem(*surface) # pylint: disable=E1101 + rect.setBrush( + pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable)) + ) # pylint: disable=E1101 + rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + widget.addItem(rect) + text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5)) + widget.addItem(text) + text.setPos(surface[0] + surface[2] / 2, surface[1] + surface[3] / 2) + text.setZValue(10) + self.walls.append(rect) + self.texts.append(text) + + for name, plot in self.plots.items(): + if name in "cm": + plot_surface(plot["widget"], mirror_surface_geometries("cm")) + elif name in "mo1_1": + plot_surface(plot["widget"], mo_surface_geometries("mo1", 0)) + elif name in "mo1_2": + plot_surface(plot["widget"], mo_surface_geometries("mo1", 1)) + elif name in "fm": + plot_surface(plot["widget"], mirror_surface_geometries("fm_flat")) + plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid")) + else: + raise Exception(f"Plot {name} not found!") + for name, plot in self.plots.items(): + plot["widget"].disableAutoRange() + + def update_surfaces(self, scene, data): + self.surfaces[scene] = data + for name, device in self.surfaces[scene].items(): + plot = self.plots[name][scene] + x = np.array(device["x"] + [device["x"][0]]) if len(device["x"]) != 0 else np.array([]) + y = np.array(device["y"] + [device["y"][0]]) if len(device["y"]) != 0 else np.array([]) + plot.setData(x=x, y=y) + + +class SideviewPlot(QWidget): + """Plot widget with two curves and legend.""" + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._layout = QVBoxLayout(self) + # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + self.plot_widget = pg.PlotWidget() + self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False) + self.plot_widget.invertX(True) + self.plot_widget.addLegend() + + self.color_impenetrable = (0, 0, 0) + self.colors = [(255, 255, 0), (255, 0, 255)] + + self.data = { + "assistant": {"x": [0, 1000, 2000], "y": [0, 20, 30]}, + "reality": {"x": [0, 1000, 2000], "y": [0, 15, 50]}, + } + + self.plots = {} + + self.pipes = [] + self.walls = [] + + for idx, scene in enumerate(self.data.keys()): + if scene in "assistant": + pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DotLine) + z_value = 2 + else: + pen = pg.mkPen(color=self.colors[idx], width=2) + z_value = 1 + self.plots[scene] = self.plot_widget.plot([], [], pen=pen, name=scene) + self.plots[scene].setZValue(z_value) + + self.plot_group = Group("Side View", [self.plot_widget]) + + 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.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + # Background + self.plot_widget.setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = self.plot_widget.getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + if theme == "light": + self.color_impenetrable = (30, 30, 30) + self.colors = [(79, 163, 224), (240, 128, 60)] + self.text_color = (255, 255, 255) + else: # dark theme + 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=3, style=Qt.DotLine) + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + 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=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=3)) + + def plot_vacuum_pipes(self): + pipes = pipe_geometries() + for pipe in pipes: + self.pipes.append( + self.plot_widget.plot( + x=pipe["x"], y=pipe["y"], pen=pg.mkPen(color=self.color_impenetrable, width=2) + ) + ) + + def plot_walls(self): + walls = wall_geometries() + for wall in walls: + rect = pg.QtWidgets.QGraphicsRectItem(*wall) # pylint: disable=E1101 + 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) + self.walls.append(rect) + + def update_curves(self, scene, data): + self.data[scene] = data + plot = self.plots[scene] + plot.setData(x=self.data[scene]["x"], y=self.data[scene]["y"]) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py new file mode 100644 index 0000000..31723b3 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py @@ -0,0 +1,27 @@ +""" +Settings panel for the digital twin widget +""" + +# pylint: disable=E0611 +from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget + +from debye_bec.bec_widgets.widgets.qt_widgets import Button, Group + + +class SettingsPanel(QWidget): + """Right-side control panel: input field, indicator, send, recording.""" + + def __init__(self, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + # Reload offsets + self.reload_offsets = Button(label="Reload Offsets", label_button="Reload", enabled=True) + self.unload_offsets = Button(label="Unload Offsets", label_button="Unload", enabled=True) + + # Assemble complete offset group + self.offset_group = Group("Axes Offsets", [self.reload_offsets, self.unload_offsets]) + + self._layout.addWidget(self.offset_group) + self._layout.addStretch()