diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py index 79cb809..885dcb3 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py @@ -183,7 +183,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): # TRY - if cfg["fm_stripe"] in "Rh (toroid)": + if cfg["fm_stripe"] == "Rh (toroid)": r = bl.fm.r[0] h_cyl = bl.fm.hToroid[0] else: # PT toroid @@ -199,7 +199,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: pos["fm_try"] = {"value": fm_height} # TRX - if cfg["fm_stripe"] in "Rh (toroid)": + if cfg["fm_stripe"] == "Rh (toroid)": x_cyl = -bl.fm.xToroid[0] else: x_cyl = -bl.fm.xToroid[1] @@ -213,7 +213,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: pos["fm_try"] = {"value": fm_height} # TRX - if cfg["fm_stripe"] in "Rh (flat)": + if cfg["fm_stripe"] == "Rh (flat)": x_flat = -bl.fm.xFlat[0] else: x_flat = -bl.fm.xFlat[1] diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py index afa0b29..7ec677d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py @@ -27,7 +27,7 @@ def calc_sideview(cfg: ConfigDict) -> DataDict: beam["y"].append(bl.sourceHeight) beam["x"].append(bl.cm.center[1]) # CM beam["y"].append(bl.sourceHeight) - if cfg["mo1_mode"] in "Monochromatic": + if cfg["mo1_mode"] == "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"])) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py index dcac3dd..0b90b93 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py @@ -65,7 +65,7 @@ def calc_surfaces(cfg: ConfigDict) -> SurfaceDict: 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": + if cfg["mo1_mode"] == "Monochromatic": out["mo1_1"]["x"] = [ xtal_pos - width_beam / 2, xtal_pos + width_beam / 2, diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py index 0cc43f0..b70e751 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py @@ -258,7 +258,7 @@ def fm_ideal_pitch( """ p = bl.fm.center[1] # posFM q = smpl - bl.fm.center[1] # dist posFM to posEX - if fm_focus in "Defocused": + if fm_focus == "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" @@ -294,9 +294,9 @@ def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float: Returns: float: Critical angle in rad """ - if cm_stripe in "Si": + if cm_stripe == "Si": stripe = bl.stripeSi - elif cm_stripe in "Pt": + elif cm_stripe == "Pt": stripe = bl.stripePt else: stripe = bl.stripeRh @@ -320,15 +320,15 @@ def mirror_surface_geometries( dict[str, tuple[float, float, float, float]]: Dictionary mapping surface names to tuples of (x, y, width, height). """ - if mirror in "cm": + if mirror == "cm": surface = bl.cm.surface lim_opt_x = bl.cm.limOptX lim_opt_y = bl.cm.limOptY - elif mirror in "fm_toroid": + elif mirror == "fm_toroid": surface = bl.fm.surfaceToroid lim_opt_x = bl.fm.limOptXToroid lim_opt_y = bl.fm.limOptYToroid - elif mirror in "fm_flat": + elif mirror == "fm_flat": surface = bl.fm.surfaceFlat lim_opt_x = bl.fm.limOptXFlat lim_opt_y = bl.fm.limOptYFlat @@ -354,7 +354,7 @@ def mo_surface_geometries( dict[str, tuple[float, float, float, float]]: Dictionary mapping surface names to tuples of (x, y, width, height). """ - if mo in "mo1": + if mo == "mo1": xtal = bl.mo1.xtal xtal_width = bl.mo1.xtalWidth xtal_offset_x = bl.mo1.xtalOffsetX @@ -416,3 +416,16 @@ def pipe_geometries() -> list[dict[str, np.ndarray]]: } ) return pipes + + +def table_to_smpl_pos(table: str) -> float: + """ + Return the sample position based on the table name. + + Args: + table (str): Table name, e.g. ES1 or ES2 + """ + + if table in bl.tables: + return bl.tables[table]["smpl"] + raise ValueError(f"Table {table} not found in beamline parameter file") 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 7b7ee59..03d1ba9 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -2,6 +2,7 @@ Digital Twin: Custom BEC widget to support the beamline alignment. """ +import socket import sys from pathlib import Path from typing import Literal, cast @@ -20,12 +21,16 @@ from qtpy.QtCore import Qt, QTimer from qtpy.QtGui import QFont from qtpy.QtWidgets import ( QApplication, + QComboBox, QDialog, QDialogButtonBox, + QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, + QScrollArea, + QSizePolicy, QStyle, QVBoxLayout, QWidget, @@ -46,17 +51,19 @@ from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import ( mo1_bragg_angle, mo1_energy_resolution, sldi_gap_to_acc, + table_to_smpl_pos, ) 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 +from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId, ConfigDict +from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboBox, InputNumberField logger = bec_logger.logger -OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml" - +OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml") +OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml") class DigitalTwin(BECWidget, QWidget): """ @@ -70,36 +77,66 @@ class DigitalTwin(BECWidget, QWidget): super().__init__(parent=parent, theme_update=True, *arg, **kwargs) self.get_bec_shortcuts() - # Check if devices are all in config - self.check_config() - self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update()) + self.beamline = self.get_beamline_id() + # Debugging, override beamline! + # self.beamline = BeamlineId.X10DA - central = QWidget() - self.root_layout = QHBoxLayout(central) + self.offset_file = Path() + match self.beamline: + case "x01da": + self.offset_file = OFFSET_FILE_X01DA + case "x10da": + self.offset_file = OFFSET_FILE_X10DA + + # Check if devices are all in config + self.check_bec_config() + self.bec_dispatcher.connect_slot( + self.check_bec_config, MessageEndpoints.device_config_update() + ) + + logger.info(f"Start Digital Twin with beamline {self.beamline} and all devices available") + + self.content_widget = QWidget(self) + self.root_layout = QHBoxLayout(self.content_widget) + self.root_layout.setContentsMargins(6, 6, 6, 6) + self.root_layout.setSpacing(6) self.input_widget = QWidget() self.input_layout = QVBoxLayout(self.input_widget) - self.input = InputPanel() + self.input_layout.setContentsMargins(4, 4, 4, 4) + self.input_layout.setSpacing(6) + self.input = InputPanel(self.beamline) self.settings = SettingsPanel() self.input_layout.addWidget(self.input) self.input_layout.addWidget(self.settings) + self.input_layout.addStretch() self.plot_widget = QWidget() self.plot_layout = QVBoxLayout(self.plot_widget) + self.plot_layout.setContentsMargins(4, 4, 4, 4) + self.plot_layout.setSpacing(6) self.sideview_plot = SideviewPlot() self.surface_plots = SurfacePlots() - self.plot_layout.addWidget(self.sideview_plot) - self.plot_layout.addWidget(self.surface_plots) + self.plot_layout.addWidget(self.sideview_plot, stretch=1) + self.plot_layout.addWidget(self.surface_plots, stretch=1) + self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.mover = MoverPanel(self.dev) - 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.input_scroll = self._scroll_area(self.input_widget, min_width=320, max_width=360) + self.mover_scroll = self._scroll_area(self.mover, min_width=380, max_width=460) - self.setLayout(self.root_layout) + self.root_layout.addWidget(self.input_scroll) + self.root_layout.addWidget(self.plot_widget, stretch=1) + self.root_layout.addWidget(self.mover_scroll) + widget_layout = self.layout() + if widget_layout is None: + widget_layout = QVBoxLayout(self) + widget_layout.setContentsMargins(0, 0, 0, 0) + widget_layout.setSpacing(0) + widget_layout.addWidget(self.content_widget) self.setWindowTitle("Digital Twin") - self.resize(1800, 800) + self.resize(1450, 950) self.input.energy.value_changed_connect(self.calc_assistant) self.input.sldi_hacc.value_changed_connect(self.calc_assistant) @@ -113,7 +150,11 @@ class DigitalTwin(BECWidget, QWidget): self.input.fm_rotx.value_changed_connect(self.calc_assistant) self.input.fm_focx.value_changed_connect(self.calc_assistant) self.input.fm_focy.value_changed_connect(self.calc_assistant) - self.input.smpl.value_changed_connect(self.calc_assistant) + match self.input.smpl: + case InputNumberField(): + self.input.smpl.value_changed_connect(self.calc_assistant) + case ComboBox(): + self.input.smpl.activated_connect(self.calc_assistant) self.input.adapt_reality.clicked_connect(self.adapt_reality) self.settings.load_offsets.clicked_connect(self.load_offsets) @@ -127,12 +168,26 @@ class DigitalTwin(BECWidget, QWidget): self.load_offsets(recalculate=False) self.calc_assistant(identifier="init") - # Timer: update plots every 1 second + # Timer: update reality plots every 1 second self._timer = QTimer(self) - self._timer.setInterval(100) + self._timer.setInterval(1000) self._timer.timeout.connect(self.calc_reality) self._timer.start() + @staticmethod + def _scroll_area(widget: QWidget, min_width: int, max_width: int) -> QScrollArea: + """Wrap a side panel in a compact vertical scroll area.""" + scroll = QScrollArea() + scroll.setWidgetResizable(True) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding) + scroll.setWidget(widget) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll.setMinimumWidth(min_width) + scroll.setMaximumWidth(max_width) + scroll.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) + return scroll + def apply_theme(self, theme: Literal["dark", "light"]): """ Apply the theme @@ -144,16 +199,48 @@ class DigitalTwin(BECWidget, QWidget): self.surface_plots.apply_theme(theme) self.mover.apply_theme(theme) + def get_beamline_id(self) -> BeamlineId: + """ + Based on the bec servers hostname, tries to extract the beamline + identifier (e.g. x01da, x10da, etc). + + Raises: + ValueError if beamline cannot be extracted from hostname or beamline not implemented. + """ + bec_hostname = socket.gethostname() + start = bec_hostname.find("x") + if start != -1: + beamline = bec_hostname[start : start + 5] + match beamline: + case "x01da": + return BeamlineId.X01DA + case "x10da": + return BeamlineId.X10DA + case _: + raise ValueError(f"Not implemented beamline {beamline}") + else: + logger.warning(f"Failed to extract beamline from bec server hostname {bec_hostname}") + choice = input("Do you want to manually select a beamline? (yes/no): ").strip().lower() + if choice in ["yes", "y"]: + bl = input(f"Choose from: {[bl.value for bl in BeamlineId]}") + if bl in BeamlineId: + logger.info(f'Manually selected beamline {bl}') + return BeamlineId(bl) + else: + raise ValueError(f'Wrong selection {bl}') + else: + raise ValueError('Cannot open digital twin without a beamline') + @SafeSlot() - def check_config(self, *args): + def check_bec_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: + reload_config = (args[0] if args else {}).get("action") == "reload" + if reload_config: self._timer.stop() devices = [ "abs", @@ -161,7 +248,7 @@ class DigitalTwin(BECWidget, QWidget): "sldi_gapy", "cm_trx", "cm_try", - "cm_bnd_radius", + "cm_bnd", "cm_rotx", "mo1_bragg", "mo1_trx", @@ -171,23 +258,33 @@ class DigitalTwin(BECWidget, QWidget): "bm1_try", "fm_trx", "fm_try", - "fm_bnd_radius", + "fm_bnd", "fm_rotx", "fm_roty", "fm_rotz", - "sl2_centery", - "sl2_gapy", "bm2_try", "ot_try", - "ot_rotx", "es0wi_try", - "ot_es1_trz", ] + if self.beamline == "x01da": # X01DA specific devices + devices.extend( + [ + "cm_bnd_radius", + "fm_bnd_radius", + "sl2_centery", + "sl2_gapy", + "ot_try", + "ot_es1_trz", + ] + ) + if self.beamline == "x10da": # X10DA specific devices + devices.extend(["mo1_rotx", "es1_try", "es1ic0_try", "es1ic1_try", "es1ic2_try"]) + while True: missing = [d for d in devices if d not in self.dev] if not missing: break - dialog = QDialog() + dialog = QDialog(self) dialog.setWindowTitle("Digital Twin - Config Check") dialog.setFixedWidth(400) layout = QVBoxLayout() @@ -234,7 +331,7 @@ class DigitalTwin(BECWidget, QWidget): running_app = QApplication.instance() if running_app is not None: running_app.exit(0) - if reload: + if reload_config: self._timer.start() @SafeSlot() @@ -309,10 +406,10 @@ class DigitalTwin(BECWidget, QWidget): ConfigDict: config of the assistant """ fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "Manual": fm_rotx = self.input.fm_rotx.value() fm_qy = None - elif fm_focus in "Focused": + elif fm_focus == "Focused": fm_rotx = self.input.fm_rotx_ideal.value() fm_qy = None else: # Focused @@ -327,6 +424,13 @@ class DigitalTwin(BECWidget, QWidget): 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}!" + match self.input.smpl: + case InputNumberField(): + smpl = self.input.smpl.value() + case ComboBox(): + table = self.input.smpl.currentText() + smpl = table_to_smpl_pos(table) + config: ConfigDict = { "energy": self.input.energy.value(), "h_acc": self.input.sldi_hacc.value(), @@ -342,7 +446,7 @@ class DigitalTwin(BECWidget, QWidget): "fm_trx": fm_trx, "fm_qy": fm_qy, "fm_gain_height": 1, - "smpl": self.input.smpl.value(), + "smpl": smpl, } # Apply offsets @@ -508,9 +612,52 @@ class DigitalTwin(BECWidget, QWidget): 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"]) + match self.input.smpl: + case InputNumberField(): + self.input.smpl.set_number(pos["ot_es1_trz"]) + case ComboBox(): + table = self.ask_table_selection(self.input.smpl.currentText()) + self.input.smpl.set_current_text(table) + self.calc_assistant(identifier="init") + def ask_table_selection(self, preset=None) -> str | None: + """ + Opens a dialog asking the user to select a table (ES1 or ES2). + + Args: + preset (str): Preset text for the table, either 'ES1' or 'ES2'. + + Returns: + The selected table ('ES1' or 'ES2'), or None if the user cancelled. + """ + dialog = QDialog(self) + dialog.setWindowTitle("Select Table") + layout = QVBoxLayout(dialog) + + text = QLabel("Select the current table in use.") + + combo = QComboBox() + choice = ["ES1", "ES2"] + combo.addItems(choice) + if preset is not None: + if preset in choice: + combo.setCurrentText(preset) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + + layout.addWidget(text) + layout.addWidget(combo) + layout.addWidget(buttons) + + if dialog.exec() == QDialog.DialogCode.Accepted: + return combo.currentText() + return None + @SafeSlot() def load_offsets(self, *_, recalculate: bool = True): """ @@ -523,11 +670,10 @@ class DigitalTwin(BECWidget, QWidget): if self.offsets == {}: # Load offsets - 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: + if not self.offset_file.exists(): + raise FileNotFoundError(f"Offset file not found: {self.offset_file}") + + with self.offset_file.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) if not isinstance(data, dict): @@ -568,7 +714,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(OFFSET_FILE) + file = QLabel(str(self.offset_file)) file.setWordWrap(True) font = QFont() font.setItalic(True) @@ -600,13 +746,13 @@ class DigitalTwin(BECWidget, QWidget): selection of the focus strategy. """ fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "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": + elif fm_focus == "Focused": self.input.fm_rotx.setVisible(False) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(False) @@ -659,7 +805,7 @@ class DigitalTwin(BECWidget, QWidget): """ fm_stripe = self.input.fm_stripe.currentText() fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "Manual": fm_rotx = -self.input.fm_rotx.value() * 1e-3 else: fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3 @@ -745,11 +891,11 @@ class DigitalTwin(BECWidget, QWidget): Calculates bragg angle in rad """ xtal = self.input.mo1_xtal.currentText() - if xtal in "Si(111)": + if xtal == "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)": + elif xtal == "Si(311)": d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)[ "mo1_bragg_crystal_d_spacing_si311" ]["value"] @@ -767,7 +913,7 @@ class DigitalTwin(BECWidget, QWidget): Updates the monochromator input group based on the selection of the mode. """ - if self.input.mo1_mode.currentText() in "Monochromatic": + if self.input.mo1_mode.currentText() == "Monochromatic": self.input.mo1_xtal.setVisible(True) self.input.mo1_bragg_angle.setVisible(True) self.input.mo1_eres.setVisible(True) @@ -784,7 +930,12 @@ class DigitalTwin(BECWidget, QWidget): Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText() ) fm_stripe = self.input.fm_stripe.currentText() - smpl = self.input.smpl.value() + match self.input.smpl: + case InputNumberField(): + smpl = self.input.smpl.value() + case ComboBox(): + table = self.input.smpl.currentText() + smpl = table_to_smpl_pos(table) sldi_hacc = self.input.sldi_hacc.value() * 1e-3 sldi_vacc = self.input.sldi_vacc.value() * 1e-3 fm_focx = self.input.fm_focx.value() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py index 093dae9..2620b9b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py @@ -2,9 +2,12 @@ Panel for user inputs of the digital twin widget """ -# pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from typing import Union +# pylint: disable=E0611 +from qtpy.QtWidgets import QVBoxLayout, QWidget + +from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( Button, ComboBox, @@ -15,12 +18,18 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( class InputPanel(QWidget): - """Panel for user inputs of the digital twin widget""" + """ + Panel for user inputs of the digital twin widget - def __init__(self, parent=None): + Args: + beamline (BeamlineId): Beamline id type + """ + + def __init__(self, beamline: BeamlineId, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) # Adapt to reality self.adapt_reality = Button(label_button="Adapt to reality", enabled=True) @@ -91,9 +100,11 @@ class InputPanel(QWidget): ) # Focusing Mirror - self.fm_stripe = ComboBox( - "fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"] - ) + stripes: dict[BeamlineId, list[str]] = { + BeamlineId.X01DA: ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"], + BeamlineId.X10DA: ["Rh (toroid)", "Pt (toroid)"], + } + self.fm_stripe = ComboBox("fm_stripe", "Stripe", stripes[beamline]) self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"]) self.fm_rotx = InputNumberField( "fm_rotx", @@ -144,18 +155,9 @@ class InputPanel(QWidget): # 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, - ) + self.smpl = self._create_smpl(beamline) - # Assemble complete assitant group + # Assemble complete assistant group self.input_group = Group( "User Input", [ @@ -172,3 +174,19 @@ class InputPanel(QWidget): self._layout.addWidget(self.input_group) self._layout.addStretch() + + def _create_smpl(self, beamline: BeamlineId) -> Union[InputNumberField, ComboBox]: + match beamline: + case BeamlineId.X01DA: + return InputNumberField( + "smpl", + "Sample Position", + unit="mm", + init=23511, + decimals=0, + single_step=100, + ll=23000, + hl=30000, + ) + case BeamlineId.X10DA: + return ComboBox("smpl", "Sample Position", ["ES1", "ES2"]) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py index 3f26f06..fcf3703 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py @@ -5,7 +5,7 @@ Panel to move an axis to a certain position from typing import Literal # pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import QVBoxLayout, QWidget from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import ( AbsorberWidget, @@ -20,7 +20,8 @@ class MoverPanel(QWidget): def __init__(self, dev, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) self.mover_widgets = [] @@ -189,7 +190,7 @@ class MoverPanel(QWidget): ) self.mover_widgets.append(self.es0wi_try) - self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try]) + self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try]) # Experimental Station 1 self.ot_es1_trz = MoveWidget( @@ -197,7 +198,7 @@ class MoverPanel(QWidget): ) self.mover_widgets.append(self.ot_es1_trz) - self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz]) + self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz]) # Assemble complete mover group self.mover_group = Group( diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py index 5e17c85..8cbafce 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py @@ -33,6 +33,8 @@ class SurfacePlots(QWidget): def __init__(self, parent=None): super().__init__(parent=parent) self._layout = QHBoxLayout(self) + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(6) self.surfaces: dict[str, SurfaceDict] = { "assistant": { @@ -74,7 +76,7 @@ class SurfacePlots(QWidget): # Create surfaces for idx, scene in enumerate(self.surfaces): for name, _ in self.surfaces[scene].items(): - if scene in "assistant": + if scene == "assistant": brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) pen = pg.mkPen( QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine @@ -130,7 +132,7 @@ class SurfacePlots(QWidget): for idx, scene in enumerate(self.surfaces): for name, _ in self.surfaces[scene].items(): - if scene in "assistant": + if scene == "assistant": brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) pen = pg.mkPen( QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine @@ -165,13 +167,13 @@ class SurfacePlots(QWidget): self.texts.append(text) for name, plot in self.plots.items(): - if name in "cm": + if name == "cm": plot_surface(plot["widget"], mirror_surface_geometries("cm")) - elif name in "mo1_1": + elif name == "mo1_1": plot_surface(plot["widget"], mo_surface_geometries("mo1", 0)) - elif name in "mo1_2": + elif name == "mo1_2": plot_surface(plot["widget"], mo_surface_geometries("mo1", 1)) - elif name in "fm": + elif name == "fm": plot_surface(plot["widget"], mirror_surface_geometries("fm_flat")) plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid")) else: @@ -202,7 +204,8 @@ class SideviewPlot(QWidget): def __init__(self, parent=None): super().__init__(parent=parent) self._layout = QVBoxLayout(self) - # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(0) self.plot_widget = pg.PlotWidget() self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False) @@ -223,7 +226,7 @@ class SideviewPlot(QWidget): self.walls = [] for idx, scene in enumerate(self.data.keys()): - if scene in "assistant": + if scene == "assistant": pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine) z_value = 2 else: @@ -243,7 +246,6 @@ class SideviewPlot(QWidget): self.plot_widget.hideButtons() self._layout.addWidget(self.plot_group) - self._layout.addStretch() self.plot_vacuum_pipes() self.plot_walls() @@ -281,7 +283,7 @@ class SideviewPlot(QWidget): self.text_color = (0, 0, 0) for idx, scene in enumerate(self.data): - if scene in "assistant": + if scene == "assistant": 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: diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py index 8947ea3..88ce3e1 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py @@ -3,7 +3,7 @@ Settings panel for the digital twin widget """ # pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import QVBoxLayout, QWidget from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( Button, @@ -18,7 +18,8 @@ class SettingsPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) # Reload offsets self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/types.py b/debye_bec/bec_widgets/widgets/digital_twin/types.py index 674b59f..d5c0393 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/types.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/types.py @@ -1,8 +1,18 @@ """Types used for the beamline config and for plotting data""" +from enum import Enum from typing import TypedDict +class BeamlineId(str, Enum): + """ + Identifier for supported beamlines. + """ + + X01DA = "x01da" + X10DA = "x10da" + + class ConfigDict(TypedDict): """ Typed dictionary representing the beamline configuration. diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py index 8266b58..65efd74 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py @@ -128,8 +128,8 @@ class MotionWorker(QObject): """ position_changed = Signal(float) - error = Signal(bool) # True = error - finished = Signal(bool) # True = reached target, False = stopped + error = Signal() + finished = Signal() def __init__(self, dev, motor, target_pos: float): super().__init__() @@ -284,7 +284,7 @@ class MotionWorker(QObject): 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) + self.error.emit() break self.finished.emit() @@ -315,35 +315,33 @@ class MoveWidget(QWidget): self.decimals = decimals layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.setSpacing(4) # Name self.label = QLabel(label) - self.label.setFixedWidth(100) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(76) self.label.setWordWrap(True) layout.addWidget(self.label) # Target self.target_label = QLabel("-") - self.target_label.setFixedWidth(100) + self.target_label.setFixedWidth(84) layout.addWidget(self.target_label) # Feedback self.fb_label = QLabel("-") - self.fb_label.setFixedWidth(100) + self.fb_label.setFixedWidth(84) layout.addWidget(self.fb_label) # Status icon self.status_icon = StatusIcon() - self.status_icon.setFixedWidth(30) - self.status_icon.setContentsMargins(0, 0, 10, 0) + self.status_icon.setFixedWidth(24) layout.addWidget(self.status_icon) # Start / Stop button self.btn_action = QPushButton("Move") - self.btn_action.setFixedWidth(90) + self.btn_action.setFixedWidth(64) self.btn_action.setFixedHeight(20) self.btn_action.clicked.connect(self._on_button_clicked) layout.addWidget(self.btn_action) @@ -485,7 +483,7 @@ class MoveWidget(QWidget): def _on_motion_finished(self): """Finished a movement""" target = self.target - if self.status not in Status.ERROR: + if self.status != Status.ERROR: if abs(self.fb - target) <= self.deadband: self._set_status(Status.IN_POSITION) else: @@ -522,35 +520,33 @@ class AbsorberWidget(QWidget): self.text_color = (0, 0, 0) layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.setSpacing(4) # Name self.label = QLabel(label) - self.label.setFixedWidth(100) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(76) self.label.setWordWrap(True) layout.addWidget(self.label) # Blank self.blank_label = QLabel("") - self.blank_label.setFixedWidth(100) + self.blank_label.setFixedWidth(84) layout.addWidget(self.blank_label) # Feedback self.fb_label = QLabel("-") - self.fb_label.setFixedWidth(100) + self.fb_label.setFixedWidth(84) layout.addWidget(self.fb_label) # Blank icon self.blank_icon = QLabel("") - self.blank_icon.setFixedWidth(30) - self.blank_icon.setContentsMargins(0, 0, 10, 0) + self.blank_icon.setFixedWidth(24) layout.addWidget(self.blank_icon) # Open self.btn_action = QPushButton("Open") - self.btn_action.setFixedWidth(90) + self.btn_action.setFixedWidth(64) self.btn_action.setFixedHeight(20) self.btn_action.clicked.connect(self._on_button_clicked) layout.addWidget(self.btn_action) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py index 5a0f59b..b127418 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py @@ -21,11 +21,17 @@ from qtpy.QtWidgets import ( QWidget, ) +LABEL_WIDTH = 118 +ROW_MARGINS = (4, 0, 4, 0) +ROW_SPACING = 6 + class Group(QGroupBox): def __init__(self, label, widgets): super().__init__(label) self.layout = QVBoxLayout(self) # type: ignore + self.layout.setContentsMargins(6, 6, 6, 6) + self.layout.setSpacing(4) for widget in widgets: self.layout.addWidget(widget) # type: ignore @@ -34,16 +40,14 @@ class NumberIndicator(QWidget): def __init__(self, label="", unit=None, highlight=False, decimals=3): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.val = QLabel("-") self.val.setAlignment(Qt.AlignTop) # type: ignore - # self.val.setFixedWidth(140) layout.addWidget(self.val) self.unit = unit self.highlight = highlight @@ -85,12 +89,11 @@ class InputNumberField(QWidget): ): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.identifier = identifier self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.val = QDoubleSpinBox() @@ -102,7 +105,6 @@ class InputNumberField(QWidget): self.val.setSuffix(" " + unit) if prefix is not None: self.val.setPrefix(prefix + " ") - # self.val.setFixedWidth(140) layout.addWidget(self.val) def set_number(self, number): @@ -124,19 +126,18 @@ class InputNumberField(QWidget): class ComboBox(QWidget): - def __init__(self, identifier="", label="", enums=[]): + def __init__(self, identifier="", label="", enums=None): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.identifier = identifier self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.value = QComboBox() - for entry in enums: + for entry in enums or []: self.value.addItem(entry) layout.addWidget(self.value) @@ -168,15 +169,13 @@ class Button(QWidget): def __init__(self, label=None, label_button: str = "", enabled=False): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) if label is not None: self.label = QLabel(label) - self.label.setFixedWidth(140) + self.label.setFixedWidth(LABEL_WIDTH) layout.addWidget(self.label) self.button = QPushButton(label_button) - if label is not None: - self.button.setFixedWidth(160) self.enable_button(enabled) layout.addWidget(self.button) @@ -204,11 +203,10 @@ class TextIndicator(QWidget): def __init__(self, label): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.text = QLabel("-") @@ -223,84 +221,3 @@ class TextIndicator(QWidget): def setColor(self, color: str): self.text.setStyleSheet(f"QLabel {{color:{color}}}") - - -# class Button(QWidget): -# def __init__(self, label, label_button): -# 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.button = QPushButton(label_button) -# self.button.setStyleSheet("color: black; background-color: dodgerblue;") -# self.button.setFixedWidth(160) -# layout.addWidget(self.button) - -# def set_on_press(self, func): -# """Connect a function to the button press.""" -# self.button.clicked.connect(func) - -# def enable_button(self): -# self.button.setEnabled(True) -# self.button.setStyleSheet("color: black; background-color: dodgerblue;") - -# def disable_button(self): -# self.button.setEnabled(False) -# self.button.setStyleSheet("color: black; background-color: grey;") - -# def set_button_text(self, text): -# self.button.setText(text) - -# class LED(QWidget): -# def __init__(self, states, colors, label): -# super().__init__() -# self.states = states -# self.colors = colors -# 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.led = QLabel() -# self.led.setFixedWidth(160) -# layout.addWidget(self.led) - -# def apply_color(self, val): -# color = self.colors[self.states.index(val)] -# self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;") - -# class InputTextField(QWidget): -# def __init__(self, topic, label): -# super().__init__() -# self.topic = topic -# 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.val = QLineEdit() -# self.val.setPlaceholderText('0') -# # self.val.setFixedWidth(140) -# layout.addWidget(self.val) - -# def set_text(self, text): -# self.val.setText(text) - -# def has_focus(self) -> bool: -# return self.val.hasFocus() - -# def text(self) -> str: -# return self.val.text() - -# def set_on_return(self, func): -# """Connect a function to the Enter/Return key press.""" -# self.val.returnPressed.connect( -# partial(func, self.val, self.topic, lambda: self.val.text()) -# ) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py index 630d7fc..214cb7d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py @@ -304,6 +304,8 @@ smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight]) smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight]) +tables = {} + # Vacuum pipes # DN40CF ID = 35 mm oder 37 mm # DN50CF ID = 47.5 mm diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml b/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py new file mode 100644 index 0000000..5bd76c3 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py @@ -0,0 +1,269 @@ +""" +X10DA / SuperXAS Beamline Parameters. +This file describes the parameter of each component of the SuperXAS beamline +to be used for raytracing and geometrical calculations. +""" + +from collections import namedtuple + +import numpy as np +import xrt.backends.raycing.materials as rm + +# 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] + +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 + +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"] +) + +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"]) + +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 +slits = namedtuple("slits", ["name", "center", "maxDivH", "maxDivV"]) + +feSlits = slits(name="FE-SLITS", center=(0, 5290, sourceHeight), maxDivH=1.8e-3, maxDivV=0.8e-3) + +# Filters +filt = namedtuple( + "filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"] +) + +feWindow = filt( + name="FE-WINDOW", + center=(0.0, 6158, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-6, 6), + limPhysY=(-3.0, 3.0), + surface="None", + material=filterDiamond, + thickness=0.1, +) +feWindow = feWindow._replace( + surface="CVD Diamond window {0:0.0f} $\mu$m".format(feWindow.thickness * 1e3) +) + +feFilt = filt( + name="FE-FI", + center=(0.0, 6590, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-15, 15), + limPhysY=(-10, 10), + surface="None", + material=filterGraphite, + thickness=0.25, +) +feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\mu$m".format(feFilt.thickness * 1e3)) + +# 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", + center=[0, 7618, sourceHeight], + surface=("Si", "Pt", "Rh"), + material=(stripeSi, stripePt, stripeRh), + limPhysX=(-30, 30), + limPhysY=(-600, 600), + limOptX=((-21, -8, 5), (-11, 2, 21)), + limOptY=((-500, -500, -500), (500, 500, 500)), + R=[3e6, 15e6], + pitch=[1.4e-3, 4.5e-3], + 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, 8760, sourceHeight], opening=[-39 / 2, 39 / 2, -10, 29] +) # left, right, bottom, top + +opWbBsBlock = apertures( + name="OP-WB-BS-BLOCK", center=[0.0, 13606 - 135, sourceHeight], opening=[-18.0, 18.0, 42, 76] +) # left, right, bottom, top + +opSlits = apertures( + name="OP-SLITS", center=[0, 14145 - 135, sourceHeight], opening=[-35 / 2, 35 / 2, 47.5, 82.5] +) + +# Monochromator +monochromator = namedtuple( + "monochromator", + [ + "name", + "center", + "xtal", + "material1", + "material2", + "xtalWidth", + "xtalOffsetX", + "xtalLength1", + "xtalLength2", + "xtalGap", + "rotOffset", + "heightOffset", + "braggLim", + "jack1", + "jack2", + "jack3", + "tx", + ], +) + +mo1 = monochromator( + name="OP-CCM1", + center=[0.0, 11670 - 135, sourceHeight], + xtal=("Si311", "Si111"), + material1=(si311_1, si111_1), + material2=(si311_2, si111_2), + xtalWidth=(20, 20), + xtalOffsetX=(-19.2, 19.2), + xtalLength1=(60, 60), + xtalLength2=(60, 60), + xtalGap=(8, 8), + rotOffset=6, # not sure what it is + heightOffset=8.5, # not sure what it is + braggLim=[4, 35], + jack1=[0.0, 11350.0, 0.0], # Tripod not available! + jack2=[-400.0, 12350.0, 0.0], + jack3=[400.0, 12350.0, 0.0], + tx=0.0, +) # X-Stage [x] + +# Focusing mirror +focusingMirror = namedtuple( + "focusingMirror", + [ + "name", + "center", + "surfaceToroid", + "materialToroid", + "limPhysXToroid", + "limPhysYToroid", + "limOptXToroid", + "limOptYToroid", + "R", + "pitch", + "r", + "xToroid", + "hToroid", + "jack1", + "jack2", + "jack3", + "tx1", + "tx2", + ], +) + +fm = focusingMirror( + name="OP-FM", + center=[0.0, 15580 - 135, sourceHeight], + surfaceToroid=("Rh (toroid)", "Pt (toroid)"), + materialToroid=(stripeRh, stripePt), + limPhysXToroid=(-54.0, 54.0), + limPhysYToroid=(-565.0, 565.0), + limOptXToroid=((4.865, -40.882), (43.388, -4.865)), + limOptYToroid=((-500.0, -500.0), (500.0, 500.0)), + R=[3e6, 15e6], + pitch=[1.4e-3, 4.5e-3], + r=[30, 20], + xToroid=[24.126, -22, 874], # offset in local x + hToroid=[7.0, 11.3], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. + jack1=[0.0, 14980.0, 0.0], + jack2=[-75.0, 16180.0, 0.0], + jack3=[75.0, 16180.0, 0.0], + tx1=[0.0, -575.0], # X-Stage 1 [x, y] + tx2=[0.0, 575.0], +) # X-Stage 2 [x, y] + +ehWindow = filt( + name="EH-WINDOW", + center=(0.0, 22225 - 135, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-10.0, 10.0), + limPhysY=(17.5, 92.5), + surface="None", + material=filterBe, + thickness=0.25, +) +ehWindow = ehWindow._replace( + surface="Beryllium window {0:0.0f} $\mu$m".format(ehWindow.thickness * 1e3) +) + +# Sample +sample = namedtuple("sample", ["name", "center"]) + +smpl = sample(name="OP-SMPL", center=[0, 24000 - 135, sourceHeight]) + +ES1 = sample(name="EH-ES1", center=[0, 24000, sourceHeight]) +ES2 = sample(name="EH-ES2", center=[0, 25000, sourceHeight]) diff --git a/debye_bec/device_configs/x01da_sim.yaml b/debye_bec/device_configs/x01da_sim.yaml new file mode 100644 index 0000000..ff1467e --- /dev/null +++ b/debye_bec/device_configs/x01da_sim.yaml @@ -0,0 +1,302 @@ + +################################### +## Frontend Absorber ## +################################### + +abs: + readoutPriority: baseline + description: Frontend Absorber + deviceClass: debye_bec.devices.sim.absorber.SimAbsorber + deviceConfig: {} + enabled: true + +################################### +## Frontend Slits ## +################################### + +sldi_gapx: + readoutPriority: baseline + description: Front-end slit diaphragm X-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 4.75 + enabled: true + +sldi_gapy: + readoutPriority: baseline + description: Front-end slit diaphragm Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 1.5 + enabled: true + +################################### +## Collimating Mirror ## +################################### + +cm_bnd: + readoutPriority: baseline + description: Collimating Mirror bender + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2573 + enabled: true + +cm_bnd_radius: + readoutPriority: baseline + description: Collimating Mirror Bending Radius (simulated) + deviceClass: ophyd.Signal + deviceConfig: + value: 5.51 + readOnly: true + enabled: true + +cm_rotx: + readoutPriority: baseline + description: Collimating Mirror Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2.498 + enabled: true + +cm_rotz: + readoutPriority: baseline + description: Collimating Mirror Roll + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0 + enabled: true + +cm_roty: + readoutPriority: baseline + description: Collimating Mirror Yaw + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0 + enabled: true + +cm_trx: + readoutPriority: baseline + description: Collimating Mirror Center Point X + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -1.5 + enabled: true + +cm_try: + readoutPriority: baseline + description: Collimating Mirror Center Point Y + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0.2 + enabled: true + + +################################### +## Monochromator ## +################################### + +mo1_bragg: + readoutPriority: baseline + deviceClass: debye_bec.devices.sim.mo1_bragg.SimMo1BraggPositioner + deviceConfig: {} + enabled: true + +mo1_try: + readoutPriority: baseline + description: Monochromator Y Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 30.4 + enabled: true + +mo1_trx: + readoutPriority: baseline + description: Monochromator X Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 21.2 + enabled: true + +################################### +## Optics Slits + Beam Monitor 1 ## +################################### + +bm1_try: + readoutPriority: baseline + description: Beam Monitor 1 Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 105 + enabled: true + +sl1_centery: + readoutPriority: baseline + description: Optics slits 1 Y-center + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 52 + enabled: true + +sl1_gapy: + readoutPriority: baseline + description: Optics slits 1 Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true + +################################### +## Focusing Mirror ## +################################### + +fm_bnd: + readoutPriority: baseline + description: Focusing Mirror bender + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -30500 + enabled: true + +fm_bnd_radius: + readoutPriority: baseline + description: Focusing Mirror Bending Radius (simulated) + deviceClass: ophyd.Signal + deviceConfig: + value: 6.55 + readOnly: true + enabled: true + +fm_rotx: + readoutPriority: baseline + description: Focusing Morror Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2.578 + enabled: true + +fm_roty: + readoutPriority: baseline + description: Focusing Morror Yaw + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.038 + enabled: true + +fm_rotz: + readoutPriority: baseline + description: Focusing Morror Roll + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.004 + enabled: true + +fm_trx: + readoutPriority: baseline + description: Focusing Morror Center Point X + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -49.5 + enabled: true + +fm_try: + readoutPriority: baseline + description: Focusing Morror Center Point Y + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 51.75 + enabled: true + +################################### +## Optics Slits + Beam Monitor 2 ## +################################### + +bm2_try: + readoutPriority: baseline + description: Beam Monitor 2 Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 115 + enabled: true + +sl2_centery: + readoutPriority: baseline + description: Optics slits 2 Y-center + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 58.9 + enabled: true + +sl2_gapy: + readoutPriority: baseline + description: Optics slits 2 Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true + +################################### +## Optical Table ## +################################### + +ot_es1_trz: + readoutPriority: baseline + description: Optical Table ES1 Z-Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 23508 + enabled: true + +ot_try: + readoutPriority: baseline + description: Optical Table Y-Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 60.185 + enabled: true + +ot_rotx: + readoutPriority: baseline + description: Optical Table Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.2 + enabled: true + +################################### +## Exit Window ## +################################### + +es0wi_try: + readoutPriority: baseline + description: End Station 0 Exit Window Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true diff --git a/debye_bec/devices/sim/absorber.py b/debye_bec/devices/sim/absorber.py new file mode 100644 index 0000000..fb3b492 --- /dev/null +++ b/debye_bec/devices/sim/absorber.py @@ -0,0 +1,74 @@ +"""Frontend Absorber""" + +from __future__ import annotations + +import enum +from typing import TYPE_CHECKING + +from ophyd import Component as Cpt +from ophyd.sim import SynSignal +from ophyd_devices import CompareStatus, DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + + +if TYPE_CHECKING: + from bec_lib.devicemanager import ScanInfo + +class AbsorberError(Exception): + """Absorber specific exception""" + +class STATUS(int, enum.Enum): + """Absorber States""" + + MOVING_CLOSE = 0 + OPEN = 1 + MOVING_OPEN = 2 + CLOSED = 3 + NOT_ENABLED = 4 + TIMEOUT_CLOSE = 5 + TIMEOUT_OPEN = 6 + CLOSE_LS_LOST = 7 + OPEN_LS_LOST = 8 + CLOSE_LS_NOT_FREE = 9 + OPEN_LS_NOT_FREE = 10 + ERROR_LS = 11 + TO_CONNECT = 12 + MAN_OPEN = 13 + UNDEFINED = 14 + +class SimAbsorber(PSIDeviceBase): + """Simulated class for the Frontend Absorber""" + + USER_ACCESS = ["open", "close"] + + request = Cpt(SynSignal, name="request", kind="config", doc="Open/Close Absorber") + status = Cpt(SynSignal, name="status", kind="normal", doc="Absorber Status") + status_string = Cpt(SynSignal, name="status_string", kind="normal", doc="Absorber Status") + + def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): + super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) + + self.timeout_for_move = 10 + # Set initial simulated state + self.status.put(STATUS.CLOSED) + self.status_string.put("CLOSED") + + def open(self) -> DeviceStatus | None: + """Open the Absorber""" + if self.status.get() == STATUS.CLOSED: + self.status.put(STATUS.OPEN) + self.status_string.put("OPEN") + status_open = CompareStatus(self.status, STATUS.OPEN, timeout=self.timeout_for_move) + return status_open + else: + return None + + def close(self) -> DeviceStatus | None: + """Close the Absorber""" + if self.status.get() == STATUS.OPEN: + self.status.put(STATUS.CLOSED) + self.status_string.put("CLOSED") + status_close = CompareStatus(self.status, STATUS.CLOSED, timeout=self.timeout_for_move) + return status_close + else: + return None diff --git a/debye_bec/devices/sim/mo1_bragg.py b/debye_bec/devices/sim/mo1_bragg.py new file mode 100644 index 0000000..7378d93 --- /dev/null +++ b/debye_bec/devices/sim/mo1_bragg.py @@ -0,0 +1,94 @@ +"""Module for the Mo1 Bragg positioner""" + +from ophyd import Component as Cpt +from ophyd import ( + Device, + DeviceStatus, +) +from ophyd.sim import SynSignal, Signal +from ophyd_devices.sim.sim_positioner import ReadOnlySignal +from ophyd.ophydobj import Kind + +from debye_bec.devices.sim.positioner import SimPositionerWithInitVal + + +class SimMo1BraggCrystalCurrent(Device): + + d_spacing = Cpt(Signal, value=3.1344024, kind=Kind.normal) + bragg_off = Cpt(Signal, value=1.01063, kind=Kind.normal) + phi_off = Cpt(Signal, value=0.07078, kind=Kind.normal) + azm_off = Cpt(Signal, value=0.0425, kind=Kind.normal) + miscut = Cpt(Signal, value=0.05, kind=Kind.normal) + xtal = Cpt(Signal, value=0, kind=Kind.normal) + xtal_string = Cpt(Signal, value="Si(111)", kind=Kind.normal) + + +class SimMo1BraggCrystal(Device): + + current = Cpt(SimMo1BraggCrystalCurrent, "") + d_spacing_si111 = Cpt(Signal, value=3.1344024, kind=Kind.normal) + d_spacing_si311 = Cpt(Signal, value=1.6375, kind=Kind.normal) + + +class SimMo1BraggPositioner(SimPositionerWithInitVal): + + crystal = Cpt(SimMo1BraggCrystal, "") + angle = Cpt(Signal, value=6.310127056633, kind=Kind.normal) + + ####### Energy PVs ####### + + readback = Cpt(SynSignal, name="readback", kind="hinted") + setpoint = Cpt(SynSignal, name="setpoint", kind="normal") + motor_is_moving = Cpt(SynSignal, name="motor_is_moving", kind="normal") + low_lim = Cpt(SynSignal, name="low_lim", kind="config") + high_lim = Cpt(SynSignal, name="high_lim", kind="config") + velocity = Cpt(SynSignal, name="velocity", kind="config") + + ####### Move Command PVs ####### + + move_abs = Cpt(SynSignal, name="move_abs", kind="config") + move_stop = Cpt(SynSignal, name="move_stop", kind="config") + + SUB_READBACK = "readback" + _default_sub = SUB_READBACK + SUB_PROGRESS = "progress" + + def __init__(self, *, name: str, prefix: str = "", **kwargs): + super().__init__(name=name, prefix=prefix, **kwargs) + + self.tolerance.put(0.001) + + self._set_status = None + self.readback.put(17995) + self.setpoint.put(17995) + self.motor_is_moving.put(0) + self.low_lim.put(-100.0) + self.high_lim.put(100.0) + self.velocity.put(1.0) + + @property + def position(self): + return self.readback.get() + + @property + def egu(self) -> str: + """Return the engineering units of the positioner""" + return "eV" + + def set(self, new_pos: float, timeout: float = 10.0) -> DeviceStatus: + """Move to new_pos immediately (no real motion).""" + self.motor_is_moving.put(1) + self.readback.put(new_pos) + self.setpoint.put(new_pos) + self._position = new_pos + self.motor_is_moving.put(0) + self._run_subs(sub_type=self.SUB_READBACK, value=new_pos, obj=self) + + status = DeviceStatus(self) + status._finished() + return status + + def stop(self, success: bool = False): + self.motor_is_moving.put(0) + if self._set_status is not None and not self._set_status.done: + self._set_status._finished(success=success) \ No newline at end of file diff --git a/debye_bec/devices/sim/positioner.py b/debye_bec/devices/sim/positioner.py new file mode 100644 index 0000000..119cb36 --- /dev/null +++ b/debye_bec/devices/sim/positioner.py @@ -0,0 +1,27 @@ +from ophyd_devices.sim.sim_positioner import SimPositioner + +class SimPositionerWithInitVal(SimPositioner): + """SimPositioner with support for an initial readback value via sim_init. + + Example YAML config: + samx: + deviceClass: my_plugin.devices.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 1234.0 + enabled: true + """ + + def __init__(self, *args, sim_init: dict = None, **kwargs): + # Extract init_val before passing sim_init to the parent, + # since the parent's set_init doesn't know about it. + self._init_val = None + if sim_init: + self._init_val = sim_init.pop("init_val", None) + + super().__init__(*args, sim_init=sim_init if sim_init else None, update_frequency=10, **kwargs) + + self.tolerance.put(0.001) + + if self._init_val is not None: + self._update_readback(self._init_val) \ No newline at end of file