From 6b5ff49b046c2664ca7af51a9cb8ac09652f04cf Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 18 May 2026 10:38:01 +0200 Subject: [PATCH 1/5] 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() From 62582da1d9159bebebf7bad08ec1bf93de3e407c Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 19 May 2026 06:44:04 +0200 Subject: [PATCH 2/5] refactoring --- .../widgets/digital_twin/calc_sideview.py | 87 +++++---- .../widgets/digital_twin/calc_surfaces.py | 143 ++++++++------- .../widgets/digital_twin/calc_varia.py | 170 +++++++++++------- .../widgets/digital_twin/digital_twin.py | 124 ++++++++----- .../widgets/digital_twin/input_panel.py | 2 +- .../widgets/digital_twin/move_widget.py | 118 ++++++++---- .../widgets/digital_twin/mover_panel.py | 11 +- .../bec_widgets/widgets/digital_twin/plots.py | 83 ++++++--- .../widgets/digital_twin/settings_panel.py | 2 +- .../bec_widgets/widgets/digital_twin/types.py | 34 ++++ 10 files changed, 513 insertions(+), 261 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/types.py diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py index 33487a2..55d09c5 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py @@ -1,42 +1,69 @@ import numpy as np + import debye_bec.bec_widgets.widgets.x01da_parameters as bl +from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict + def calc_sideview(cfg): # Calculate height of beam after CM - height = 2 * bl.cm.center[1] * np.tan(cfg['v_acc']) + # height = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) # beam height (Y=height, Z=along beam) - beam = {} - beam['x'] = [] - beam['y'] = [] - beam['x'].append(0) # Source - beam['y'].append(bl.sourceHeight) - beam['x'].append(bl.cm.center[1]) # CM - beam['y'].append(bl.sourceHeight) - if cfg['mo1_mode'] in 'Monochromatic': - diag = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg']) # Calculations for Mono - dy = diag*np.sin(2*(cfg['cm_pitch']+cfg['mo1_bragg'])) - dz = diag*np.cos(2*(cfg['cm_pitch']+cfg['mo1_bragg'])) - beam['x'].append(bl.mo1.center[1]-dz/2) # Mono 1.1 - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1])) - beam['x'].append(bl.mo1.center[1]+dz/2) # Mono 1.2 - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1])+dy) - beam['x'].append(bl.fm.center[1]) # FM - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy) - beam['x'].append(cfg['smpl']) # Experiment - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy+np.tan(2*(cfg['cm_pitch']-cfg['fm_rotx']))*(cfg['smpl']-bl.fm.center[1])) - elif cfg['mo1_mode'] == 'Pinkbeam': - beam['x'].append(bl.fm.center[1]) # FM - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1])) - beam['x'].append(cfg['smpl']) # Experiment - beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1])+np.tan(2*(cfg['cm_pitch']-cfg['fm_rotx']))*(cfg['smpl']-bl.fm.center[1])) - dy_fm_ex = beam['y'][-1] - beam['y'][-2] - dz_fm_ex = beam['x'][-1] - beam['x'][-2] - dz_fm_win = bl.ehWindow.center[1] - beam['x'][-2] - h_at_win = beam['y'][-2] + dy_fm_ex / dz_fm_ex * dz_fm_win + beam: DataDict = {"x": [], "y": []} - beam['heightWindow'] = h_at_win + beam["x"] = [] + beam["y"] = [] + beam["x"].append(0) # Source + beam["y"].append(bl.sourceHeight) + beam["x"].append(bl.cm.center[1]) # CM + beam["y"].append(bl.sourceHeight) + if cfg["mo1_mode"] in "Monochromatic": + diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono + dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) + dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) + beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1 + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) + ) + beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2 + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) + + dy + ) + beam["x"].append(bl.fm.center[1]) # FM + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) + + dy + ) + beam["x"].append(cfg["smpl"]) # Experiment + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) + + dy + + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) + ) + elif cfg["mo1_mode"] == "Pinkbeam": + beam["x"].append(bl.fm.center[1]) # FM + beam["y"].append( + bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) + ) + beam["x"].append(cfg["smpl"]) # Experiment + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) + + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) + ) + + dy_fm_ex = beam["y"][-1] - beam["y"][-2] + dz_fm_ex = beam["x"][-1] - beam["x"][-2] + dz_fm_win = bl.ehWindow.center[1] - beam["x"][-2] + h_at_win = beam["y"][-2] + dy_fm_ex / dz_fm_ex * dz_fm_win + + # beam["heightWindow"] = h_at_win return beam diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py index 013164b..d8e80ce 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py @@ -1,5 +1,6 @@ import os import re + import numpy as np from bec_lib import bec_logger @@ -7,87 +8,107 @@ logger = bec_logger.logger os.environ["USE_XRT"] = "False" import debye_bec.bec_widgets.widgets.x01da_parameters as bl +from debye_bec.bec_widgets.widgets.digital_twin.types import SurfaceDict + def calc_surfaces(cfg): - out = { - 'cm': {'x': [], 'y': []}, - 'mo1_1': {'x': [], 'y': []}, - 'mo1_2': {'x': [], 'y': []}, - 'fm': {'x': [], 'y': []}, + out: SurfaceDict = { + "cm": {"x": [], "y": []}, + "mo1_1": {"x": [], "y": []}, + "mo1_2": {"x": [], "y": []}, + "fm": {"x": [], "y": []}, } # Collimating mirror - l = 2 * bl.cm.center[1] * np.tan(cfg['v_acc'])/np.sin(cfg['cm_pitch']) + l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"]) - w1 = 2 * (bl.cm.center[1]-l/2) * np.tan(cfg['h_acc']) - w2 = 2 * (bl.cm.center[1]+l/2) * np.tan(cfg['h_acc']) + w1 = 2 * (bl.cm.center[1] - l / 2) * np.tan(cfg["h_acc"]) + w2 = 2 * (bl.cm.center[1] + l / 2) * np.tan(cfg["h_acc"]) - index = bl.cm.surface.index(cfg['cm_stripe']) - cen = (bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 + index = bl.cm.surface.index(cfg["cm_stripe"]) - if cfg['cm_trx'] is not None: - cen = cfg['cm_trx'] - - out['cm']['x'] = [cen-w1/2, cen-w2/2, cen+w2/2, cen+w1/2] - out['cm']['y'] = [-l/2, l/2, l/2, -l/2] + cen = -cfg["cm_trx"] + out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2] + out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2] # Monochromator # calculate height of center of first crystal surface - c = bl.mo1.heightOffset*1/np.sin(cfg['mo1_bragg'])-bl.mo1.rotOffset*1/np.tan(cfg['mo1_bragg']) - e = bl.mo1.xtalGap[0]/np.tan(cfg['mo1_bragg'])-c + c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan( + cfg["mo1_bragg"] + ) + e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c - xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + xtal = cfg["mo1_xtal"].translate( + str.maketrans("", "", "()") + ) # Remove brackets from xtal name to conform with parameters index = bl.mo1.xtal.index(xtal) xtalPos = bl.mo1.xtalOffsetX[index] xtalLength1 = bl.mo1.xtalLength1[index] xtalLength2 = bl.mo1.xtalLength2[index] - widthBeam = 2 * bl.mo1.center[1] * np.tan(cfg['h_acc']) - - heightBeam = 2 * bl.cm.center[1] * np.tan(cfg['v_acc']) - w = heightBeam / np.sin(cfg['mo1_bragg']) + widthBeam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"]) - if cfg['mo1_mode'] in 'Monochromatic': - out['mo1_1']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/2] - out['mo1_1']['y'] = [xtalLength1/2-c-w/2, xtalLength1/2-c-w/2, xtalLength1/2-c+w/2, xtalLength1/2-c+w/2] - out['mo1_2']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/2] - out['mo1_2']['y'] = [-xtalLength2/2+e-w/2, -xtalLength2/2+e-w/2, -xtalLength2/2+e+w/2, -xtalLength2/2+e+w/2] - else: # Pinkbeam - out['mo1_1']['x'] = [] - out['mo1_1']['y'] = [] - out['mo1_2']['x'] = [] - out['mo1_2']['y'] = [] + heightBeam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) + w = heightBeam / np.sin(cfg["mo1_bragg"]) + + if cfg["mo1_mode"] in "Monochromatic": + out["mo1_1"]["x"] = [ + xtalPos - widthBeam / 2, + xtalPos + widthBeam / 2, + xtalPos + widthBeam / 2, + xtalPos - widthBeam / 2, + ] + out["mo1_1"]["y"] = [ + xtalLength1 / 2 - c - w / 2, + xtalLength1 / 2 - c - w / 2, + xtalLength1 / 2 - c + w / 2, + xtalLength1 / 2 - c + w / 2, + ] + out["mo1_2"]["x"] = [ + xtalPos - widthBeam / 2, + xtalPos + widthBeam / 2, + xtalPos + widthBeam / 2, + xtalPos - widthBeam / 2, + ] + out["mo1_2"]["y"] = [ + -xtalLength2 / 2 + e - w / 2, + -xtalLength2 / 2 + e - w / 2, + -xtalLength2 / 2 + e + w / 2, + -xtalLength2 / 2 + e + w / 2, + ] + else: # Pinkbeam + out["mo1_1"]["x"] = [] + out["mo1_1"]["y"] = [] + out["mo1_2"]["x"] = [] + out["mo1_2"]["y"] = [] # Focusing mirror - if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'): + if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): surface = bl.fm.surfaceToroid - stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip() + stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() index = surface.index(stripe) - off = (bl.fm.limOptXToroid[0][index] + bl.fm.limOptXToroid[1][index]) / 2 r = bl.fm.r[index] else: surface = bl.fm.surfaceFlat - stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip() + stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() index = surface.index(stripe) - off = (bl.fm.limOptXFlat[0][index] + bl.fm.limOptXFlat[1][index]) / 2 r = bl.fm.r[index] - if cfg['fm_trx'] is not None: - off = cfg['fm_trx'] + off = -cfg["fm_trx"] - widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc']) + widthBeam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"]) - if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'): + if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): - l = heightBeam/np.sin(cfg['fm_rotx']) - alpha = np.arccos(1-widthBeam**2/(2*r**2)) - h = r-(r*np.cos(alpha/2)) - z = h/np.tan(cfg['fm_rotx']) + l = heightBeam / np.sin(cfg["fm_rotx"]) + alpha = np.arccos(1 - widthBeam**2 / (2 * r**2)) + h = r - (r * np.cos(alpha / 2)) + z = h / np.tan(cfg["fm_rotx"]) - x = [off-widthBeam/2, off-widthBeam/2] - y = [l/2-z/2, -l/2-z/2] + x = [off - widthBeam / 2, off - widthBeam / 2] + y = [l / 2 - z / 2, -l / 2 - z / 2] # logger.info(f'stripe: {cfg["fm_stripe"]}') # logger.info(f'fm_rotx: {cfg["fm_rotx"]}') @@ -98,34 +119,34 @@ def calc_surfaces(cfg): res = 20 xElipse = np.linspace(0, np.pi, res) yElipse = np.linspace(0, np.pi, res) - xElipse = [-widthBeam/2*np.cos(i)+off for i in xElipse] - yElipse = [widthBeam*np.sin(i)*z/widthBeam-l/2-z/2 for i in yElipse] + xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse] + yElipse = [widthBeam * np.sin(i) * z / widthBeam - l / 2 - z / 2 for i in yElipse] x.extend(xElipse) y.extend(yElipse) - x.extend([off+widthBeam/2, off+widthBeam/2]) - y.extend([-l/2-z/2, l/2-z/2]) + x.extend([off + widthBeam / 2, off + widthBeam / 2]) + y.extend([-l / 2 - z / 2, l / 2 - z / 2]) res = 50 xElipse = np.linspace(np.pi, 0, res) yElipse = np.linspace(np.pi, 0, res) - xElipse = [-widthBeam/2*np.cos(i)+off for i in xElipse] - yElipse = [widthBeam*np.sin(i)*z/widthBeam+l/2-z/2 for i in yElipse] + xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse] + yElipse = [widthBeam * np.sin(i) * z / widthBeam + l / 2 - z / 2 for i in yElipse] x.extend(xElipse) y.extend(yElipse) - out['fm']['x'] = x - out['fm']['y'] = y + out["fm"]["x"] = x + out["fm"]["y"] = y - else: # flat surface, no toroid - l = heightBeam/np.sin(cfg['fm_rotx']) + else: # flat surface, no toroid + l = heightBeam / np.sin(cfg["fm_rotx"]) - w1 = 2 * (bl.fm.center[1]-l/2) * np.tan(cfg['h_acc']) - w2 = 2 * (bl.fm.center[1]+l/2) * np.tan(cfg['h_acc']) + w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"]) + w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"]) - out['fm']['x'] = [off-w1/2, off+w1/2, off+w2/2, off-w2/2] - out['fm']['y'] = [-l/2, -l/2, l/2, l/2] + out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2] + out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2] return out diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py index 029be5d..710212a 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py @@ -1,13 +1,15 @@ import re + import numpy as np -from scipy.interpolate import UnivariateSpline -from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO from bec_lib import bec_logger +from scipy.interpolate import UnivariateSpline +from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm import debye_bec.bec_widgets.widgets.x01da_parameters as bl logger = bec_logger.logger + def sldi_gap_to_acc(sldi_gapx, sldi_gapy): d1 = bl.feSlits.center1[1] d2 = bl.feSlits.center2[1] @@ -18,6 +20,7 @@ def sldi_gap_to_acc(sldi_gapx, sldi_gapy): # v_acc = np.tan(sldi_gapy / (2 * d1)) return h_acc, v_acc + def cm_trx_to_stripe(cm_trx): cm_stripe = None for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): @@ -25,28 +28,47 @@ def cm_trx_to_stripe(cm_trx): cm_stripe = name return cm_stripe + +def cm_stripe_to_trx(cm_stripe): + for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): + if cm_stripe == name: + return -(low + high) / 2 + return 0 + + def fm_trx_to_stripe(fm_trx): fm_stripe = None for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): if low <= fm_trx <= high: - fm_stripe = name + ' (flat)' + fm_stripe = name + " (flat)" for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): if low <= fm_trx <= high: - fm_stripe = name + ' (toroid)' + fm_stripe = name + " (toroid)" return fm_stripe + +def fm_stripe_to_trx(fm_stripe): + for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): + if fm_stripe == name + " (flat)": + return (low + high) / 2 + for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): + if fm_stripe == name + " (toroid)": + return -(low + high) / 2 + return 0 + + def mo1_energy_resolution(xtal, energy): index = bl.mo1.xtal.index(xtal) crystal = bl.mo1.material1[index] dtheta = np.linspace(-30, 90, 601) theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6 - refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0])**2 # single crystal + refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal refl2 = refl**2 # DCM with parallel crystals # FWHM of the DCM curve - spline = UnivariateSpline(dtheta, refl2 - refl2.max()/2, s=0) + spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0) r1, r2 = spline.roots() fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad @@ -61,85 +83,88 @@ def mo1_energy_resolution(xtal, energy): return dE + def cm_reflectivity(cm_stripe, cm_pitch, energy): index = bl.cm.surface.index(cm_stripe) - rs, rp = bl.cm.material[index].get_amplitude( - energy, - np.sin(cm_pitch) - )[0:2] - refl = abs(rs)**2 + rs, rp = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2] + refl = abs(rs) ** 2 return refl + def fm_reflectivity(fm_stripe, fm_pitch, energy): - if fm_stripe in ('Rh (toroid)', 'Pt (toroid)'): + if fm_stripe in ("Rh (toroid)", "Pt (toroid)"): surface = bl.fm.surfaceToroid material = bl.fm.materialToroid - stripe = re.sub(r'\s*\(.*?\)', '', fm_stripe).strip() + stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() index = surface.index(stripe) else: surface = bl.fm.surfaceFlat material = bl.fm.materialFlat - stripe = re.sub(r'\s*\(.*?\)', '', fm_stripe).strip() + stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() index = surface.index(stripe) - rs, rp = material[index].get_amplitude( - energy, - np.sin(fm_pitch) - )[0:2] - refl = abs(rs)**2 + rs, rp = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2] + refl = abs(rs) ** 2 return refl + def mo1_bragg_angle(mo_mode, d_spacing, energy, cm_pitch): - H = 6.62606957E-34 - E = 1.602176634E-19 + H = 6.62606957e-34 + E = 1.602176634e-19 C = 299792458 wl = C * H / (E * energy) val = wl / (2 * d_spacing * 1e-10) bragg_angle = 0 if val > -1 and val < 1: bragg_angle = np.asin(val) - if mo_mode in 'Monochromatic': + if mo_mode in "Monochromatic": # Add 2x CM pitch to the bragg angle - bragg_angle_cor = ((2 * cm_pitch) + bragg_angle) - elif mo_mode in 'Pinkbeam': + bragg_angle_cor = (2 * cm_pitch) + bragg_angle + elif mo_mode in "Pinkbeam": # Align xtal surfaces parallel to beam - bragg_angle_cor = (2 * cm_pitch) + bragg_angle_cor = 2 * cm_pitch return bragg_angle, bragg_angle_cor -def fm_ideal_pitch(fm_focus, fm_stripe, smpl, sldi_hacc=None, sldi_vacc=None, fm_focx=None, fm_focy=None): - p = bl.fm.center[1] # posFM - q = smpl - bl.fm.center[1] # dist posFM to posEX - if fm_focus in 'Defocused': - a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror - b = 2 * np.tan(sldi_vacc) * bl.cm.center[1] # Beam height at focusing mirror (collimated beam) + +def fm_ideal_pitch( + fm_focus, fm_stripe, smpl, sldi_hacc=None, sldi_vacc=None, fm_focx=None, fm_focy=None +): + p = bl.fm.center[1] # posFM + q = smpl - bl.fm.center[1] # dist posFM to posEX + if fm_focus in "Defocused": + a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror + b = ( + 2 * np.tan(sldi_vacc) * bl.cm.center[1] + ) # Beam height at focusing mirror (collimated beam) x = fm_focx y = fm_focy qx = q + x * p / a qy = q + y * p / b - f = (p * qx) / (p + qx) # focal length - else: # Calculate for focused beam on sample in "manual" and "focused" mode + f = (p * qx) / (p + qx) # focal length + else: # Calculate for focused beam on sample in "manual" and "focused" mode qy = None - f = (p * q) / (p + q) # focal length + f = (p * q) / (p + q) # focal length pitch = 0 - if 'Rh' in fm_stripe: - pitch = np.arcsin(bl.fm.r[0]/(2*f))# ideal pitch for FM - if 'Pt' in fm_stripe: - pitch = np.arcsin(bl.fm.r[1]/(2*f)) # ideal pitch for FM + if "Rh" in fm_stripe: + pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM + if "Pt" in fm_stripe: + pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM return pitch, qy + def cm_critical_angle(cm_stripe, energy): - if cm_stripe in 'Si': + if cm_stripe in "Si": stripe = bl.stripeSi - elif cm_stripe in 'Pt': + elif cm_stripe in "Pt": stripe = bl.stripePt - elif cm_stripe in 'Rh': + elif cm_stripe in "Rh": stripe = bl.stripeRh else: - raise Exception(f'Stripe {stripe} not found in beamline parameters!') - w = CHeVcm/100/energy # convert energy [eV] to wavelength [m] + raise Exception(f"Stripe {stripe} not found in beamline parameters!") + w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m] # Calculate critical angle for mirror f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy)) - numberDensity = stripe.rho*1e3*AVOGADRO/(stripe.elements[0].mass/1e3) - criticalAngle = np.sqrt(numberDensity*2.8179e-15*w**2*f1/np.pi) + numberDensity = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3) + criticalAngle = np.sqrt(numberDensity * 2.8179e-15 * w**2 * f1 / np.pi) return criticalAngle @@ -148,23 +173,24 @@ def mirror_surface_geometries(mirror): surface = bl.cm.surface limOptX = bl.cm.limOptX limOptY = bl.cm.limOptY - elif mirror in 'fm_toroid': + elif mirror in "fm_toroid": surface = bl.fm.surfaceToroid limOptX = bl.fm.limOptXToroid limOptY = bl.fm.limOptYToroid - elif mirror in 'fm_flat': + elif mirror in "fm_flat": surface = bl.fm.surfaceFlat limOptX = bl.fm.limOptXFlat limOptY = bl.fm.limOptYFlat else: - raise ValueError(f'Requested mirror {mirror} not available!') + raise ValueError(f"Requested mirror {mirror} not available!") geom = {} for sf, lx, hx, ly, hy in zip(surface, limOptX[0], limOptX[1], limOptY[0], limOptY[1]): - geom[sf] = (lx, ly, hx-lx, hy-ly) + geom[sf] = (lx, ly, hx - lx, hy - ly) return geom + def mo_surface_geometries(mo, plane): - if mo in 'mo1': + if mo in "mo1": xtal = bl.mo1.xtal xtal_width = bl.mo1.xtalWidth xtal_offset_x = bl.mo1.xtalOffsetX @@ -173,34 +199,42 @@ def mo_surface_geometries(mo, plane): else: xtal_length = bl.mo1.xtalLength2 else: - raise ValueError(f'Requested mono {mo} not available!') + raise ValueError(f"Requested mono {mo} not available!") geom = {} for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length): - geom[sf] = (offx-w/2, -length/2, w, length) + geom[sf] = (offx - w / 2, -length / 2, w, length) return geom + def wall_geometries(): geom = [] for i, _ in enumerate(bl.walls.start): - geom.append([ - bl.walls.start[i], - bl.walls.height[i][0], - bl.walls.end[i] - bl.walls.start[i], - bl.walls.height[i][1] - bl.walls.height[i][0], - ]) + geom.append( + [ + bl.walls.start[i], + bl.walls.height[i][0], + bl.walls.end[i] - bl.walls.start[i], + bl.walls.height[i][1] - bl.walls.height[i][0], + ] + ) return geom + def pipe_geometries(): pipes = [] for i, _ in enumerate(bl.vacuum_pipes.center): - top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight - bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight - pipes.append({ - 'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), - 'y': np.array([top, top]) - }) - pipes.append({ - 'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), - 'y': np.array([bottom, bottom]) - }) + top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight + bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight + pipes.append( + { + "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + "y": np.array([top, top]), + } + ) + pipes.append( + { + "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + "y": np.array([bottom, bottom]), + } + ) return pipes 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 6eb288d..d98975d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -4,18 +4,19 @@ Digital Twin: Custom BEC widget to support the beamline alignment. import sys from pathlib import Path +from typing import Literal import numpy as np import yaml from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints +from bec_widgets.utils.bec_dispatcher import BECDispatcher from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.utils.colors import apply_theme from bec_widgets.utils.error_popups import SafeSlot # pylint: disable=E0611 from qtpy.QtCore import Qt, QTimer - -# pylint: disable=E0611 from qtpy.QtWidgets import ( QApplication, QDialog, @@ -33,9 +34,11 @@ from debye_bec.bec_widgets.widgets.digital_twin.calc_surfaces import calc_surfac from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( cm_critical_angle, cm_reflectivity, + cm_stripe_to_trx, cm_trx_to_stripe, fm_ideal_pitch, fm_reflectivity, + fm_stripe_to_trx, fm_trx_to_stripe, mo1_bragg_angle, mo1_energy_resolution, @@ -74,21 +77,21 @@ 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) + self.input_layout.addWidget(self.settings) 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) + self.plot_layout.addWidget(self.surface_plots) 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.mover, alignment=Qt.AlignTop) + self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignmentFlag.AlignTop) + self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignmentFlag.AlignTop) + self.root_layout.addWidget(self.mover, alignment=Qt.AlignmentFlag.AlignTop) self.setLayout(self.root_layout) self.setWindowTitle("Digital Twin") @@ -126,7 +129,13 @@ class DigitalTwin(BECWidget, QWidget): self._timer.timeout.connect(self.calc_reality) self._timer.start() - def apply_theme(self, theme): + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ self.sideview_plot.apply_theme(theme) self.surface_plots.apply_theme(theme) self.mover.apply_theme(theme) @@ -176,10 +185,12 @@ class DigitalTwin(BECWidget, QWidget): top = QHBoxLayout() icon = QLabel() icon_pixmap = ( - QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(48, 48) + QApplication.style() + .standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning) + .pixmap(48, 48) ) icon.setPixmap(icon_pixmap) - icon.setAlignment(Qt.AlignTop) + icon.setAlignment(Qt.AlignmentFlag.AlignTop) top.addWidget(icon) text = QLabel( @@ -187,13 +198,13 @@ class DigitalTwin(BECWidget, QWidget): + "Reload the config with the correct devices." ) text.setWordWrap(True) - text.setAlignment(Qt.AlignTop) + text.setAlignment(Qt.AlignmentFlag.AlignTop) top.addWidget(text, stretch=1) layout.addLayout(top) info = QLabel("Missing devices:\n" + ", ".join(missing)) info.setWordWrap(True) - info.setAlignment(Qt.AlignTop) + info.setAlignment(Qt.AlignmentFlag.AlignTop) layout.addWidget(info) layout.addStretch() @@ -209,9 +220,10 @@ class DigitalTwin(BECWidget, QWidget): dialog.setLayout(layout) dialog.show() info.setMinimumHeight(info.heightForWidth(info.width())) - if dialog.exec_() == QDialog.Rejected: - QApplication.instance().exit(0) - # sys.exit(0) + if dialog.exec_() == QDialog.DialogCode.Rejected: + app = QApplication.instance() + if app is not None: + app.exit(0) if reload: self._timer.start() @@ -255,6 +267,9 @@ class DigitalTwin(BECWidget, QWidget): self.calc_fm_ideal_pitch() case "fm_focy": self.calc_fm_ideal_pitch() + case "fm_rotx": + self.calc_fm_reflectivity() + self.calc_cm_fm_harm_suppr() case "fm_stripe": self.calc_fm_reflectivity() self.calc_cm_fm_harm_suppr() @@ -265,7 +280,7 @@ class DigitalTwin(BECWidget, QWidget): self.calc_assistant_sideview() self.calc_assistant_surfaces() - def get_assistant_config(self): + def get_assistant_config(self, apply_offset: bool = False): fm_focus = self.input.fm_focus.currentText() if fm_focus in "Manual": fm_rotx = self.input.fm_rotx.value() @@ -277,23 +292,54 @@ class DigitalTwin(BECWidget, QWidget): fm_rotx = self.input.fm_rotx_ideal.value() fm_qy = self.qy - config = { # Config in SI units! + cm_stripe = self.input.cm_stripe.currentText() + cm_trx = cm_stripe_to_trx(cm_stripe) + fm_stripe = self.input.fm_stripe.currentText() + fm_trx = fm_stripe_to_trx(fm_stripe) + + config = { "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, + "h_acc": self.input.sldi_hacc.value(), + "v_acc": self.input.sldi_vacc.value(), + "cm_pitch": -self.input.cm_pitch.value(), + "cm_stripe": cm_stripe, + "cm_trx": cm_trx, "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_rotx": -fm_rotx, + "fm_stripe": fm_stripe, + "fm_trx": fm_trx, "fm_qy": fm_qy, "fm_gain_height": 1, "smpl": self.input.smpl.value(), } + + # Apply offsets + if apply_offset: + for axis, _ in config.items(): + if axis in self.offsets: + axis_offsets = self.offsets[axis] + logger.info(f"Axis: {axis}") + if "modifier" in axis_offsets and "offset" in axis_offsets: + for idx, rng in enumerate(axis_offsets["modifier"]["range"]): + logger.info(f"rng: {rng}") + logger.info(f'value: {config[axis_offsets["modifier"]["axis"]]}') + if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]: + logger.info(f'offset: {axis_offsets["offset"][idx]}') + # logger.info(f"axis_data before: {axis_data}") + config[axis] += axis_offsets["offset"][idx] + # logger.info(f"axis_data after: {axis_data}") + break + elif "offset" in axis_offsets: + config[axis] += axis_offsets["offset"] + + # Convert to SI units! + config["h_acc"] *= 1e-3 + config["v_acc"] *= 1e-3 + config["cm_pitch"] *= 1e-3 + config["fm_rotx"] *= 1e-3 + # logger.info(f'Config created: {config}') return config @@ -321,13 +367,13 @@ class DigitalTwin(BECWidget, QWidget): "v_acc": v_acc, "cm_pitch": -cm_pitch * 1e-3, "cm_stripe": cm_stripe, - "cm_trx": -cm_trx, + "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_trx": fm_trx, "fm_gain_height": 1, "smpl": smpl, } @@ -396,7 +442,7 @@ class DigitalTwin(BECWidget, QWidget): pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"] # Removing offsets - for axis, value in pos.items(): + for axis, _ in pos.items(): if axis in self.offsets: axis_offsets = self.offsets[axis] if "modifier" in axis_offsets and "offset" in axis_offsets: @@ -471,9 +517,8 @@ class DigitalTwin(BECWidget, QWidget): 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 = calc_sideview(config) + self.sideview_plot.update_curves("reality", data=data) # logger.info('Calc reality surfaces') surfaces = calc_surfaces(config) self.surface_plots.update_surfaces(scene="reality", data=surfaces) @@ -517,8 +562,8 @@ class DigitalTwin(BECWidget, QWidget): ) def calc_assistant_sideview(self): - beam = calc_sideview(self.get_assistant_config()) - data = {"x": beam["x"], "y": beam["y"]} + config = self.get_assistant_config(apply_offset=True) + data = calc_sideview(config) self.sideview_plot.update_curves("assistant", data) def calc_assistant_surfaces(self): @@ -581,11 +626,11 @@ class DigitalTwin(BECWidget, QWidget): "mo1_bragg_crystal_d_spacing_si311" ]["value"] else: - raise Exception(f"Invalid xtal selection: {xtal}") + raise ValueError(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) + theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch) self.bragg_angle = theta self.input.mo1_bragg_angle.setValue(theta / np.pi * 180) @@ -599,7 +644,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): fm_focus = self.input.fm_focus.currentText() fm_stripe = self.input.fm_stripe.currentText() smpl = self.input.smpl.value() @@ -620,9 +665,6 @@ class DigitalTwin(BECWidget, QWidget): if __name__ == "__main__": - from bec_widgets.utils.bec_dispatcher import BECDispatcher - from bec_widgets.utils.colors import apply_theme - app = QApplication(sys.argv) apply_theme("light") dispatcher = BECDispatcher(gui_id="digital_twin") diff --git a/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py index 99a29be..3364af4 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py @@ -15,7 +15,7 @@ from debye_bec.bec_widgets.widgets.qt_widgets import ( class InputPanel(QWidget): - """Right-side control panel: input field, indicator, send, recording.""" + """Panel for user inputs of the digital twin widget""" def __init__(self, parent=None): super().__init__(parent) 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 01cd73e..f11be4f 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -1,25 +1,19 @@ -import random +"""Move widget to display an axis and also move it through BEC""" + import threading import time +from typing import Literal, Optional 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 qtpy.QtCore import Property, QObject, QPropertyAnimation, Qt, QThread, Signal + +# pylint: disable=E0611 +from qtpy.QtCore import Property # type: ignore[attr-defined] +from qtpy.QtCore import Signal # type: ignore[attr-defined] +from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread from qtpy.QtGui import QTransform -from qtpy.QtWidgets import ( - QApplication, - QDoubleSpinBox, - QFrame, - QGroupBox, - QHBoxLayout, - QLabel, - QPushButton, - QVBoxLayout, - QWidget, -) +from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget from debye_bec.devices.absorber import STATUS as ABS_STATUS @@ -27,6 +21,8 @@ logger = bec_logger.logger class Status: + """Status class for the axis""" + IN_POSITION = "in_position" # green mdi.check-circle NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle MOVING = "moving" # blue mdi.loading (spinning) @@ -55,10 +51,10 @@ class StatusIcon(QWidget): self._label = QLabel(self) self._label.setFixedSize(self.ICON_SIZE, self.ICON_SIZE) - self._label.setAlignment(Qt.AlignCenter) + self._label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE) - self._spin_anim = QPropertyAnimation(self, b"rotation") + self._spin_anim = QPropertyAnimation(self, b"rotation") # type: ignore[call-arg] self._spin_anim.setStartValue(0) self._spin_anim.setEndValue(360) self._spin_anim.setDuration(1000) @@ -67,19 +63,42 @@ class StatusIcon(QWidget): self.set_status(Status.NOT_IN_POSITION) def get_rotation(self): + """Return the current rotation angle in degrees.""" return self._rotation - def set_rotation(self, angle): + def set_rotation(self, angle: float): + """ + Set the rotation angle and update the displayed pixmap. + + Rotates the current base pixmap around its center point using a smooth + transformation. Has no effect on the display if no base pixmap is set. + + Args: + angle (float): Rotation angle in degrees, clockwise. + """ self._rotation = angle if self._current_pixmap_base is not None: cx = self._current_pixmap_base.width() / 2 cy = self._current_pixmap_base.height() / 2 t = QTransform().translate(cx, cy).rotate(angle).translate(-cx, -cy) - self._label.setPixmap(self._current_pixmap_base.transformed(t, Qt.SmoothTransformation)) + self._label.setPixmap( + self._current_pixmap_base.transformed(t, Qt.TransformationMode.SmoothTransformation) + ) - rotation = Property(float, get_rotation, set_rotation) + rotation = Property(float, get_rotation, set_rotation) # type: ignore[call-arg] def set_status(self, status: str): + """ + Update the widget's status and refresh the displayed icon accordingly. + + Looks up the icon name and color associated with the given status from + ``_ICON_MAP``, renders a new pixmap, and starts or stops the spin + animation depending on whether the status is ``Status.MOVING``. Returns + early without any updates if the status has not changed. + + Args: + status (str): The new status value. Must be a key in ``_ICON_MAP``. + """ if status == self._status: return self._status = status @@ -115,9 +134,11 @@ class MotionWorker(QObject): self._stop_flag = threading.Event() def stop(self): + """Sets the stop flag""" self._stop_flag.set() def run(self): + """Prepares the movement based on the axis (motor)""" match self.motor: case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery": self.motion() @@ -214,7 +235,7 @@ class MotionWorker(QObject): case _: 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: bool = False, relative: bool = False, rb=None, surveyed_axes=None): """ Moves an axis while surverying a set of axes (if set). Example surveyed_axes: @@ -226,7 +247,8 @@ class MotionWorker(QObject): if abs_closed: if self.dev.abs.status.get() == ABS_STATUS.OPEN: status = self.dev.abs.close() - # TODO Set timeout to 0.001 and check if it actually raises (it should not start motion). + # TODO Set timeout to 0.001 and check if it actually raises + # (it should not start motion). # Check of behavior of digital twin afterwards. status.wait(timeout=5) if surveyed_axes is not None: @@ -259,7 +281,7 @@ class MotionWorker(QObject): self.dev[self.motor].stop() self.error.emit(1) break - self.finished.emit(True) + self.finished.emit() class MoveWidget(QWidget): @@ -326,7 +348,13 @@ class MoveWidget(QWidget): self.apply_theme() - def apply_theme(self, theme=None): + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ if theme is None: app = QApplication.instance() theme = app.theme.theme # type: ignore @@ -342,14 +370,17 @@ class MoveWidget(QWidget): if self.btn_mode == "start": self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" + "QPushButton " + + f"{{background-color: {get_accent_colors().success.name()}; color: white;}}" ) else: self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}" + "QPushButton " + + f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}" ) def set_target(self, target): + """Change the target value in the ui""" self.target = target text = f"{target:.{int(self.decimals)}f}" if self.unit is not None: @@ -358,6 +389,7 @@ class MoveWidget(QWidget): self._on_target_or_fb_changed() def set_feedback(self, fb): + """Change the feedback value in the ui""" if self.status != Status.MOVING: self.fb = fb text = f"{fb:.{int(self.decimals)}f}" @@ -367,19 +399,23 @@ class MoveWidget(QWidget): self._on_target_or_fb_changed() def _apply_button_style(self, mode: str): + """Apply a button style depending on if the button shows start or stop""" self.btn_mode = mode if mode == "start": self.btn_action.setText("Move") self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" + "QPushButton " + + f"{{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;}}" + "QPushButton " + + f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}" ) def _set_status(self, status: str): + """Set the current status icon in the ui""" self.status = status self.status_icon.set_status(status) @@ -393,12 +429,14 @@ class MoveWidget(QWidget): self._set_status(Status.NOT_IN_POSITION) def _on_button_clicked(self): + """Starts or stops motion depending on current situation""" if self._thread and self._thread.isRunning(): self._stop_motion() else: self._start_motion() def _start_motion(self): + """Start a motion""" target = self.target if abs(target - self.fb) <= self.deadband: self._set_status(Status.IN_POSITION) @@ -422,21 +460,25 @@ class MoveWidget(QWidget): self._thread.start() def _on_error(self): + """Called when an error occurs""" self._set_status(Status.ERROR) self._apply_button_style("start") def _stop_motion(self): + """Attempts to stop the motion""" if self._worker: self._worker.stop() def _on_position_changed(self, pos: float): + """Change the feedback value in the ui""" self.fb = pos text = f"{pos:.{int(self.decimals)}f}" if self.unit is not None: text = text + " " + self.unit self.fb_label.setText(text) - def _on_motion_finished(self, reached: bool): + def _on_motion_finished(self): + """Finished a movement""" target = self.target if self.status not in Status.ERROR: if abs(self.fb - target) <= self.deadband: @@ -446,6 +488,7 @@ class MoveWidget(QWidget): self._apply_button_style("start") def _cleanup_thread(self): + """Cleaning up of the mover thread""" if self._thread: self._thread.deleteLater() self._thread = None @@ -454,6 +497,7 @@ class MoveWidget(QWidget): self._worker = None def shutdown(self): + """Cleaning up of the mover when shutting down the application""" if self._worker: self._worker.stop() if self._thread: @@ -507,6 +551,12 @@ class AbsorberWidget(QWidget): layout.addWidget(self.btn_action) def set_feedback(self, fb: bool): + """ + Displays the status of the absober in the ui + + Args: + fb (bool): True will set the button to Open, False to Closed + """ self.fb = fb if fb: self.fb_label.setText("Open") @@ -516,9 +566,16 @@ class AbsorberWidget(QWidget): self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().emergency.name()}}}") def enable_open(self, enable: bool = False): + """ + Enable or disable the open/close button + + Args: + enable (bool): Enables and disables the button + """ if enable: self.btn_action.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" + "QPushButton " + + f"{{background-color: {get_accent_colors().success.name()}; color: white;}}" ) self.btn_action.setEnabled(True) else: # disabled @@ -528,4 +585,5 @@ class AbsorberWidget(QWidget): self.btn_action.setDisabled(True) def _on_button_clicked(self): + """Open absorber""" self.absorber.open() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py index 061ce27..2260c8a 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py @@ -2,6 +2,8 @@ Panel to move an axis to a certain position """ +from typing import Literal + # pylint: disable=E0611 from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget @@ -10,6 +12,7 @@ from debye_bec.bec_widgets.widgets.qt_widgets import Group class MoverPanel(QWidget): + """ "Panel to move an axis to a certain position""" def __init__(self, dev, parent=None): super().__init__(parent) @@ -215,6 +218,12 @@ class MoverPanel(QWidget): self._layout.addWidget(self.mover_group) self._layout.addStretch() - def apply_theme(self, theme): + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ 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 index 5368389..415fe6b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/plots.py @@ -2,6 +2,8 @@ Two plot classes to plot side-view and surface-view """ +from typing import Literal, Optional, TypedDict, cast + import numpy as np import pyqtgraph as pg from bec_lib import bec_logger @@ -19,6 +21,7 @@ from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( pipe_geometries, wall_geometries, ) +from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict, SurfaceDict from debye_bec.bec_widgets.widgets.qt_widgets import Group logger = bec_logger.logger @@ -31,7 +34,7 @@ class SurfacePlots(QWidget): super().__init__(parent=parent) self._layout = QHBoxLayout(self) - self.surfaces = { + self.surfaces: dict[str, SurfaceDict] = { "assistant": { "cm": {"x": [], "y": []}, "mo1_1": {"x": [], "y": []}, @@ -72,8 +75,10 @@ class SurfacePlots(QWidget): 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) + brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) + pen = pg.mkPen( + QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine + ) z_value = 2 else: brush = QBrush(QColor(*self.colors[idx], 255)) @@ -92,8 +97,13 @@ class SurfacePlots(QWidget): self.apply_theme() - def apply_theme(self, theme=None): + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ if theme is None: app = QApplication.instance() theme = app.theme.theme # type: ignore @@ -121,8 +131,10 @@ class SurfacePlots(QWidget): 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) + brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) + pen = pg.mkPen( + QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine + ) else: brush = QBrush(QColor(*self.colors[idx], 255)) pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0) @@ -131,21 +143,18 @@ class SurfacePlots(QWidget): 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 + wall.setBrush(QBrush(QColor(*self.color_impenetrable))) for text in self.texts: text.setColor(self.text_color) def plot_walls(self): + """Plot walls""" 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.setBrush(QBrush(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)) @@ -166,13 +175,21 @@ class SurfacePlots(QWidget): 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!") + raise ValueError(f"Plot {name} not found!") for name, plot in self.plots.items(): plot["widget"].disableAutoRange() - def update_surfaces(self, scene, data): + def update_surfaces(self, scene: Literal["assistant", "reality"], data: SurfaceDict): + """Update the curves of the plot + + Args: + scene (str): The scene to update, either "assistant" or "reality". + data (DataDict): The new data to plot, with keys "x" and "y", + each containing a list of values. + """ self.surfaces[scene] = data for name, device in self.surfaces[scene].items(): + device = cast(DataDict, device) 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([]) @@ -195,7 +212,7 @@ class SideviewPlot(QWidget): self.color_impenetrable = (0, 0, 0) self.colors = [(255, 255, 0), (255, 0, 255)] - self.data = { + self.data: dict[str, DataDict] = { "assistant": {"x": [0, 1000, 2000], "y": [0, 20, 30]}, "reality": {"x": [0, 1000, 2000], "y": [0, 15, 50]}, } @@ -207,7 +224,7 @@ class SideviewPlot(QWidget): for idx, scene in enumerate(self.data.keys()): if scene in "assistant": - pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DotLine) + pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine) z_value = 2 else: pen = pg.mkPen(color=self.colors[idx], width=2) @@ -220,8 +237,8 @@ class SideviewPlot(QWidget): 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.setXRange(0, 25000, 0.1) # pylint: disable=E1121 # type: ignore + self.plot_widget.setYRange(-20, 120, 0.1) # pylint: disable=E1121 # type: ignore self.plot_widget.setMenuEnabled(False) self.plot_widget.hideButtons() @@ -233,8 +250,13 @@ class SideviewPlot(QWidget): self.apply_theme() - def apply_theme(self, theme=None): + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ if theme is None: app = QApplication.instance() theme = app.theme.theme # type: ignore @@ -260,8 +282,8 @@ class SideviewPlot(QWidget): 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) + brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.PenStyle.DashLine) else: brush = QBrush(QColor(*self.colors[idx], 255)) pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3) @@ -270,14 +292,13 @@ class SideviewPlot(QWidget): 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 + wall.setBrush(QBrush(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): + """Plot vacuum pipes""" pipes = pipe_geometries() for pipe in pipes: self.pipes.append( @@ -287,17 +308,23 @@ class SideviewPlot(QWidget): ) def plot_walls(self): + """Plot walls""" 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.setBrush(QBrush(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): + def update_curves(self, scene: Literal["assistant", "reality"], data: DataDict): + """Update the curves of the plot + + Args: + scene (str): The scene to update, either "assistant" or "reality". + data (DataDict): The new data to plot, with keys "x" and "y", + each containing a list of values. + """ 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 index 31723b3..694e1b4 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py @@ -9,7 +9,7 @@ from debye_bec.bec_widgets.widgets.qt_widgets import Button, Group class SettingsPanel(QWidget): - """Right-side control panel: input field, indicator, send, recording.""" + """Settings panel for the digital twin widget""" def __init__(self, parent=None): super().__init__(parent) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/types.py b/debye_bec/bec_widgets/widgets/digital_twin/types.py new file mode 100644 index 0000000..737845b --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/types.py @@ -0,0 +1,34 @@ +"""Types used for plotting data""" + +from typing import TypedDict + + +class DataDict(TypedDict): + """ + Typed dictionary representing plot data. + + Attributes: + x (list[float]): List of x-axis values. + y (list[float]): List of y-axis values. + """ + + x: list + y: list + + +class SurfaceDict(TypedDict): + """ + Typed dictionary representing the surfaces of a scene, + grouping plot data by surface type. + + Attributes: + cm (DataDict): Data for the cm surface. + mo1_1 (DataDict): Data for the mo1_1 surface. + mo1_2 (DataDict): Data for the mo1_2 surface. + fm (DataDict): Data for the fm surface. + """ + + cm: DataDict + mo1_1: DataDict + mo1_2: DataDict + fm: DataDict From 5a54675f1e90ef87460ee6f69751c94a7392b555 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 19 May 2026 08:42:14 +0200 Subject: [PATCH 3/5] refactoring --- .../widgets/digital_twin/calc_positions.py | 287 ++++++++++-------- .../widgets/digital_twin/calc_sideview.py | 25 +- .../widgets/digital_twin/calc_surfaces.py | 107 +++---- .../widgets/digital_twin/calc_varia.py | 274 ++++++++++++++--- .../widgets/digital_twin/digital_twin.py | 130 ++++++-- .../bec_widgets/widgets/digital_twin/types.py | 41 ++- 6 files changed, 592 insertions(+), 272 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py index 55a1cca..f463286 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py @@ -1,242 +1,259 @@ -import os +""" +Calculates the positions of axes based on a beamline config +""" + import numpy as np from bec_lib import bec_logger -os.environ["USE_XRT"] = "False" import debye_bec.bec_widgets.widgets.x01da_parameters as bl +from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict logger = bec_logger.logger -def calc_positions(cfg): + +def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: + """ + Calculates the positions of axes based on a beamline config. + + Args: + cfg(ConfigDict): Dictionary with beamline config + + Returns: + dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries + containing a "value" key with the corresponding float value (position). + """ pos = {} ## FE slits - trxr = -np.arctan(cfg['h_acc'])*bl.feSlits.center1[1] - trxw = (np.arctan(cfg['h_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1] - tryb = -np.arctan(cfg['v_acc'])*bl.feSlits.center1[1] - tryt = (np.arctan(cfg['v_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1] + trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1] + trxw = ( + (np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]) + / bl.feSlits.center1[1] + * bl.feSlits.center2[1] + ) + tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1] + tryt = ( + (np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]) + / bl.feSlits.center1[1] + * bl.feSlits.center2[1] + ) - # trxw_proj = trxw/bl.feSlits.center2[1]*bl.feSlits.center1[1] - # tryt_proj = tryt/bl.feSlits.center2[1]*bl.feSlits.center1[1] - - # xcen = (trxr + trxw) / 2 - # ycen = (tryb + tryt) / 2 xgap = trxw - trxr ygap = tryt - tryb - pos['sldi_gapx'] = {'value': xgap} - pos['sldi_gapy'] = {'value': ygap} + pos["sldi_gapx"] = {"value": xgap} + pos["sldi_gapy"] = {"value": ygap} ## Collimating Mirror - obj_dist = bl.cm.center[1] # object distance - beam_vs = 2 * obj_dist * np.tan(cfg['v_acc']) # vertical size of beam after CM + obj_dist = bl.cm.center[1] # object distance + beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM # TRX - try: - index = bl.cm.surface.index(cfg['cm_stripe']) - except: + if cfg["cm_stripe"] in bl.cm.surface: + index = bl.cm.surface.index(cfg["cm_stripe"]) + else: raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!") cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 - pos['cm_trx'] = {'value': cm_trx} + pos["cm_trx"] = {"value": cm_trx} # TRY - height = obj_dist * np.tan(cfg['v_acc'])**2 * 1 / np.tan(cfg['cm_pitch']) - pos['cm_try'] = {'value': height} + height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"]) + pos["cm_try"] = {"value": height} # Pitch - pos['cm_rotx'] = {'value': -cfg["cm_pitch"]*1e3} # invert and convert to mrad (same as EGU of rotx axis) + pos["cm_rotx"] = { + "value": -cfg["cm_pitch"] * 1e3 + } # invert and convert to mrad (same as EGU of rotx axis) # Bending Radius - radius = 2. * obj_dist / np.sin(cfg['cm_pitch']) # Elements of modern X-ray Physics, page 108 ff. - pos['cm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km + radius = ( + 2.0 * obj_dist / np.sin(cfg["cm_pitch"]) + ) # Elements of modern X-ray Physics, page 108 ff. + pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km ## Monochromator - # Bragg Angle - # if cfg['mo1_mode'] == 'Monochromatic': - # # Add 2x CM pitch to the bragg angle - # bragg = ((2 * cfg['cm_pitch']) + cfg['mo1_bragg']) / np.pi * 180 - # elif cfg['mo1_mode'] == 'Pinkbeam': - # # Align xtal surfaces parallel to beam - # bragg = (2 * cfg['cm_pitch']) / np.pi * 180 - # else: - # raise Exception('Monochromator mode not supported') - if cfg['mo1_mode'] == 'Monochromatic': + if cfg["mo1_mode"] == "Monochromatic": # Add 2x CM pitch to the bragg angle - bragg = cfg['mo1_bragg'] - elif cfg['mo1_mode'] == 'Pinkbeam': + bragg = cfg["mo1_bragg"] + elif cfg["mo1_mode"] == "Pinkbeam": # Align xtal surfaces parallel to beam - bragg = 0 + bragg = 0 else: - raise Exception('Monochromator mode not supported') - pos['mo1_bragg_angle'] = {'value': bragg/np.pi*180} # Bragg angle in deg + raise ValueError("Monochromator mode not supported") + pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg # TRY, Height - l = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg']) - yhor = l*np.cos(2.*(cfg['mo1_bragg']+cfg['cm_pitch'])) - yver = yhor*np.tan(2.*cfg['cm_pitch']) + l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) + yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) + yver = yhor * np.tan(2.0 * cfg["cm_pitch"]) - if cfg['mo1_mode'] == 'Monochromatic': - beamOffsetCCM = l*np.sin(2.*(cfg['mo1_bragg']+cfg['cm_pitch']))-yver # Resultat ist korrekt! - elif cfg['mo1_mode'] == 'Pinkbeam': - beamOffsetCCM = 0 + if cfg["mo1_mode"] == "Monochromatic": + beam_offset_mo1 = ( + l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver + ) # Resultat ist korrekt! + elif cfg["mo1_mode"] == "Pinkbeam": + beam_offset_mo1 = 0 else: - raise Exception('Monochromator mode not supported') + raise ValueError("Monochromator mode not supported") def csc(a): - return 1/np.sin(a) + return 1 / np.sin(a) def cot(a): - return 1/np.tan(a) + return 1 / np.tan(a) # calculate height of center of first crystal surface - f = bl.mo1.rotOffset # rotation offset, mm - # logger.info(f'f = {f}') - d = bl.mo1.heightOffset # xtal height offset, mm - # logger.info(f'd = {d}') - c = d*csc(cfg['mo1_bragg'])-f*cot(cfg['mo1_bragg']) - # logger.info(f'c = {c}') + f = bl.mo1.rotOffset # rotation offset, mm + d = bl.mo1.heightOffset # xtal height offset, mm + c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"]) # Calculate height of center of rotation - b = np.sqrt(d**2*csc(cfg['mo1_bragg'])**2-2*d*f*cot(cfg['mo1_bragg'])*csc(cfg['mo1_bragg'])+f**2*cot(cfg['mo1_bragg'])**2+f**2) - # logger.info(f'b = {b}') - h = np.cos(np.pi/2-np.arctan(f/c)-cfg['mo1_bragg']-2*cfg['cm_pitch'])*b - # logger.info(f'h = {h}') - h2 = ((bl.mo1.center[1] - bl.cm.center[1])-np.sqrt(b**2-h**2))*np.tan(2*cfg['cm_pitch']) - # logger.info(f'mo1 = {bl.mo1.center[1]}') - # logger.info(f'cm = {bl.cm.center[1]}') - # logger.info(f'pitch = {cfg["cm_pitch"]}') - # logger.info(f'h2 = {h2}') - #TODO Mono height not exactly the same as in raytracing - heightCCM1real = h + h2 # per design, the height should not change if the pitch of the CM is not changed! - # heightCCM1real = heightCCM1real - 30 # Zero position of stage is at 1430 mm from ground. - if cfg['mo1_mode'] == 'Monochromatic': + b = np.sqrt( + d**2 * csc(cfg["mo1_bragg"]) ** 2 + - 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"]) + + f**2 * cot(cfg["mo1_bragg"]) ** 2 + + f**2 + ) + h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b + h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan(2 * cfg["cm_pitch"]) + height_mo1_real = ( + h + h2 + ) # per design, the height should not change if the pitch of the CM is not changed! + if cfg["mo1_mode"] == "Monochromatic": pass - elif cfg['mo1_mode'] == 'Pinkbeam': - heightCCM1real = heightCCM1real - 13 # Move down to let beam pass between both crystal without touching copper cooler + elif cfg["mo1_mode"] == "Pinkbeam": + height_mo1_real = ( + height_mo1_real - 13 + ) # Move down to let beam pass between both crystal without touching copper cooler else: - raise Exception('Monochromator mode not supported') - pos['mo1_try'] = {'value': heightCCM1real} + raise ValueError("Monochromator mode not supported") + pos["mo1_try"] = {"value": height_mo1_real} # TRX, Crystal selection - if cfg['mo1_mode'] == 'Monochromatic': - try: - xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + if cfg["mo1_mode"] == "Monochromatic": + xtal = cfg["mo1_xtal"].translate( + str.maketrans("", "", "()") + ) # Remove brackets from xtal name to conform with parameters + if xtal in bl.mo1.xtal: index = bl.mo1.xtal.index(xtal) - except: + else: raise ValueError(f"Requested xtal {xtal} not found in parameters!") - pos['mo1_trx'] = {'value': bl.mo1.xtalOffsetX[index]} + pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]} else: - pos['mo1_trx'] = {'value': 0} + pos["mo1_trx"] = {"value": 0} - - #TODO move to mono, calc for beam Z-movement between crystal surfaces - diag = bl.mo1.xtalGap[0] / np.sin(cfg['mo1_bragg']) # Calculations for Mono - dz = diag * np.cos(2 * (cfg['cm_pitch'] + cfg['mo1_bragg'])) + diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono + dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) ## Slits 1 d = bl.opSlits1.center[1] - bl.cm.center[1] - dz - sl1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - pos['sl1_centery'] = {'value': sl1_beam_height} - pos['sl1_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam + sl1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 + pos["sl1_centery"] = {"value": sl1_beam_height} + pos["sl1_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam ## Beam Monitor 1 d = bl.opBM1.center[1] - bl.cm.center[1] - dz - # logger.info(f'distance: {d}') - # logger.info(f'cm pitch: {cfg["cm_pitch"]}') - # logger.info(f'mono offset: {beamOffsetCCM}') - bm1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - pos['bm1_try'] = {'value': bm1_beam_height} + bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 + pos["bm1_try"] = {"value": bm1_beam_height} ## Focusing Mirror p = bl.fm.center[1] - q = cfg['smpl'] - bl.fm.center[1] - f = (p*q)/(p+q) # focal length + q = cfg["smpl"] - bl.fm.center[1] + f = (p * q) / (p + q) # focal length # Bender radius - if cfg['fm_qy'] is None: - radius = 2 * q / np.sin(cfg['fm_rotx']) # ideal bending radius for focused beam + if cfg["fm_qy"] is None: + radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam else: - radius = 2 * cfg['fm_qy'] / np.sin(cfg['fm_rotx']) # ideal bending radius for unfocused beam - pos['fm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km + radius = ( + 2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"]) + ) # ideal bending radius for unfocused beam + pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km # Pitch d = bl.fm.center[1] - bl.cm.center[1] - dz - fm_rotx = 2 * cfg['cm_pitch'] - cfg['fm_rotx'] # calculate pitch in absolute values (according to horizontal plane) - pos['fm_rotx'] = {'value': -fm_rotx * 1e3} # invert and convert to mrad (same as EGU of rotx axis) + fm_rotx = ( + 2 * cfg["cm_pitch"] - cfg["fm_rotx"] + ) # calculate pitch in absolute values (according to horizontal plane) + pos["fm_rotx"] = { + "value": -fm_rotx * 1e3 + } # invert and convert to mrad (same as EGU of rotx axis) - if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'): + if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): # TRY - if cfg['fm_stripe'] in 'Rh (toroid)': + if cfg["fm_stripe"] in "Rh (toroid)": r = bl.fm.r[0] h_cyl = bl.fm.hToroid[0] - else: # PT toroid + else: # PT toroid r = bl.fm.r[1] h_cyl = bl.fm.hToroid[1] - widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc'] * 1e-3) - alpha = np.arccos(1 - widthBeam**2 / (2 * r**2)) + width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3) + alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) h = r - (r * np.cos(alpha / 2)) - fm_beam_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height'] - fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - h_cyl + h / 2) * cfg['fm_gain_height'] - pos['fm_try'] = {'value': fm_height} + fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"] + fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[ + "fm_gain_height" + ] + pos["fm_try"] = {"value": fm_height} # TRX - if cfg['fm_stripe'] in 'Rh (toroid)': - x_cyl = - bl.fm.xToroid[0] + if cfg["fm_stripe"] in "Rh (toroid)": + x_cyl = -bl.fm.xToroid[0] else: - x_cyl = - bl.fm.xToroid[1] - pos['fm_trx'] = {'value': x_cyl} + x_cyl = -bl.fm.xToroid[1] + pos["fm_trx"] = {"value": x_cyl} - elif cfg['fm_stripe'] in ('Rh (flat)', 'Pt (flat)'): + elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"): # TRY - fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height'] + fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"] fm_beam_height = fm_height - pos['fm_try'] = {'value': fm_height} + pos["fm_try"] = {"value": fm_height} # TRX - if cfg['fm_stripe'] in 'Rh (flat)': - x_flat = - bl.fm.xFlat[0] + if cfg["fm_stripe"] in "Rh (flat)": + x_flat = -bl.fm.xFlat[0] else: - x_flat = - bl.fm.xFlat[1] - pos['fm_trx'] = {'value': x_flat} + x_flat = -bl.fm.xFlat[1] + pos["fm_trx"] = {"value": x_flat} else: - raise Exception('FM Stripe selection not valid') + raise ValueError("FM Stripe selection not valid") - pos['fm_roty'] = {'value': 0} - pos['fm_rotz'] = {'value': 0} + pos["fm_roty"] = {"value": 0} + pos["fm_rotz"] = {"value": 0} ## Slits 2 d = bl.opSlits2.center[1] - bl.fm.center[1] - sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])) - pos['sl2_centery'] = {'value': sl2_beam_height} - pos['sl2_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam + sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) + pos["sl2_centery"] = {"value": sl2_beam_height} + pos["sl2_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam ## Beam Monitor 2 d = bl.opBM2.center[1] - bl.fm.center[1] - bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])) - pos['bm2_try'] = {'value': bm2_beam_height} + bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) + pos["bm2_try"] = {"value": bm2_beam_height} ## Optical Table # TRY d = bl.ehWindow.center[1] - bl.fm.center[1] - ot_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])) - # logger.info(fm_height) - # logger.info(d * np.tan((2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))) - pos['ot_try'] = {'value': ot_height} + ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) + pos["ot_try"] = {"value": ot_height} # Pitch - ot_pitch = - (2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']) - pos['ot_rotx'] = {'value': ot_pitch * 1e3} + ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]) + pos["ot_rotx"] = {"value": ot_pitch * 1e3} # TRZ ES1 - ot_es1_trz = cfg['smpl'] - pos['ot_es1_trz'] = {'value': ot_es1_trz} + ot_es1_trz = cfg["smpl"] + pos["ot_es1_trz"] = {"value": ot_es1_trz} # ES0 exit window - pos['es0wi_try'] = {'value': 5} # At 5mm, the middle of the window is 500 mm from the table (neutral position) + pos["es0wi_try"] = { + "value": 5 + } # At 5mm, the middle of the window is 500 mm from the table (neutral position) return pos diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py index 55d09c5..092d5d4 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py @@ -1,15 +1,23 @@ +""" +Calculates the sideview coordinates based on a beamline config. +""" + import numpy as np import debye_bec.bec_widgets.widgets.x01da_parameters as bl -from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict +from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, DataDict -def calc_sideview(cfg): +def calc_sideview(cfg: ConfigDict) -> DataDict: + """ + Calculates the sideview coordinates based on a beamline config. - # Calculate height of beam after CM - # height = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) + Args: + cfg(ConfigDict): Dictionary with beamline config - # beam height (Y=height, Z=along beam) + Returns: + DataDict: Sideview data + """ beam: DataDict = {"x": [], "y": []} @@ -59,11 +67,4 @@ def calc_sideview(cfg): + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) ) - dy_fm_ex = beam["y"][-1] - beam["y"][-2] - dz_fm_ex = beam["x"][-1] - beam["x"][-2] - dz_fm_win = bl.ehWindow.center[1] - beam["x"][-2] - h_at_win = beam["y"][-2] + dy_fm_ex / dz_fm_ex * dz_fm_win - - # beam["heightWindow"] = h_at_win - return beam diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py index d8e80ce..168f977 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py @@ -1,17 +1,28 @@ -import os +""" +Calculates the surface coordinates based on a beamline config. +""" + import re import numpy as np from bec_lib import bec_logger +import debye_bec.bec_widgets.widgets.x01da_parameters as bl +from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, SurfaceDict + logger = bec_logger.logger -os.environ["USE_XRT"] = "False" -import debye_bec.bec_widgets.widgets.x01da_parameters as bl -from debye_bec.bec_widgets.widgets.digital_twin.types import SurfaceDict +def calc_surfaces(cfg: ConfigDict) -> SurfaceDict: + """ + Calculates the surface coordinates based on a beamline config. -def calc_surfaces(cfg): + Args: + cfg(ConfigDict): Dictionary with beamline config + + Returns: + SurfaceDict: Surface data + """ out: SurfaceDict = { "cm": {"x": [], "y": []}, @@ -45,39 +56,39 @@ def calc_surfaces(cfg): ) # Remove brackets from xtal name to conform with parameters index = bl.mo1.xtal.index(xtal) - xtalPos = bl.mo1.xtalOffsetX[index] - xtalLength1 = bl.mo1.xtalLength1[index] - xtalLength2 = bl.mo1.xtalLength2[index] + xtal_pos = bl.mo1.xtalOffsetX[index] + xtal_length_1 = bl.mo1.xtalLength1[index] + xtal_length_2 = bl.mo1.xtalLength2[index] - widthBeam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"]) + width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"]) - heightBeam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) - w = heightBeam / np.sin(cfg["mo1_bragg"]) + height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) + w = height_beam / np.sin(cfg["mo1_bragg"]) if cfg["mo1_mode"] in "Monochromatic": out["mo1_1"]["x"] = [ - xtalPos - widthBeam / 2, - xtalPos + widthBeam / 2, - xtalPos + widthBeam / 2, - xtalPos - widthBeam / 2, + xtal_pos - width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos - width_beam / 2, ] out["mo1_1"]["y"] = [ - xtalLength1 / 2 - c - w / 2, - xtalLength1 / 2 - c - w / 2, - xtalLength1 / 2 - c + w / 2, - xtalLength1 / 2 - c + w / 2, + xtal_length_1 / 2 - c - w / 2, + xtal_length_1 / 2 - c - w / 2, + xtal_length_1 / 2 - c + w / 2, + xtal_length_1 / 2 - c + w / 2, ] out["mo1_2"]["x"] = [ - xtalPos - widthBeam / 2, - xtalPos + widthBeam / 2, - xtalPos + widthBeam / 2, - xtalPos - widthBeam / 2, + xtal_pos - width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos - width_beam / 2, ] out["mo1_2"]["y"] = [ - -xtalLength2 / 2 + e - w / 2, - -xtalLength2 / 2 + e - w / 2, - -xtalLength2 / 2 + e + w / 2, - -xtalLength2 / 2 + e + w / 2, + -xtal_length_2 / 2 + e - w / 2, + -xtal_length_2 / 2 + e - w / 2, + -xtal_length_2 / 2 + e + w / 2, + -xtal_length_2 / 2 + e + w / 2, ] else: # Pinkbeam out["mo1_1"]["x"] = [] @@ -98,50 +109,44 @@ def calc_surfaces(cfg): r = bl.fm.r[index] off = -cfg["fm_trx"] - widthBeam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"]) + width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"]) if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): - l = heightBeam / np.sin(cfg["fm_rotx"]) - alpha = np.arccos(1 - widthBeam**2 / (2 * r**2)) + l = height_beam / np.sin(cfg["fm_rotx"]) + alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) h = r - (r * np.cos(alpha / 2)) z = h / np.tan(cfg["fm_rotx"]) - x = [off - widthBeam / 2, off - widthBeam / 2] + x = [off - width_beam / 2, off - width_beam / 2] y = [l / 2 - z / 2, -l / 2 - z / 2] - # logger.info(f'stripe: {cfg["fm_stripe"]}') - # logger.info(f'fm_rotx: {cfg["fm_rotx"]}') - # logger.info(f'h: {h}') - # logger.info(f'z: {z}') - # logger.info(f'r: {r}') - res = 20 - xElipse = np.linspace(0, np.pi, res) - yElipse = np.linspace(0, np.pi, res) - xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse] - yElipse = [widthBeam * np.sin(i) * z / widthBeam - l / 2 - z / 2 for i in yElipse] + x_elipse = np.linspace(0, np.pi, res) + y_elipse = np.linspace(0, np.pi, res) + x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] + y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse] - x.extend(xElipse) - y.extend(yElipse) + x.extend(x_elipse) + y.extend(y_elipse) - x.extend([off + widthBeam / 2, off + widthBeam / 2]) + x.extend([off + width_beam / 2, off + width_beam / 2]) y.extend([-l / 2 - z / 2, l / 2 - z / 2]) res = 50 - xElipse = np.linspace(np.pi, 0, res) - yElipse = np.linspace(np.pi, 0, res) - xElipse = [-widthBeam / 2 * np.cos(i) + off for i in xElipse] - yElipse = [widthBeam * np.sin(i) * z / widthBeam + l / 2 - z / 2 for i in yElipse] + x_elipse = np.linspace(np.pi, 0, res) + y_elipse = np.linspace(np.pi, 0, res) + x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] + y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse] - x.extend(xElipse) - y.extend(yElipse) + x.extend(x_elipse) + y.extend(y_elipse) out["fm"]["x"] = x out["fm"]["y"] = y else: # flat surface, no toroid - l = heightBeam / np.sin(cfg["fm_rotx"]) + l = height_beam / np.sin(cfg["fm_rotx"]) w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"]) w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"]) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py index 710212a..9ec7cc8 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py @@ -1,4 +1,9 @@ +""" +Various calculations for the digital twin +""" + import re +from typing import Literal, cast import numpy as np from bec_lib import bec_logger @@ -9,19 +14,41 @@ import debye_bec.bec_widgets.widgets.x01da_parameters as bl logger = bec_logger.logger +H = 6.62606957e-34 +E = 1.602176634e-19 +C = 299792458 +RE = 2.8179e-15 -def sldi_gap_to_acc(sldi_gapx, sldi_gapy): + +def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]: + """ + Calculate the slits acceptance based on the gap values + + Args: + sldi_gapx(float): GAPX value of the slits in mm + sldi_gapy(float): GAPY value of the slits in mm + + Returns: + tuple[float, float]: Horizontal and vertical acceptance in rad + """ d1 = bl.feSlits.center1[1] d2 = bl.feSlits.center2[1] h_acc = np.tan(sldi_gapx / (d2 + d1)) v_acc = np.tan(sldi_gapy / (d2 + d1)) - - # h_acc = np.tan(sldi_gapx / (2 * d1)) - # v_acc = np.tan(sldi_gapy / (2 * d1)) return h_acc, v_acc -def cm_trx_to_stripe(cm_trx): +def cm_trx_to_stripe(cm_trx: float) -> str | None: + """ + Based on the trx value of the collimating mirror, return + the correct stripe + + Args: + cm_trx(float): Collimating mirror trx value + + Returns + str | None: Stripe of the mirror, None if not found + """ cm_stripe = None for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): if low <= cm_trx <= high: @@ -29,14 +56,34 @@ def cm_trx_to_stripe(cm_trx): return cm_stripe -def cm_stripe_to_trx(cm_stripe): +def cm_stripe_to_trx(cm_stripe: str) -> float | None: + """ + Based on the stripe of the collimating mirror, return + the trx value + + Args: + cm_stripe(str): Stripe of the collimating mirror + + Returns: + float | None: TRX value of the stripe. None if not found + """ for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): if cm_stripe == name: return -(low + high) / 2 - return 0 + return None -def fm_trx_to_stripe(fm_trx): +def fm_trx_to_stripe(fm_trx: float) -> str | None: + """ + Based on the trx value of the focusing mirror, return + the correct stripe + + Args: + fm_trx(float): focusing mirror trx value + + Returns + str | None: Stripe of the mirror, None if not found + """ fm_stripe = None for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): if low <= fm_trx <= high: @@ -47,17 +94,37 @@ def fm_trx_to_stripe(fm_trx): return fm_stripe -def fm_stripe_to_trx(fm_stripe): +def fm_stripe_to_trx(fm_stripe: str) -> float | None: + """ + Based on the stripe of the focusing mirror, return + the trx value + + Args: + fm_stripe(str): Stripe of the focusing mirror + + Returns: + float | None: TRX value of the stripe. None if not found + """ for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): if fm_stripe == name + " (flat)": return (low + high) / 2 for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): if fm_stripe == name + " (toroid)": return -(low + high) / 2 - return 0 + return None -def mo1_energy_resolution(xtal, energy): +def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float: + """ + Calculate the energy resolution of the monochromator + + Args: + xtal(str): Xtal name. "Si111" or "Si311" + energy(float): Energy in eV + + Returns: + float: Energy resolution in eV + """ index = bl.mo1.xtal.index(xtal) crystal = bl.mo1.material1[index] @@ -69,29 +136,54 @@ def mo1_energy_resolution(xtal, energy): # FWHM of the DCM curve spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0) - r1, r2 = spline.roots() + roots = cast(np.ndarray, spline.roots()) + r1, r2 = float(roots[0]), float(roots[1]) fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad # Energy resolution - theta_B = crystal.get_Bragg_angle(energy) - dE_over_E = fwhm_rad / np.tan(theta_B) - dE = dE_over_E * energy + theta_b = crystal.get_Bragg_angle(energy) + de_over_e = fwhm_rad / np.tan(theta_b) + de = de_over_e * energy # logger.info(f"DCM FWHM : {r2-r1:.2f} µrad") # logger.info(f"ΔE/E : {dE_over_E:.2e}") # logger.info(f"ΔE : {dE:.3f} eV at {E} eV") - return dE + return de -def cm_reflectivity(cm_stripe, cm_pitch, energy): +def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float: + """ + Calculate the reflectivity of the mirror stripe based + on the pitch and energy. + + Args: + cm_stripe(str): Mirror stripe + cm_pitch(float): Pitch of the mirror (beam incidence angle) + energy(float): Energy of the beam in eV + + Returns: + float: Reflectivity [0-1] + """ index = bl.cm.surface.index(cm_stripe) - rs, rp = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2] + rs, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2] refl = abs(rs) ** 2 return refl -def fm_reflectivity(fm_stripe, fm_pitch, energy): +def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float: + """ + Calculate the reflectivity of the mirror stripe based + on the pitch and energy. + + Args: + cm_stripe(str): Mirror stripe + cm_pitch(float): Pitch of the mirror (beam incidence angle) + energy(float): Energy of the beam in eV + + Returns: + float: Reflectivity [0-1] + """ if fm_stripe in ("Rh (toroid)", "Pt (toroid)"): surface = bl.fm.surfaceToroid material = bl.fm.materialToroid @@ -102,35 +194,75 @@ def fm_reflectivity(fm_stripe, fm_pitch, energy): material = bl.fm.materialFlat stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() index = surface.index(stripe) - rs, rp = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2] + rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2] refl = abs(rs) ** 2 return refl -def mo1_bragg_angle(mo_mode, d_spacing, energy, cm_pitch): - H = 6.62606957e-34 - E = 1.602176634e-19 - C = 299792458 +def mo1_bragg_angle( + mo_mode: Literal["Monochromatic", "Pinkbeam"], d_spacing: float, energy: float, cm_pitch: float +) -> tuple[float, float]: + """ + Calculate the bragg angle of the monochromator. + Corrects for the collimating mirror pitch. + + Args: + mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam" + d_spacing(float): D-spacing of the crystal in Angstrom + energy(float): Energy of the beam in eV + cm_pitch(float): Pitch of collimating mirror in rad + + Returns: + tuple[float, float]: Bragg angle and corrected bragg angle + """ wl = C * H / (E * energy) val = wl / (2 * d_spacing * 1e-10) bragg_angle = 0 if val > -1 and val < 1: bragg_angle = np.asin(val) - if mo_mode in "Monochromatic": + if mo_mode == "Monochromatic": # Add 2x CM pitch to the bragg angle bragg_angle_cor = (2 * cm_pitch) + bragg_angle - elif mo_mode in "Pinkbeam": + else: # Align xtal surfaces parallel to beam bragg_angle_cor = 2 * cm_pitch return bragg_angle, bragg_angle_cor def fm_ideal_pitch( - fm_focus, fm_stripe, smpl, sldi_hacc=None, sldi_vacc=None, fm_focx=None, fm_focy=None -): + fm_focus: Literal["Defocused", "Focused", "Manual"], + fm_stripe: str, + smpl: float, + sldi_hacc: float | None = None, + sldi_vacc: float | None = None, + fm_focx: float | None = None, + fm_focy: float | None = None, +) -> tuple[float, float | None]: + """ + Calculates the ideal pitch for the focusing mirror depending on the + focusing strategy. + If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy + must be provided. + + Args: + fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual + fm_stripe(str): Mirror stripe + smpl(float): Sample position in mm from source + sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None + sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None + fm_focx(float): Requested horizontal spot size in mm. Defaults to None + fm_focy(float): Requested vertical spot size in mm. Defaults to None + + Returns: + tuple[float, float | None]: Pitch of mirror in rad, qy in mm + """ p = bl.fm.center[1] # posFM q = smpl - bl.fm.center[1] # dist posFM to posEX if fm_focus in "Defocused": + assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode" + assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode" + assert fm_focx is not None, "fm_focx must be provided for Defocused mode" + assert fm_focy is not None, "fm_focy must be provided for Defocused mode" a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror b = ( 2 * np.tan(sldi_vacc) * bl.cm.center[1] @@ -151,45 +283,77 @@ def fm_ideal_pitch( return pitch, qy -def cm_critical_angle(cm_stripe, energy): +def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float: + """ + Calculate the critical angle of the mirror stripe + + Args: + cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh" + energy(float): Energy in eV + + Returns: + float: Critical angle in rad + """ if cm_stripe in "Si": stripe = bl.stripeSi elif cm_stripe in "Pt": stripe = bl.stripePt - elif cm_stripe in "Rh": - stripe = bl.stripeRh else: - raise Exception(f"Stripe {stripe} not found in beamline parameters!") + stripe = bl.stripeRh w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m] - # Calculate critical angle for mirror f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy)) - numberDensity = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3) - criticalAngle = np.sqrt(numberDensity * 2.8179e-15 * w**2 * f1 / np.pi) - return criticalAngle + number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3) + critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi) + return critical_angle -def mirror_surface_geometries(mirror): +def mirror_surface_geometries( + mirror: Literal["cm", "fm_toroid", "fm_flat"], +) -> dict[str, tuple[float, float, float, float]]: + """ + Return the mirror stripe geometries + + Args: + mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat" + + Returns: + dict[str, tuple[float, float, float, float]]: Dictionary mapping surface + names to tuples of (x, y, width, height). + """ if mirror in "cm": surface = bl.cm.surface - limOptX = bl.cm.limOptX - limOptY = bl.cm.limOptY + lim_opt_x = bl.cm.limOptX + lim_opt_y = bl.cm.limOptY elif mirror in "fm_toroid": surface = bl.fm.surfaceToroid - limOptX = bl.fm.limOptXToroid - limOptY = bl.fm.limOptYToroid + lim_opt_x = bl.fm.limOptXToroid + lim_opt_y = bl.fm.limOptYToroid elif mirror in "fm_flat": surface = bl.fm.surfaceFlat - limOptX = bl.fm.limOptXFlat - limOptY = bl.fm.limOptYFlat + lim_opt_x = bl.fm.limOptXFlat + lim_opt_y = bl.fm.limOptYFlat else: raise ValueError(f"Requested mirror {mirror} not available!") geom = {} - for sf, lx, hx, ly, hy in zip(surface, limOptX[0], limOptX[1], limOptY[0], limOptY[1]): + for sf, lx, hx, ly, hy in zip(surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1]): geom[sf] = (lx, ly, hx - lx, hy - ly) return geom -def mo_surface_geometries(mo, plane): +def mo_surface_geometries( + mo: Literal["mo1"], plane: Literal[0, 1] +) -> dict[str, tuple[float, float, float, float]]: + """ + Return the monochromator xtal geometries + + Args: + mo(str): Monochromator. Only "mo1" implemented + plane(int): Surface of xtal. 0 and 1 (First and second) + + Returns: + dict[str, tuple[float, float, float, float]]: Dictionary mapping surface + names to tuples of (x, y, width, height). + """ if mo in "mo1": xtal = bl.mo1.xtal xtal_width = bl.mo1.xtalWidth @@ -199,14 +363,20 @@ def mo_surface_geometries(mo, plane): else: xtal_length = bl.mo1.xtalLength2 else: - raise ValueError(f"Requested mono {mo} not available!") + return {} geom = {} for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length): geom[sf] = (offx - w / 2, -length / 2, w, length) return geom -def wall_geometries(): +def wall_geometries() -> list[list[float]]: + """ + Return the wall geometries + + Returns: + list[list[float]]: List of [x, y, width, height] geometry values for each wall. + """ geom = [] for i, _ in enumerate(bl.walls.start): geom.append( @@ -220,7 +390,15 @@ def wall_geometries(): return geom -def pipe_geometries(): +def pipe_geometries() -> list[dict[str, np.ndarray]]: + """ + Return the wall geometries + + Returns: + list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y", + each containing a numpy array of two float values representing + the start and end coordinates of the pipe top and bottom edges. + """ pipes = [] for i, _ in enumerate(bl.vacuum_pipes.center): top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight 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 d98975d..985c8f5 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -4,7 +4,7 @@ Digital Twin: Custom BEC widget to support the beamline alignment. import sys from pathlib import Path -from typing import Literal +from typing import Literal, cast import numpy as np import yaml @@ -48,6 +48,7 @@ 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 +from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict logger = bec_logger.logger @@ -62,7 +63,7 @@ class DigitalTwin(BECWidget, QWidget): PLUGIN = True ICON_NAME = "lightbulb" - def __init__(self, parent=None, *arg, **kwargs): + def __init__(self, *arg, parent=None, **kwargs): super().__init__(parent=parent, theme_update=True, *arg, **kwargs) self.get_bec_shortcuts() @@ -115,15 +116,15 @@ class DigitalTwin(BECWidget, QWidget): self.settings.reload_offsets.clicked_connect(self.load_offsets) self.settings.unload_offsets.clicked_connect(self.unload_offsets) - self.bragg_angle = 0 - self.qy = 0 + self.bragg_angle = 0.0 + self.qy = 0.0 self.offsets = {} # Initialize all values self.load_offsets(recalculate=False) self.calc_assistant(identifier="init") - # Timer: update plot every 1 second + # Timer: update plots every 1 second self._timer = QTimer(self) self._timer.setInterval(100) self._timer.timeout.connect(self.calc_reality) @@ -142,6 +143,12 @@ class DigitalTwin(BECWidget, QWidget): @SafeSlot() def check_config(self, *args): + """ + Checks the BEC config and opens a window if not all necessary + devices are loaded in the config. If called from a slot from + BEC dispatcher whenever there is a config update, stop the timer + that updates the plot in the background. + """ reload = (args[0] if args else {}).get("action") == "reload" if reload: self._timer.stop() @@ -221,14 +228,21 @@ class DigitalTwin(BECWidget, QWidget): dialog.show() info.setMinimumHeight(info.heightForWidth(info.width())) if dialog.exec_() == QDialog.DialogCode.Rejected: - app = QApplication.instance() - if app is not None: - app.exit(0) + running_app = QApplication.instance() + if running_app is not None: + running_app.exit(0) if reload: self._timer.start() @SafeSlot() - def calc_assistant(self, *args, **kwargs): + def calc_assistant(self, *_, **kwargs): + """ + Calculates various values for the assistant. + If called from a qt slot, the identifier represents + the button pressed / value changed. Based on the identifier, + calculate different values. + Note: identifier=init calculates all values + """ identifier = kwargs["identifier"] match identifier: case "init": @@ -281,6 +295,13 @@ class DigitalTwin(BECWidget, QWidget): self.calc_assistant_surfaces() def get_assistant_config(self, apply_offset: bool = False): + """ + Assembles the digital twin config from the assistants input. + + Args: + apply_offset(bool): Applies the offset values to the config. + Defaults to False + """ fm_focus = self.input.fm_focus.currentText() if fm_focus in "Manual": fm_rotx = self.input.fm_rotx.value() @@ -297,7 +318,10 @@ class DigitalTwin(BECWidget, QWidget): fm_stripe = self.input.fm_stripe.currentText() fm_trx = fm_stripe_to_trx(fm_stripe) - config = { + assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!" + assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!" + + config: ConfigDict = { "energy": self.input.energy.value(), "h_acc": self.input.sldi_hacc.value(), "v_acc": self.input.sldi_vacc.value(), @@ -320,16 +344,10 @@ class DigitalTwin(BECWidget, QWidget): for axis, _ in config.items(): if axis in self.offsets: axis_offsets = self.offsets[axis] - logger.info(f"Axis: {axis}") if "modifier" in axis_offsets and "offset" in axis_offsets: for idx, rng in enumerate(axis_offsets["modifier"]["range"]): - logger.info(f"rng: {rng}") - logger.info(f'value: {config[axis_offsets["modifier"]["axis"]]}') if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]: - logger.info(f'offset: {axis_offsets["offset"][idx]}') - # logger.info(f"axis_data before: {axis_data}") config[axis] += axis_offsets["offset"][idx] - # logger.info(f"axis_data after: {axis_data}") break elif "offset" in axis_offsets: config[axis] += axis_offsets["offset"] @@ -344,6 +362,9 @@ class DigitalTwin(BECWidget, QWidget): return config def get_reality_config(self): + """ + Assembles the digital twin config based on the real axis positions. + """ mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"] if abs(mo1_trx) > 5: mo1_mode = "Monochromatic" @@ -361,7 +382,7 @@ class DigitalTwin(BECWidget, QWidget): 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! + raw = { # Config in SI units! "energy": mo1_bragg["mo1_bragg"]["value"], "h_acc": h_acc, "v_acc": v_acc, @@ -374,9 +395,11 @@ class DigitalTwin(BECWidget, QWidget): "fm_rotx": -fm_rotx_real * 1e-3, "fm_stripe": fm_stripe, "fm_trx": fm_trx, + "fm_qy": None, "fm_gain_height": 1, "smpl": smpl, } + config = cast(ConfigDict, raw) # logger.info(f'Config created: {config}') abs_open = self.dev.abs.read(cached=True)["abs_status_string"]["value"] == "OPEN" @@ -430,7 +453,12 @@ class DigitalTwin(BECWidget, QWidget): self.mover.abs.set_feedback(abs_open) return config - def adapt_reality(self, *args): + @SafeSlot() + def adapt_reality(self, *_): + """ + Based on the real axis positions, adjust the assistant to reflect + the reality. + """ 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"] @@ -474,7 +502,15 @@ class DigitalTwin(BECWidget, QWidget): self.input.smpl.set_number(pos["ot_es1_trz"]) self.calc_assistant(identifier="init") - def load_offsets(self, recalculate=True, *args): + @SafeSlot() + def load_offsets(self, *_, recalculate: bool = True): + """ + Loads the offsets from the file + + Args: + recalculate(bool): Recalculates the assistant values. + Defaults to True + """ file = Path(OFFSET_FILE) if not file.exists(): raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") @@ -490,11 +526,19 @@ class DigitalTwin(BECWidget, QWidget): if recalculate: self.calc_assistant(identifier="init") - def unload_offsets(self, *args): + @SafeSlot() + def unload_offsets(self, *_): + """ + Removes the offsets and recalculates the assistant values. + """ self.offsets = {} self.calc_assistant(identifier="init") def update_fm_mode(self): + """ + Updates the focusing mirror input group based on the + selection of the focus strategy. + """ fm_focus = self.input.fm_focus.currentText() if fm_focus in "Manual": self.input.fm_rotx.setVisible(True) @@ -515,22 +559,32 @@ class DigitalTwin(BECWidget, QWidget): self.input.fm_focy.setVisible(True) self.input.fm_rotx_ideal.setLabel("Incidence Angle for defocused beam") + @SafeSlot() def calc_reality(self): + """ + Updates the plots for the reality scene + """ config = self.get_reality_config() data = calc_sideview(config) self.sideview_plot.update_curves("reality", data=data) - # logger.info('Calc reality surfaces') surfaces = calc_surfaces(config) self.surface_plots.update_surfaces(scene="reality", data=surfaces) def calc_mo1_energy_resolution(self): + """ + Calculates the energy resolution of the monochromator + """ xtal = self.input.mo1_xtal.currentText().translate( str.maketrans("", "", "()") ) # Remove brackets from xtal name to conform with parameters + xtal = cast(Literal["Si111", "Si311"], xtal) energy = self.input.energy.value() self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy)) def calc_cm_reflectivity(self): + """ + Calculates the collimating mirror reflectivity + """ cm_stripe = self.input.cm_stripe.currentText() cm_pitch = -self.input.cm_pitch.value() * 1e-3 energy = self.input.energy.value() @@ -540,6 +594,9 @@ class DigitalTwin(BECWidget, QWidget): self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_fm_reflectivity(self): + """ + Calculates the focusing mirror reflectivity + """ fm_stripe = self.input.fm_stripe.currentText() fm_focus = self.input.fm_focus.currentText() if fm_focus in "Manual": @@ -553,6 +610,9 @@ class DigitalTwin(BECWidget, QWidget): self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_cm_fm_harm_suppr(self): + """ + Calculates the combined harmonics suppression of both mirrors + """ harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / ( self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value() ) @@ -562,16 +622,24 @@ class DigitalTwin(BECWidget, QWidget): ) def calc_assistant_sideview(self): + """ + Updates the sideview plot based on the assistant values + """ config = self.get_assistant_config(apply_offset=True) data = calc_sideview(config) self.sideview_plot.update_curves("assistant", data) def calc_assistant_surfaces(self): - # logger.info('Calc assistant surfaces') + """ + Updates the surface plot based on the assistant values + """ surfaces = calc_surfaces(self.get_assistant_config()) self.surface_plots.update_surfaces(scene="assistant", data=surfaces) def calc_positions(self): + """ + Calculates the positions for the axes based on the assistant values + """ out = calc_positions(self.get_assistant_config()) # Apply offsets @@ -628,13 +696,17 @@ class DigitalTwin(BECWidget, QWidget): else: raise ValueError(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() + mo1_mode = cast(Literal["Monochromatic", "Pinkbeam"], self.input.mo1_mode.currentText()) energy = self.input.energy.value() theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch) self.bragg_angle = theta self.input.mo1_bragg_angle.setValue(theta / np.pi * 180) def update_mo1_mode(self): + """ + Updates the monochromator input group based on the + selection of the mode. + """ if self.input.mo1_mode.currentText() in "Monochromatic": self.input.mo1_xtal.setVisible(True) self.input.mo1_bragg_angle.setVisible(True) @@ -645,7 +717,12 @@ class DigitalTwin(BECWidget, QWidget): self.input.mo1_eres.setVisible(False) def calc_fm_ideal_pitch(self): - fm_focus = self.input.fm_focus.currentText() + """ + Calculate the ideal pitch for the focusing mirror. + """ + fm_focus = cast( + Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText() + ) fm_stripe = self.input.fm_stripe.currentText() smpl = self.input.smpl.value() sldi_hacc = self.input.sldi_hacc.value() * 1e-3 @@ -659,7 +736,10 @@ class DigitalTwin(BECWidget, QWidget): self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3) def calc_cm_crit_pitch(self): - cm_stripe = self.input.cm_stripe.currentText() + """ + Calculate the critical pitch for the collimating mirror + """ + cm_stripe = cast(Literal["Si", "Pt", "Rh"], self.input.cm_stripe.currentText()) energy = self.input.energy.value() self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/types.py b/debye_bec/bec_widgets/widgets/digital_twin/types.py index 737845b..674b59f 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/types.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/types.py @@ -1,8 +1,47 @@ -"""Types used for plotting data""" +"""Types used for the beamline config and for plotting data""" from typing import TypedDict +class ConfigDict(TypedDict): + """ + Typed dictionary representing the beamline configuration. + + Attributes: + energy (float): Beam energy. + h_acc (float): Horizontal acceptance. + v_acc (float): Vertical acceptance. + cm_pitch (float): CM pitch angle. + cm_stripe (str): CM stripe name. + cm_trx (float): CM translation x. + mo1_mode (str): MO1 mode. + mo1_xtal (str): MO1 crystal. + mo1_bragg (float): MO1 Bragg angle. + fm_rotx (float): FM rotation x. + fm_stripe (str): FM stripe name. + fm_trx (float): FM translation x. + fm_qy (float): FM qy value. + fm_gain_height (int): FM gain height. + smpl (float): Sample value. + """ + + energy: float + h_acc: float + v_acc: float + cm_pitch: float + cm_stripe: str + cm_trx: float + mo1_mode: str + mo1_xtal: str + mo1_bragg: float + fm_rotx: float + fm_stripe: str + fm_trx: float + fm_qy: None | float + fm_gain_height: int + smpl: float + + class DataDict(TypedDict): """ Typed dictionary representing plot data. From ef4c82262c9a04aa88c1417edc92b32eaaf70e53 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 19 May 2026 10:45:42 +0200 Subject: [PATCH 4/5] refactoring --- .../widgets/digital_twin/digital_twin.py | 100 +++- .../widgets/digital_twin/move_widget.py | 9 +- .../bec_widgets/widgets/digital_twin/plots.py | 14 +- .../widgets/digital_twin/settings_panel.py | 11 +- debye_bec/bec_widgets/widgets/qt_widgets.py | 126 +++-- .../bec_widgets/widgets/x01da_parameters.py | 450 +++++++++--------- 6 files changed, 411 insertions(+), 299 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index 985c8f5..a9c87af 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -12,16 +12,19 @@ from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints from bec_widgets.utils.bec_dispatcher import BECDispatcher from bec_widgets.utils.bec_widget import BECWidget -from bec_widgets.utils.colors import apply_theme +from bec_widgets.utils.colors import apply_theme, get_accent_colors from bec_widgets.utils.error_popups import SafeSlot # pylint: disable=E0611 from qtpy.QtCore import Qt, QTimer +from qtpy.QtGui import QFont from qtpy.QtWidgets import ( QApplication, QDialog, + QDialogButtonBox, QHBoxLayout, QLabel, + QPlainTextEdit, QPushButton, QStyle, QVBoxLayout, @@ -113,8 +116,8 @@ class DigitalTwin(BECWidget, QWidget): self.input.smpl.value_changed_connect(self.calc_assistant) self.input.adapt_reality.clicked_connect(self.adapt_reality) - self.settings.reload_offsets.clicked_connect(self.load_offsets) - self.settings.unload_offsets.clicked_connect(self.unload_offsets) + self.settings.load_offsets.clicked_connect(self.load_offsets) + self.settings.show_offsets.clicked_connect(self.show_offsets) self.bragg_angle = 0.0 self.qy = 0.0 @@ -294,13 +297,16 @@ class DigitalTwin(BECWidget, QWidget): self.calc_assistant_sideview() self.calc_assistant_surfaces() - def get_assistant_config(self, apply_offset: bool = False): + def get_assistant_config(self, apply_offset: bool = False) -> ConfigDict: """ Assembles the digital twin config from the assistants input. Args: apply_offset(bool): Applies the offset values to the config. Defaults to False + + Returns: + ConfigDict: config of the assistant """ fm_focus = self.input.fm_focus.currentText() if fm_focus in "Manual": @@ -361,9 +367,12 @@ class DigitalTwin(BECWidget, QWidget): # logger.info(f'Config created: {config}') return config - def get_reality_config(self): + def get_reality_config(self) -> ConfigDict: """ Assembles the digital twin config based on the real axis positions. + + Returns: + ConfigDict: config of the reality """ mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"] if abs(mo1_trx) > 5: @@ -505,34 +514,85 @@ class DigitalTwin(BECWidget, QWidget): @SafeSlot() def load_offsets(self, *_, recalculate: bool = True): """ - Loads the offsets from the file + Loads or unloads the offsets from the file Args: - recalculate(bool): Recalculates the assistant values. + recalculate(bool): Recalculates the assistant values after loading. Defaults to True """ - file = Path(OFFSET_FILE) - if not file.exists(): - raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") - with file.open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) + if self.offsets == {}: + # Load offsets + file = Path(OFFSET_FILE) + if not file.exists(): + raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") - if not isinstance(data, dict): - raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}") + with file.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) - self.offsets = data + if not isinstance(data, dict): + raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}") - if recalculate: + self.offsets = data + + if recalculate: + self.calc_assistant(identifier="init") + + self.settings.load_offsets.setText("Unload") + self.settings.offsets_status.setText("Loaded and applied") + self.settings.offsets_status.setColor(get_accent_colors().success.name()) + self.settings.show_offsets.enable_button(True) + else: + # Unload offsets + self.offsets = {} self.calc_assistant(identifier="init") + self.settings.load_offsets.setText("Load") + self.settings.offsets_status.setText("No offsets") + self.settings.offsets_status.setColor(get_accent_colors().default.name()) + self.settings.show_offsets.enable_button(False) + @SafeSlot() - def unload_offsets(self, *_): + def show_offsets(self, *_): """ - Removes the offsets and recalculates the assistant values. + Shows the offsets in a popup window """ - self.offsets = {} - self.calc_assistant(identifier="init") + dialog = QDialog() + dialog.setWindowTitle("Digital Twin - Offsets") + dialog.setFixedWidth(500) + layout = QVBoxLayout(dialog) + layout.setSpacing(12) + layout.setContentsMargins(20, 20, 20, 20) + + intro_label = QLabel("The offsets are saved in the digital twin BEC widget folder:") + intro_label.setWordWrap(True) + layout.addWidget(intro_label) + + file = QLabel(OFFSET_FILE) + file.setWordWrap(True) + font = QFont() + font.setItalic(True) + file.setFont(font) + layout.addWidget(file) + + text_edit = QPlainTextEdit() + text_edit.setReadOnly(True) + text_edit.setFont(QFont("Consolas", 9)) + + class InlineListDumper(yaml.Dumper): + """YAML dumper that renders all sequences on a single line.""" + + def represent_sequence(self, tag, sequence, *_): + return super().represent_sequence(tag, sequence, flow_style=True) + + text_edit.setPlainText(yaml.dump(self.offsets, Dumper=InlineListDumper, sort_keys=False)) + layout.addWidget(text_edit) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + buttons.rejected.connect(dialog.reject) + layout.addWidget(buttons) + + dialog.exec() def update_fm_mode(self): """ 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 f11be4f..8266b58 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -62,8 +62,13 @@ class StatusIcon(QWidget): self.set_status(Status.NOT_IN_POSITION) - def get_rotation(self): - """Return the current rotation angle in degrees.""" + def get_rotation(self) -> float: + """ + Return the current rotation angle in degrees. + + Returns: + float: Rotation angle in deg + """ return self._rotation def set_rotation(self, angle: float): diff --git a/debye_bec/bec_widgets/widgets/digital_twin/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/plots.py index 415fe6b..666abc2 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/plots.py @@ -2,7 +2,7 @@ Two plot classes to plot side-view and surface-view """ -from typing import Literal, Optional, TypedDict, cast +from typing import Literal, Optional, cast import numpy as np import pyqtgraph as pg @@ -13,7 +13,7 @@ from qtpy.QtCore import Qt from qtpy.QtGui import QBrush, QColor # pylint: disable=E0611 -from qtpy.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( mirror_surface_geometries, @@ -153,8 +153,8 @@ class SurfacePlots(QWidget): def plot_surface(widget, surfaces): for name, surface in surfaces.items(): - rect = pg.QtWidgets.QGraphicsRectItem(*surface) # pylint: disable=E1101 - rect.setBrush(QBrush(QColor(*self.color_impenetrable))) # pylint: disable=E1101 + rect = QGraphicsRectItem(*surface) + rect.setBrush(QBrush(QColor(*self.color_impenetrable))) 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)) @@ -292,7 +292,7 @@ class SideviewPlot(QWidget): for wall in self.walls: wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3)) - wall.setBrush(QBrush(QColor(*self.color_impenetrable))) # pylint: disable=E1101 + wall.setBrush(QBrush(QColor(*self.color_impenetrable))) for pipe in self.pipes: pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3)) @@ -311,8 +311,8 @@ class SideviewPlot(QWidget): """Plot walls""" walls = wall_geometries() for wall in walls: - rect = pg.QtWidgets.QGraphicsRectItem(*wall) # pylint: disable=E1101 - rect.setBrush(QBrush(QColor(*self.color_impenetrable))) # pylint: disable=E1101 + rect = QGraphicsRectItem(wall[0], wall[1], wall[2], wall[3]) + rect.setBrush(QBrush(QColor(*self.color_impenetrable))) rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) self.plot_widget.addItem(rect) self.walls.append(rect) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py index 694e1b4..d715c65 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py @@ -5,7 +5,7 @@ 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 +from debye_bec.bec_widgets.widgets.qt_widgets import Button, Group, TextIndicator class SettingsPanel(QWidget): @@ -17,11 +17,14 @@ class SettingsPanel(QWidget): 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) + self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True) + self.offsets_status = TextIndicator(label="Offsets") + self.show_offsets = Button(label="Show Offsets", label_button="Show", enabled=True) # Assemble complete offset group - self.offset_group = Group("Axes Offsets", [self.reload_offsets, self.unload_offsets]) + self.offset_group = Group( + "Axes Offsets", [self.load_offsets, self.offsets_status, self.show_offsets] + ) self._layout.addWidget(self.offset_group) self._layout.addStretch() diff --git a/debye_bec/bec_widgets/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/qt_widgets.py index 201e72f..5a0f59b 100644 --- a/debye_bec/bec_widgets/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/qt_widgets.py @@ -1,24 +1,37 @@ +""" +Universal Qt widgets +""" from functools import partial -# pylint: disable=E0611 -from qtpy.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QGroupBox, QComboBox, QApplication, QDoubleSpinBox -) -from qtpy.QtGui import QFont -from qtpy.QtCore import Qt from bec_widgets.utils.colors import get_accent_colors +from qtpy.QtCore import Qt + +# pylint: disable=E0611 +from qtpy.QtGui import QFont +from qtpy.QtWidgets import ( + QApplication, + QComboBox, + QDoubleSpinBox, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, +) + class Group(QGroupBox): def __init__(self, label, widgets): super().__init__(label) - self.layout = QVBoxLayout(self) # type: ignore + self.layout = QVBoxLayout(self) # type: ignore for widget in widgets: - self.layout.addWidget(widget) # type: ignore + self.layout.addWidget(widget) # type: ignore + class NumberIndicator(QWidget): - def __init__(self, label='', unit=None, highlight=False, decimals=3): + def __init__(self, label="", unit=None, highlight=False, decimals=3): super().__init__() layout = QHBoxLayout(self) layout.setContentsMargins(10, 0, 0, 0) @@ -28,8 +41,8 @@ class NumberIndicator(QWidget): self.label.setContentsMargins(0, 0, 10, 0) self.label.setWordWrap(True) layout.addWidget(self.label) - self.val = QLabel('-') - self.val.setAlignment(Qt.AlignTop) # type: ignore + self.val = QLabel("-") + self.val.setAlignment(Qt.AlignTop) # type: ignore # self.val.setFixedWidth(140) layout.addWidget(self.val) self.unit = unit @@ -51,13 +64,25 @@ class NumberIndicator(QWidget): def setValue(self, number): self.number = number - text = f'{number:.{int(self.decimals)}f}' + text = f"{number:.{int(self.decimals)}f}" if self.unit is not None: - text = text + ' ' + self.unit + text = text + " " + self.unit self.val.setText(text) + class InputNumberField(QWidget): - def __init__(self, identifier='', label='', unit=None, prefix=None, init=0.0, decimals=1, single_step=0.1, ll=-1e6, hl=1e6): + def __init__( + self, + identifier="", + label="", + unit=None, + prefix=None, + init=0.0, + decimals=1, + single_step=0.1, + ll=-1e6, + hl=1e6, + ): super().__init__() layout = QHBoxLayout(self) layout.setContentsMargins(10, 0, 0, 0) @@ -74,9 +99,9 @@ class InputNumberField(QWidget): self.val.setSingleStep(single_step) self.val.setValue(init) if unit is not None: - self.val.setSuffix(' ' + unit) + self.val.setSuffix(" " + unit) if prefix is not None: - self.val.setPrefix(prefix + ' ') + self.val.setPrefix(prefix + " ") # self.val.setFixedWidth(140) layout.addWidget(self.val) @@ -85,18 +110,21 @@ class InputNumberField(QWidget): def has_focus(self) -> bool: return self.val.hasFocus() - + def value(self) -> float: return self.val.value() def value_changed_connect(self, func): """Connect a function to the Enter/Return key press.""" self.val.valueChanged.connect( - partial(func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value()) + partial( + func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value() + ) ) + class ComboBox(QWidget): - def __init__(self, identifier='', label='', enums=[]): + def __init__(self, identifier="", label="", enums=[]): super().__init__() layout = QHBoxLayout(self) layout.setContentsMargins(10, 0, 0, 0) @@ -124,14 +152,20 @@ class ComboBox(QWidget): def activated_connect(self, func): """Connect a function to the Enter/Return key press.""" self.value.activated.connect( - partial(func, identifier=self.identifier, value_obj=self.value, value=lambda: self.value.currentText()) + partial( + func, + identifier=self.identifier, + value_obj=self.value, + value=lambda: self.value.currentText(), + ) ) def setDisabled(self, disable): self.value.setDisabled(disable) + class Button(QWidget): - def __init__(self, label=None, label_button:str='', enabled=False): + def __init__(self, label=None, label_button: str = "", enabled=False): super().__init__() layout = QHBoxLayout(self) layout.setContentsMargins(10, 0, 0, 0) @@ -165,31 +199,31 @@ class Button(QWidget): def setText(self, text): self.button.setText(text) -# class TextIndicator(QWidget): -# def __init__(self, label, unit=None, highlight=False): -# super().__init__() -# layout = QHBoxLayout(self) -# layout.setContentsMargins(10, 0, 0, 0) -# layout.setSpacing(0) -# self.label = QLabel(label) -# self.label.setFixedWidth(150) -# layout.addWidget(self.label) -# self.value = QLabel('-') -# self.value.setFixedWidth(160) -# layout.addWidget(self.value) -# self.unit = unit -# self.highlight = highlight -# if highlight: -# font = QFont() -# font.setBold(True) -# font.setPointSize(14) -# self.label.setFont(font) -# self.value.setFont(font) -# def set_text(self, text): -# if self.unit is not None: -# text = text + ' ' + self.unit -# self.value.setText(text) +class TextIndicator(QWidget): + def __init__(self, label): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 0, 0, 0) + layout.setSpacing(0) + self.label = QLabel(label) + self.label.setFixedWidth(140) + self.label.setContentsMargins(0, 0, 10, 0) + self.label.setWordWrap(True) + layout.addWidget(self.label) + self.text = QLabel("-") + self.text.setAlignment(Qt.AlignTop) # type: ignore + layout.addWidget(self.text) + + def setLabel(self, label) -> None: + self.label.setText(label) + + def setText(self, text): + self.text.setText(text) + + def setColor(self, color: str): + self.text.setStyleSheet(f"QLabel {{color:{color}}}") + # class Button(QWidget): # def __init__(self, label, label_button): diff --git a/debye_bec/bec_widgets/widgets/x01da_parameters.py b/debye_bec/bec_widgets/widgets/x01da_parameters.py index 8a97e5a..630d7fc 100644 --- a/debye_bec/bec_widgets/widgets/x01da_parameters.py +++ b/debye_bec/bec_widgets/widgets/x01da_parameters.py @@ -4,138 +4,123 @@ This file describes the parameter of each component of the Debye beamline to be used for raytracing and geometrical calculations. """ -import os -import numpy as np from collections import namedtuple +import numpy as np import xrt.backends.raycing.materials as rm -# if os.environ.get("USE_XRT", "True").lower() in ("1", "true", "yes"): -# import xrt.backends.raycing.materials as rm # type: ignore -# else: -# class _DummyClass: -# def __init__(self, *args, **kwargs): -# pass -# class _DummyMaterials: -# Material = _DummyClass -# CrystalSi = _DummyClass -# rm = _DummyMaterials() - # XRT definitions -filterBeryl = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType] -filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType] -filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType] +filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType] +filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType] -stripeSi = rm.Material('Si', rho=2.33) # pyright: ignore[reportArgumentType] -stripePt = rm.Material('Pt', rho=21.45) # pyright: ignore[reportArgumentType] -stripeRh = rm.Material('Rh', rho=12.41) # pyright: ignore[reportArgumentType] -stripeCr = rm.Material('Cr', rho=7.14) # pyright: ignore[reportArgumentType] -stripePyrex = rm.Material('Si', rho=2.20) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType] +stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType] +stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType] +stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType] +stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType] +stripePyrex = rm.Material( + "Si", rho=2.20 +) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType] -si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface -si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface -si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface -si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface -si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface -si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface -si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface -si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface +si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface +si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface +si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface +si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface +si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface +si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface +si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface +si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface -filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType] -filterBe = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType] -filterSi3N4 = rm.Material(['Si', 'N'], quantities=[3, 4], rho=3.44, kind='plate') # pyright: ignore[reportArgumentType] -filterAl = rm.Material('Al', rho=2.69, kind='plate') # pyright: ignore[reportArgumentType] -filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType] +filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType] +filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType] +filterSi3N4 = rm.Material( + ["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate" +) # pyright: ignore[reportArgumentType] +filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType] # General parameters sourceHeight = 0 -#Synchrotron -synchrotron = namedtuple('synchrotron', ['eE', 'eI', 'eEspread', - 'eEpsilonX', 'eEpsilonZ', 'betaX', 'betaZ']) +# Synchrotron +synchrotron = namedtuple( + "synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"] +) sls1 = synchrotron( - eE = 2.4, - eI = 0.4, - eEspread=0.878e-3, - eEpsilonX=5.63, - eEpsilonZ=0.007, - betaX=0.45, - betaZ=14.4, - ) - -sls2 = synchrotron( - eE=2.7, - eI=0.4, - eEspread=1.147e-3, - eEpsilonX=0.156, - eEpsilonZ=0.01, - betaX=0.18, - betaZ=4.6, - ) - -# Source -bendingMagnet = namedtuple('bendingMagnet', ['name', 'center', 'sync', 'B0']) + eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4 +) + +sls2 = synchrotron( + eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6 +) + +# Source +bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"]) + +sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4) + +sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1) + +sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5) + +sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0) -sls1_14t = bendingMagnet( - name='FE-BM-SLS1-1.4T', - center=(0, 0, 0), - sync=sls1, - B0=1.4,) - -sls2_21t = bendingMagnet( - name='FE-BM-SLS2-2.1T', - center=(0, 0, 0), - sync=sls2, - B0=2.1,) - -sls2_35t = bendingMagnet( - name='FE-BM-SLS2-3.5T', - center=(0, 0, 0), - sync=sls2, - B0=3.5,) - -sls2_50t = bendingMagnet( - name='FE-BM-SLS2-5.0T', - center=(0, 0, 0), - sync=sls2, - B0=5.0,) - # FE slits -fe_slits = namedtuple('slits', ['name', 'center', 'center1', 'center2', 'maxDivH', 'maxDivV']) +fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"]) feSlits = fe_slits( - name='FE-SLITS', + name="FE-SLITS", center=(0, 6117, sourceHeight), center1=(0, 5045, sourceHeight), center2=(0, 5289.5, sourceHeight), maxDivH=1.8e-3, - maxDivV=0.8e-3,) - + maxDivV=0.8e-3, +) + # FE Window -filt = namedtuple('filt', ['name', 'center', 'pitch', 'limPhysX', 'limPhysY', 'surface', 'material', 'thickness']) +filt = namedtuple( + "filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"] +) feWindow = filt( - name='FE-WINDOW', - center=(0., 7020, sourceHeight), - pitch=np.pi/2, + name="FE-WINDOW", + center=(0.0, 7020, sourceHeight), + pitch=np.pi / 2, limPhysX=(-6, 6), - limPhysY=(-3., 3.), - surface='None', + limPhysY=(-3.0, 3.0), + surface="None", material=filterDiamond, - thickness=0.1,) -feWindow = feWindow._replace(surface=f'CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m') + thickness=0.1, +) +feWindow = feWindow._replace(surface=f"CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m") + +# Collimating mirror +collimatingMirror = namedtuple( + "collimatingMirror", + [ + "name", + "center", + "surface", + "material", + "limPhysX", + "limPhysY", + "limOptX", + "limOptY", + "R", + "pitch", + "jack1", + "jack2", + "jack3", + "tx1", + "tx2", + ], +) -# Collimating mirror -collimatingMirror = namedtuple('collimatingMirror', ['name', - 'center', 'surface', 'material', 'limPhysX', 'limPhysY', - 'limOptX', 'limOptY', 'R', 'pitch', 'jack1', 'jack2', 'jack3', - 'tx1', 'tx2']) - cm = collimatingMirror( - name='FE-CM', + name="FE-CM", center=[0, 6890, sourceHeight], - surface=('Si','Pt','Rh'), + surface=("Si", "Pt", "Rh"), material=(stripeSi, stripePt, stripeRh), limPhysX=(-34, 34), limPhysY=(-600, 600), @@ -143,169 +128,194 @@ cm = collimatingMirror( limOptY=((-500, -500, -500), (500, 500, 500)), R=[3e6, 15e6], pitch=[-5.0e-3, -0.0e-3], - jack1=[0., 7210., 0.], #Tripod X, Y, Z (global) - jack2=[-210., 8310., 0.], - jack3=[210., 8310., 0.], - tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local) - tx2=[0.0, 575],) # X-Stage 2 - -apertures = namedtuple('apertures', ['name', 'center', 'opening']) + jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global) + jack2=[-210.0, 8310.0, 0.0], + jack3=[210.0, 8310.0, 0.0], + tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local) + tx2=[0.0, 575], +) # X-Stage 2 + +apertures = namedtuple("apertures", ["name", "center", "opening"]) fePS = apertures( - name='FE-PS', - center=[0, 8815, sourceHeight], - opening=[-20., 20., -20.+12.5, 20.+12.5]) # left, right, bottom, top - + name="FE-PS", center=[0, 8815, sourceHeight], opening=[-20.0, 20.0, -20.0 + 12.5, 20.0 + 12.5] +) # left, right, bottom, top + opWbBsBlock = apertures( - name='OP-WB-BS-BLOCK', - center=[0., 13860, sourceHeight], - opening=[-18., 18., 25, 85.5]) # left, right, bottom, top - # opening=[-18., 18., 42, 76], # X10DA + name="OP-WB-BS-BLOCK", center=[0.0, 13860, sourceHeight], opening=[-18.0, 18.0, 25, 85.5] +) # left, right, bottom, top +# opening=[-18., 18., 42, 76], # X10DA # Monochromator -monochromator = namedtuple('monochromator', ['name', 'center', - 'xtal', 'material1', 'material2', 'xtalWidth', 'xtalOffsetX', - 'xtalLength1', 'xtalLength2', 'xtalGap', 'rotOffset', - 'heightOffset', 'braggLim', 'jack1', 'jack2', 'jack3', 'tx']) +monochromator = namedtuple( + "monochromator", + [ + "name", + "center", + "xtal", + "material1", + "material2", + "xtalWidth", + "xtalOffsetX", + "xtalLength1", + "xtalLength2", + "xtalGap", + "rotOffset", + "heightOffset", + "braggLim", + "jack1", + "jack2", + "jack3", + "tx", + ], +) mo1 = monochromator( - name='OP-MO1', - center=[0., 11750, sourceHeight], - xtal=('Si311','Si111'), + name="OP-MO1", + center=[0.0, 11750, sourceHeight], + xtal=("Si311", "Si111"), material1=(si311_1, si111_1), material2=(si311_2, si111_2), - xtalWidth = (24, 24), + xtalWidth=(24, 24), xtalOffsetX=(-21.2, 21.2), - xtalLength1 = (55, 55), - xtalLength2 = (105, 105), - xtalGap = (8, 8), - rotOffset = 6, - heightOffset = 8.5, - braggLim = [3.6, 33], - jack1=[0., 11350., 0.], #Tripod maybe not available! - jack2=[-400., 12350., 0.], - jack3=[400., 12350., 0.], - tx=0.0,) # X-Stage [x] - + xtalLength1=(55, 55), + xtalLength2=(105, 105), + xtalGap=(8, 8), + rotOffset=6, + heightOffset=8.5, + braggLim=[3.6, 33], + jack1=[0.0, 11350.0, 0.0], # Tripod maybe not available! + jack2=[-400.0, 12350.0, 0.0], + jack3=[400.0, 12350.0, 0.0], + tx=0.0, +) # X-Stage [x] + mo2 = monochromator( - name='OP-CCM2', - center=[0., 13250, sourceHeight], - xtal=('Si311','Si111'), + name="OP-CCM2", + center=[0.0, 13250, sourceHeight], + xtal=("Si311", "Si111"), material1=(si311_1, si111_1), material2=(si311_2, si111_2), - xtalWidth = (24, 24), + xtalWidth=(24, 24), xtalOffsetX=(-21, 21), - xtalLength1 = (55, 55), - xtalLength2 = (105, 105), - xtalGap = (8, 8), - rotOffset = 6, - heightOffset = 8.5, - braggLim = [3.6, 33], - jack1=[0., 13350., 0.], #Tripod maybe not available! - jack2=[-400., 14350., 0.], - jack3=[400., 14350., 0.], - tx=0.0,) # X-Stage [x] + xtalLength1=(55, 55), + xtalLength2=(105, 105), + xtalGap=(8, 8), + rotOffset=6, + heightOffset=8.5, + braggLim=[3.6, 33], + jack1=[0.0, 13350.0, 0.0], # Tripod maybe not available! + jack2=[-400.0, 14350.0, 0.0], + jack3=[400.0, 14350.0, 0.0], + tx=0.0, +) # X-Stage [x] # OP Slits -op_slits = namedtuple('op_slits', ['name', 'center']) +op_slits = namedtuple("op_slits", ["name", "center"]) -opSlits1 = op_slits( - name='OP-SLITS 1', - center=(0, 14349.6, sourceHeight), -) +opSlits1 = op_slits(name="OP-SLITS 1", center=(0, 14349.6, sourceHeight)) -opSlits2 = op_slits( - name='OP-SLITS 2', - center=(0, 18134.8, sourceHeight), -) +opSlits2 = op_slits(name="OP-SLITS 2", center=(0, 18134.8, sourceHeight)) # OP Beam Monitors -op_bm = namedtuple('op_bm', ['name', 'center']) +op_bm = namedtuple("op_bm", ["name", "center"]) -opBM1 = op_bm( - name='OP Beam Monitor 1', - center=(0, 14599.6, sourceHeight), +opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14599.6, sourceHeight)) + +opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 18384.8, sourceHeight)) + +# Focusing mirror +focusingMirror = namedtuple( + "focusingMirror", + [ + "name", + "center", + "surfaceToroid", + "materialToroid", + "surfaceFlat", + "materialFlat", + "limPhysXToroid", + "limPhysYToroid", + "limPhysXFlat", + "limPhysYFlat", + "limOptXToroid", + "limOptYToroid", + "limOptXFlat", + "limOptYFlat", + "R", + "pitch", + "r", + "xToroid", + "xFlat", + "hToroid", + "jack1", + "jack2", + "jack3", + "tx1", + "tx2", + ], ) -opBM2 = op_bm( - name='OP Beam Monitor 2', - center=(0, 18384.8, sourceHeight), -) - -# Focusing mirror -focusingMirror = namedtuple('focusingMirror', ['name', 'center', - 'surfaceToroid', 'materialToroid', 'surfaceFlat', 'materialFlat', - 'limPhysXToroid', 'limPhysYToroid', 'limPhysXFlat', 'limPhysYFlat', - 'limOptXToroid', 'limOptYToroid', 'limOptXFlat', 'limOptYFlat', - 'R', 'pitch', 'r', 'xToroid', 'xFlat', 'hToroid', 'jack1', 'jack2', 'jack3', - 'tx1', 'tx2']) - fm = focusingMirror( - name='OP-FM', - center=[0., 15670, sourceHeight], # nominal height 58 mm above ring, SLS1! - surfaceToroid=('Rh', 'Pt'), + name="OP-FM", + center=[0.0, 15670, sourceHeight], # nominal height 58 mm above ring, SLS1! + surfaceToroid=("Rh", "Pt"), materialToroid=(stripeRh, stripePt), - surfaceFlat=('Rh', 'Pt'), + surfaceFlat=("Rh", "Pt"), materialFlat=(stripeRh, stripePt), - limPhysXToroid=(-79., 79.), - limPhysYToroid=(-575., 575.), - limPhysXFlat=(-79., 79.), - limPhysYFlat=(-575., 575.), + limPhysXToroid=(-79.0, 79.0), + limPhysYToroid=(-575.0, 575.0), + limPhysXFlat=(-79.0, 79.0), + limPhysYFlat=(-575.0, 575.0), limOptXToroid=((-38, 66), (-66, 31)), - limOptYToroid=((-500., -500.), (500., 500.)), + limOptYToroid=((-500.0, -500.0), (500.0, 500.0)), limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)), - limOptYFlat=((-500., -500.), (500., 500.)), + limOptYFlat=((-500.0, -500.0), (500.0, 500.0)), R=[3e6, 15e6], pitch=[-5.0e-3, 0e-3], r=[35.510, 24.986], - xToroid=[-52, 48.5], # offset in local x - xFlat = [-20.95, 8.55], - hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. - jack1=[-130., 15535-538., 0.], - jack2=[130., 15535+538., 0.], - jack3=[0., 15535+538., 0.], - tx1=[0., -575.], # X-Stage 1 [x, y] - tx2=[0., 575.],) # X-Stage 2 [x, y] + xToroid=[-52, 48.5], # offset in local x + xFlat=[-20.95, 8.55], + hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. + jack1=[-130.0, 15535 - 538.0, 0.0], + jack2=[130.0, 15535 + 538.0, 0.0], + jack3=[0.0, 15535 + 538.0, 0.0], + tx1=[0.0, -575.0], # X-Stage 1 [x, y] + tx2=[0.0, 575.0], +) # X-Stage 2 [x, y] # EH Window ehWindow = filt( - name='EH-WINDOW', - center=(0., 19998.3, sourceHeight), - pitch=np.pi/2, - limPhysX=(-20., 20.), + name="EH-WINDOW", + center=(0.0, 19998.3, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-20.0, 20.0), limPhysY=(-4, 4), - surface='None', + surface="None", material=filterSi3N4, - thickness=0.002,) -ehWindow = ehWindow._replace(surface=f'Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m') - + thickness=0.002, +) +ehWindow = ehWindow._replace(surface=f"Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m") + # Sample -sample = namedtuple('sample', ['name', 'center']) +sample = namedtuple("sample", ["name", "center"]) -smpl = sample( - name='EH-SMPL', - center=[0, 23365, sourceHeight],) +smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight]) -smpl2 = sample( - name='EH-SMPL2', - center=[0, 27500, sourceHeight],) +smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight]) # Vacuum pipes # DN40CF ID = 35 mm oder 37 mm # DN50CF ID = 47.5 mm # DN63CF ID = 60.2 mm oder 66 mm # DN100CF ID = 97.4 mm oder 104 mm -pipe = namedtuple('pipes', ['center', 'diameter', 'start', 'end']) +pipe = namedtuple("pipes", ["center", "diameter", "start", "end"]) vacuum_pipes = pipe( - center= [27.5, (37.5+27.5)/2, 37.5, 62.5, 72.5], - diameter=[97.4, 97.4, 97.4, 97.4, 97.4], - start= [10952.88, 11750+250, mo2.center[1]+250, 14000, fm.center[1]], - end= [11750-250, mo2.center[1]-250, 14000, fm.center[1], ehWindow.center[1]], + center=[27.5, (37.5 + 27.5) / 2, 37.5, 62.5, 72.5], + diameter=[97.4, 97.4, 97.4, 97.4, 97.4], + start=[10952.88, 11750 + 250, mo2.center[1] + 250, 14000, fm.center[1]], + end=[11750 - 250, mo2.center[1] - 250, 14000, fm.center[1], ehWindow.center[1]], ) -Walls = namedtuple('walls', ['start', 'end', 'height']) -walls = Walls( - start= [13999.30], - end= [13999+75.5+30], - height= [[-20, 25]], -) +Walls = namedtuple("walls", ["start", "end", "height"]) +walls = Walls(start=[13999.30], end=[13999 + 75.5 + 30], height=[[-20, 25]]) From 8b8138ec05e6d2d3de54f21d84cffa16d5e262b2 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 19 May 2026 10:57:59 +0200 Subject: [PATCH 5/5] refactoring --- .../{ => calculations}/calc_positions.py | 2 +- .../{ => calculations}/calc_sideview.py | 2 +- .../{ => calculations}/calc_surfaces.py | 2 +- .../{ => calculations}/calc_varia.py | 2 +- .../widgets/digital_twin/digital_twin.py | 18 +- .../digital_twin/{ => panels}/input_panel.py | 2 +- .../digital_twin/{ => panels}/mover_panel.py | 7 +- .../digital_twin/{ => panels}/plots.py | 4 +- .../{ => panels}/settings_panel.py | 6 +- .../digital_twin/{ => widgets}/move_widget.py | 0 .../{ => digital_twin/widgets}/qt_widgets.py | 0 .../{ => digital_twin}/x01da_offsets.yaml | 0 .../{ => digital_twin}/x01da_parameters.py | 0 .../file_writer/debye_nexus_structure.py | 210 +++++++++++------- 14 files changed, 152 insertions(+), 103 deletions(-) rename debye_bec/bec_widgets/widgets/digital_twin/{ => calculations}/calc_positions.py (99%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => calculations}/calc_sideview.py (97%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => calculations}/calc_surfaces.py (98%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => calculations}/calc_varia.py (99%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => panels}/input_panel.py (98%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => panels}/mover_panel.py (97%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => panels}/plots.py (98%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => panels}/settings_panel.py (88%) rename debye_bec/bec_widgets/widgets/digital_twin/{ => widgets}/move_widget.py (100%) rename debye_bec/bec_widgets/widgets/{ => digital_twin/widgets}/qt_widgets.py (100%) rename debye_bec/bec_widgets/widgets/{ => digital_twin}/x01da_offsets.yaml (100%) rename debye_bec/bec_widgets/widgets/{ => digital_twin}/x01da_parameters.py (100%) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py similarity index 99% rename from debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py rename to debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py index f463286..79cb809 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py @@ -5,7 +5,7 @@ Calculates the positions of axes based on a beamline config import numpy as np from bec_lib import bec_logger -import debye_bec.bec_widgets.widgets.x01da_parameters as bl +import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict logger = bec_logger.logger diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py similarity index 97% rename from debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py rename to debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py index 092d5d4..afa0b29 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py @@ -4,7 +4,7 @@ Calculates the sideview coordinates based on a beamline config. import numpy as np -import debye_bec.bec_widgets.widgets.x01da_parameters as bl +import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, DataDict diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py similarity index 98% rename from debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py rename to debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py index 168f977..dcac3dd 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py @@ -7,7 +7,7 @@ import re import numpy as np from bec_lib import bec_logger -import debye_bec.bec_widgets.widgets.x01da_parameters as bl +import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, SurfaceDict logger = bec_logger.logger diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py similarity index 99% rename from debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py rename to debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py index 9ec7cc8..0cc43f0 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py @@ -10,7 +10,7 @@ from bec_lib import bec_logger from scipy.interpolate import UnivariateSpline from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm -import debye_bec.bec_widgets.widgets.x01da_parameters as bl +import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl logger = bec_logger.logger 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 a9c87af..7b7ee59 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -31,10 +31,10 @@ from qtpy.QtWidgets import ( QWidget, ) -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 ( +from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_positions import calc_positions +from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_sideview import calc_sideview +from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_surfaces import calc_surfaces +from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import ( cm_critical_angle, cm_reflectivity, cm_stripe_to_trx, @@ -47,15 +47,15 @@ from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( mo1_energy_resolution, sldi_gap_to_acc, ) -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 +from debye_bec.bec_widgets.widgets.digital_twin.panels.input_panel import InputPanel +from debye_bec.bec_widgets.widgets.digital_twin.panels.mover_panel import MoverPanel +from debye_bec.bec_widgets.widgets.digital_twin.panels.plots import SideviewPlot, SurfacePlots +from debye_bec.bec_widgets.widgets.digital_twin.panels.settings_panel import SettingsPanel from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict logger = bec_logger.logger -OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/x01da_offsets.yaml" +OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml" class DigitalTwin(BECWidget, QWidget): diff --git a/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py similarity index 98% rename from debye_bec/bec_widgets/widgets/digital_twin/input_panel.py rename to debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py index 3364af4..093dae9 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/input_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py @@ -5,7 +5,7 @@ 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 ( +from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( Button, ComboBox, Group, diff --git a/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py similarity index 97% rename from debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py rename to debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py index 2260c8a..3f26f06 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py @@ -7,8 +7,11 @@ from typing import Literal # 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 +from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import ( + AbsorberWidget, + MoveWidget, +) +from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group class MoverPanel(QWidget): diff --git a/debye_bec/bec_widgets/widgets/digital_twin/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py similarity index 98% rename from debye_bec/bec_widgets/widgets/digital_twin/plots.py rename to debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py index 666abc2..5e17c85 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py @@ -15,14 +15,14 @@ from qtpy.QtGui import QBrush, QColor # pylint: disable=E0611 from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget -from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( +from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import ( mirror_surface_geometries, mo_surface_geometries, pipe_geometries, wall_geometries, ) from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict, SurfaceDict -from debye_bec.bec_widgets.widgets.qt_widgets import Group +from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group logger = bec_logger.logger diff --git a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py similarity index 88% rename from debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py rename to debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py index d715c65..8947ea3 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/settings_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py @@ -5,7 +5,11 @@ 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, TextIndicator +from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( + Button, + Group, + TextIndicator, +) class SettingsPanel(QWidget): diff --git a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py similarity index 100% rename from debye_bec/bec_widgets/widgets/digital_twin/move_widget.py rename to debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py diff --git a/debye_bec/bec_widgets/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py similarity index 100% rename from debye_bec/bec_widgets/widgets/qt_widgets.py rename to debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py diff --git a/debye_bec/bec_widgets/widgets/x01da_offsets.yaml b/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml similarity index 100% rename from debye_bec/bec_widgets/widgets/x01da_offsets.yaml rename to debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml diff --git a/debye_bec/bec_widgets/widgets/x01da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py similarity index 100% rename from debye_bec/bec_widgets/widgets/x01da_parameters.py rename to debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py diff --git a/debye_bec/file_writer/debye_nexus_structure.py b/debye_bec/file_writer/debye_nexus_structure.py index 5a44ad9..8364845 100644 --- a/debye_bec/file_writer/debye_nexus_structure.py +++ b/debye_bec/file_writer/debye_nexus_structure.py @@ -1,6 +1,7 @@ from bec_server.file_writer.default_writer import DefaultFormat -import debye_bec.bec_widgets.widgets.x01da_parameters as bl +import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl + class DebyeNexusStructure(DefaultFormat): """Nexus Structure for Debye""" @@ -31,8 +32,7 @@ class DebyeNexusStructure(DefaultFormat): if "curr" in self.device_manager.devices: ring_current = source.create_soft_link( - name="ring_current", - target="/entry/collection/devices/curr/curr/value", + name="ring_current", target="/entry/collection/devices/curr/curr/value" ) ring_current.attrs["NX_class"] = "NX_FLOAT" ring_current.attrs["units"] = "mA" @@ -57,12 +57,12 @@ class DebyeNexusStructure(DefaultFormat): name="reflection", target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value", ) - reflection.attrs["NX_class"] = "NX_CHAR" + reflection.attrs["NX_class"] = "NX_CHAR" # Create a softlink d_spacing = crystal.create_soft_link( - name="d_spacing", - target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value", + name="d_spacing", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value", ) d_spacing.attrs["NX_class"] = "NX_FLOAT" d_spacing.attrs["units"] = "angstrom" @@ -71,40 +71,40 @@ class DebyeNexusStructure(DefaultFormat): name="bragg_offset", target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_bragg_off/value", ) - bragg_offset.attrs["NX_class"] = "NX_FLOAT" - bragg_offset.attrs["units"] = "degree" + bragg_offset.attrs["NX_class"] = "NX_FLOAT" + bragg_offset.attrs["units"] = "degree" phi_offset = crystal.create_soft_link( name="phi_offset", target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_phi_off/value", ) - phi_offset.attrs["NX_class"] = "NX_FLOAT" + phi_offset.attrs["NX_class"] = "NX_FLOAT" phi_offset.attrs["units"] = "degree" ## Logic if device exist - if "mo1_roty" in self.device_manager.devices: + if "mo1_roty" in self.device_manager.devices: # Create a softlink azimuthal_angle = crystal.create_soft_link( - name="azimuthal_angle", - target="/entry/collection/devices/mo1_roty/mo1_roty/value", + name="azimuthal_angle", + target="/entry/collection/devices/mo1_roty/mo1_roty/value", ) - azimuthal_angle.attrs["NX_class"] = "NX_FLOAT" - azimuthal_angle.attrs["units"] = "degree" - + azimuthal_angle.attrs["NX_class"] = "NX_FLOAT" + azimuthal_angle.attrs["units"] = "degree" + azm_offset = crystal.create_soft_link( name="azm_offset", target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_azm_off/value", ) - azm_offset.attrs["NX_class"] = "NX_FLOAT" + azm_offset.attrs["NX_class"] = "NX_FLOAT" azm_offset.attrs["units"] = "degree" miscut = crystal.create_soft_link( name="miscut", target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_miscut/value", ) - miscut.attrs["NX_class"] = "NX_FLOAT" - miscut.attrs["units"] = "degree" + miscut.attrs["NX_class"] = "NX_FLOAT" + miscut.attrs["units"] = "degree" ################### ### cm mirror specific information @@ -118,7 +118,7 @@ class DebyeNexusStructure(DefaultFormat): ) cm_substrate_material.attrs["NX_class"] = "NX_CHAR" - #previous error due to space in name field + # previous error due to space in name field if "cm_bnd_radius" in self.device_manager.devices: cm_bending_radius = collimating_mirror.create_soft_link( @@ -149,15 +149,15 @@ class DebyeNexusStructure(DefaultFormat): cm_roll_angle.attrs["NX_class"] = "NX_FLOAT" cm_roll_angle.attrs["units"] = "mrad" - if 'cm_trx' in self.device_manager.devices: - cm_trx = - self.device_manager.devices.cm_trx.read(cached=True).get('cm_trx').get('value') - stripe = 'Unknown' + if "cm_trx" in self.device_manager.devices: + cm_trx = ( + -self.device_manager.devices.cm_trx.read(cached=True).get("cm_trx").get("value") + ) + stripe = "Unknown" for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): if low <= cm_trx <= high: stripe = name - cm_stripe = collimating_mirror.create_dataset( - name="stripe", data=stripe - ) + cm_stripe = collimating_mirror.create_dataset(name="stripe", data=stripe) cm_stripe.attrs["NX_class"] = "NX_CHAR" ################### @@ -167,9 +167,7 @@ class DebyeNexusStructure(DefaultFormat): focusing_mirror = instrument.create_group(name="focusing_mirror") focusing_mirror.attrs["NX_class"] = "NXmirror" - fm_substrate_material = focusing_mirror.create_dataset( - name="substrate_material", data="Si" - ) + fm_substrate_material = focusing_mirror.create_dataset(name="substrate_material", data="Si") fm_substrate_material.attrs["NX_class"] = "NX_CHAR" if "fm_bnd_radius" in self.device_manager.devices: @@ -201,18 +199,22 @@ class DebyeNexusStructure(DefaultFormat): fm_roll_angle.attrs["NX_class"] = "NX_FLOAT" fm_roll_angle.attrs["units"] = "mrad" - if 'fm_trx' in self.device_manager.devices: - fm_trx = - self.device_manager.devices.fm_trx.read(cached=True).get('fm_trx').get('value') - stripe = 'Unknown' - for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): - if low <= fm_trx <= high: - stripe = name + ' (flat)' - for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): - if low <= fm_trx <= high: - stripe = name + ' (toroid)' - fm_stripe = focusing_mirror.create_dataset( - name="stripe", data=stripe + if "fm_trx" in self.device_manager.devices: + fm_trx = ( + -self.device_manager.devices.fm_trx.read(cached=True).get("fm_trx").get("value") ) + stripe = "Unknown" + for name, low, high in zip( + bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0] + ): + if low <= fm_trx <= high: + stripe = name + " (flat)" + for name, low, high in zip( + bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0] + ): + if low <= fm_trx <= high: + stripe = name + " (toroid)" + fm_stripe = focusing_mirror.create_dataset(name="stripe", data=stripe) fm_stripe.attrs["NX_class"] = "NX_CHAR" ################### @@ -220,45 +222,65 @@ class DebyeNexusStructure(DefaultFormat): ################### ## Logic if device exist - if "nidaq" in self.device_manager.devices: - - #ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value") - ai_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value") - ci_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value") - #add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value") - add_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value") + if "nidaq" in self.device_manager.devices: + + # ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value") + ai_chans_bits = ( + self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value") + ) + ci_chans_bits = ( + self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value") + ) + # add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value") + add_chans_bits = ( + self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value") + ) measurement_mode = entry.create_group(name="mode") measurement_mode.attrs["NX_class"] = "NX_CHAR" if (int(ci_chans_bits) & 0x7F) != 0: # Create a dataset - rayspec_sdd_active = measurement_mode.create_group(name="Multi_Element_Partial_Fluorescence_Yield") - me_sdd = rayspec_sdd_active.create_dataset(name="Detector", data="Rayspec 7 element Silicon Drift Detector") + rayspec_sdd_active = measurement_mode.create_group( + name="Multi_Element_Partial_Fluorescence_Yield" + ) + me_sdd = rayspec_sdd_active.create_dataset( + name="Detector", data="Rayspec 7 element Silicon Drift Detector" + ) me_sdd.attrs["NX_class"] = "NX_CHAR" - if (int(ci_chans_bits) & (1<<8)) != 0: + if (int(ci_chans_bits) & (1 << 8)) != 0: # Create a dataset - ketek_sdd_active = measurement_mode.create_group(name="Single_Element_Partial_Fluorescence_Yield") - se_sdd = ketek_sdd_active.create_dataset(name="Detector", data="Ketex mini single element Silicon Drift Detector") + ketek_sdd_active = measurement_mode.create_group( + name="Single_Element_Partial_Fluorescence_Yield" + ) + se_sdd = ketek_sdd_active.create_dataset( + name="Detector", data="Ketex mini single element Silicon Drift Detector" + ) se_sdd.attrs["NX_class"] = "NX_CHAR" - if ((int(ai_chans_bits) & (1<<6)) != 0): + if (int(ai_chans_bits) & (1 << 6)) != 0: # Create a dataset pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield") - tfy = pips_active.create_dataset(name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector") + tfy = pips_active.create_dataset( + name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector" + ) tfy.attrs["NX_class"] = "NX_CHAR" - if ((int(ai_chans_bits) & (1<<0)) != 0) & ((int(ai_chans_bits) & (1<<2)) != 0): + if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0): # Create a dataset ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission") - sam_trans = ai0ai2_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers") + sam_trans = ai0ai2_active.create_dataset( + name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" + ) sam_trans.attrs["NX_class"] = "NX_CHAR" - if ((int(ai_chans_bits) & (1<<2)) != 0) & ((int(ai_chans_bits) & (1<<4)) != 0): + if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0): # Create a dataset ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission") - ref_trans = ai2ai4_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers") + ref_trans = ai2ai4_active.create_dataset( + name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" + ) ref_trans.attrs["NX_class"] = "NX_CHAR" main_data = entry.create_group(name="data") @@ -267,45 +289,57 @@ class DebyeNexusStructure(DefaultFormat): ################## ## energy, test whether the signal exists. how to check from config? ################### - + energy = main_data.create_group(name="energy") energy.attrs["NX_class"] = "NXdata" energy.attrs["units"] = "eV" - - main_data.create_soft_link(name="energy", target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value") - + + main_data.create_soft_link( + name="energy", + target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value", + ) + ################## ## i0 ################### - - if (int(ai_chans_bits) & (1<<0)) !=0: + + if (int(ai_chans_bits) & (1 << 0)) != 0: i0 = main_data.create_group(name="i0") i0.attrs["NX_class"] = "NXdata" i0.attrs["units"] = "V" - - main_data.create_soft_link(name="i0", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value") + + main_data.create_soft_link( + name="i0", + target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value", + ) ################## ## i1 ################### - - if (int(ai_chans_bits) & (1<<2)) !=0: + + if (int(ai_chans_bits) & (1 << 2)) != 0: i1 = main_data.create_group(name="i1") i1.attrs["NX_class"] = "NXdata" i1.attrs["units"] = "V" - main_data.create_soft_link(name="i1", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value") + main_data.create_soft_link( + name="i1", + target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value", + ) ################## ## i2 ################### - if (int(ai_chans_bits) & (1<<4)) !=0: + if (int(ai_chans_bits) & (1 << 4)) != 0: i2 = main_data.create_group(name="i2") i2.attrs["NX_class"] = "NXdata" i2.attrs["units"] = "V" - main_data.create_soft_link(name="i2", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value") + main_data.create_soft_link( + name="i2", + target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value", + ) ################## ## ci sum @@ -316,38 +350,46 @@ class DebyeNexusStructure(DefaultFormat): ci_sum.attrs["NX_class"] = "NXdata" ci_sum.attrs["units"] = "counts" - main_data.create_soft_link(name="Fluorescence_Sum", target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value") + main_data.create_soft_link( + name="Fluorescence_Sum", + target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value", + ) ################## ## mu sample, test whether the signal exists. how to check from config? ################### - if (int(add_chans_bits) & (1<<0)) !=0: + if (int(add_chans_bits) & (1 << 0)) != 0: mu_sample = main_data.create_group(name="mu_sample") mu_sample.attrs["NX_class"] = "NXdata" - - main_data.create_soft_link(name="mu_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value") + + main_data.create_soft_link( + name="mu_sample", + target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value", + ) ################## ## fluo sample, test whether the signal exists. how to check from config? ################### - if (int(add_chans_bits) & (1<<1)) !=0: + if (int(add_chans_bits) & (1 << 1)) != 0: mu_sample = main_data.create_group(name="fluo_sample") mu_sample.attrs["NX_class"] = "NXdata" - - main_data.create_soft_link(name="fluo_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value") + + main_data.create_soft_link( + name="fluo_sample", + target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value", + ) ################## ## mu reference, test whether the signal exists. how to check from config? ################### - - if (int(add_chans_bits) & (1<<2)) !=0: + + if (int(add_chans_bits) & (1 << 2)) != 0: mu_reference = main_data.create_group(name="mu_reference") mu_reference.attrs["NX_class"] = "NXdata" - - main_data.create_soft_link(name="mu_reference", target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value") - - - + main_data.create_soft_link( + name="mu_reference", + target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value", + )