wip from beamline, 22.05.2026 #83

Merged
hitz_s merged 10 commits from feat/digital-twin-adaption-superxas into main 2026-06-15 07:46:56 +02:00
22 changed files with 1099 additions and 222 deletions
@@ -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]
@@ -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"]))
@@ -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,
@@ -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")
@@ -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()
@@ -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"])
@@ -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(
@@ -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:
@@ -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)
@@ -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.
@@ -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)
@@ -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())
# )
@@ -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
@@ -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])
+302
View File
@@ -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
+74
View File
@@ -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
+94
View File
@@ -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)
+27
View File
@@ -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)