wip: digital twin
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m9s

This commit is contained in:
x01da
2026-05-11 10:16:42 +02:00
parent fe43dafac8
commit 3e959e6c5d
8 changed files with 452 additions and 182 deletions
@@ -17,19 +17,20 @@ def calc_positions(cfg):
tryb = -np.arctan(cfg['v_acc'])*bl.feSlits.center1[1]
tryt = (np.arctan(cfg['v_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1]
trxw_proj = trxw/bl.feSlits.center2[1]*bl.feSlits.center1[1]
tryt_proj = tryt/bl.feSlits.center2[1]*bl.feSlits.center1[1]
# trxw_proj = trxw/bl.feSlits.center2[1]*bl.feSlits.center1[1]
# tryt_proj = tryt/bl.feSlits.center2[1]*bl.feSlits.center1[1]
xcen = (trxr + trxw_proj) / 2
ycen = (tryb + tryt_proj) / 2
xgap = trxw_proj - trxr
ygap = tryt_proj - tryb
# xcen = (trxr + trxw) / 2
# ycen = (tryb + tryt) / 2
xgap = trxw - trxr
ygap = tryt - tryb
pos['sldi_gapx'] = {'value': xgap}
pos['sldi_gapy'] = {'value': ygap}
## Collimating Mirror
obj_dist = bl.cm.center[1] # object distance
beam_vs = 2 * obj_dist * np.tan(cfg['v_acc']) # vertical size of beam after CM
# TRX
try:
@@ -52,17 +53,23 @@ def calc_positions(cfg):
## Monochromator
# Bragg Angle
# TODO Should the bragg angle be corrected for the symmetric bragg case?
# See raytracing script or here: bragg = np.asin(rm.ch / (2.*cfg['dSpacing']*cfg['energyCCM'])) - aCrystal.get_dtheta_symmetric_Bragg(cfg['energyCCM'])
# if cfg['mo1_mode'] == 'Monochromatic':
# # Add 2x CM pitch to the bragg angle
# bragg = ((2 * cfg['cm_pitch']) + cfg['mo1_bragg']) / np.pi * 180
# elif cfg['mo1_mode'] == 'Pinkbeam':
# # Align xtal surfaces parallel to beam
# bragg = (2 * cfg['cm_pitch']) / np.pi * 180
# else:
# raise Exception('Monochromator mode not supported')
if cfg['mo1_mode'] == 'Monochromatic':
# Add 2x CM pitch to the bragg angle
bragg = ((2 * cfg['cm_pitch']) + cfg['mo1_bragg']) / np.pi * 180
bragg = cfg['mo1_bragg']
elif cfg['mo1_mode'] == 'Pinkbeam':
# Align xtal surfaces parallel to beam
bragg = (2 * cfg['cm_pitch']) / np.pi * 180
bragg = 0
else:
raise Exception('Monochromator mode not supported')
pos['mo1_bragg_angle'] = {'value': bragg} # Bragg angle in deg
pos['mo1_bragg_angle'] = {'value': bragg/np.pi*180} # Bragg angle in deg
# TRY, Height
l = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg'])
@@ -124,13 +131,14 @@ def calc_positions(cfg):
#TODO move to mono, calc for beam Z-movement between crystal surfaces
diag = bl.mo1.xtalGap[0] / np.sin(bragg) # Calculations for Mono
dz = diag * np.cos(2 * (cfg['cm_pitch'] + bragg))
diag = bl.mo1.xtalGap[0] / np.sin(cfg['mo1_bragg']) # Calculations for Mono
dz = diag * np.cos(2 * (cfg['cm_pitch'] + cfg['mo1_bragg']))
## Slits 1
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
sl1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM
pos['sl1_centery'] = {'value': sl1_beam_height}
pos['sl1_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 1
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
@@ -201,13 +209,14 @@ def calc_positions(cfg):
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
pos['sl2_centery'] = {'value': sl2_beam_height}
pos['sl2_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 2
d = bl.opBM2.center[1] - bl.fm.center[1]
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
pos['bm2_try'] = {'value': bm2_beam_height}
## Optical Table / Exit Window
## Optical Table
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
@@ -224,4 +233,7 @@ def calc_positions(cfg):
ot_es1_trz = cfg['smpl']
pos['ot_es1_trz'] = {'value': ot_es1_trz}
# ES0 exit window
pos['es0wi_try'] = {'value': 5} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
return pos
@@ -3,6 +3,8 @@ import re
import numpy as np
from bec_lib import bec_logger
logger = bec_logger.logger
os.environ["USE_XRT"] = "False"
import debye_bec.bec_widgets.widgets.x01da_parameters as bl
@@ -60,7 +62,6 @@ def calc_surfaces(cfg):
out['mo1_2']['y'] = []
# Focusing mirror
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
surface = bl.fm.surfaceToroid
stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip()
@@ -88,6 +89,12 @@ def calc_surfaces(cfg):
x = [off-widthBeam/2, off-widthBeam/2]
y = [l/2-z/2, -l/2-z/2]
# logger.info(f'stripe: {cfg["fm_stripe"]}')
# logger.info(f'fm_pitch: {cfg["fm_pitch"]}')
# logger.info(f'h: {h}')
# logger.info(f'z: {z}')
# logger.info(f'r: {r}')
res = 20
xElipse = np.linspace(0, np.pi, res)
yElipse = np.linspace(0, np.pi, res)
@@ -10,8 +10,12 @@ logger = bec_logger.logger
def sldi_gap_to_acc(sldi_gapx, sldi_gapy):
d1 = bl.feSlits.center1[1]
h_acc = np.tan(sldi_gapx / (2 * d1))
v_acc = np.tan(sldi_gapy / (2 * d1))
d2 = bl.feSlits.center2[1]
h_acc = np.tan(sldi_gapx / (d2 + d1))
v_acc = np.tan(sldi_gapy / (d2 + d1))
# h_acc = np.tan(sldi_gapx / (2 * d1))
# v_acc = np.tan(sldi_gapy / (2 * d1))
return h_acc, v_acc
def cm_trx_to_stripe(cm_trx):
@@ -35,8 +35,9 @@ from debye_bec.bec_widgets.widgets.qt_widgets import (
ComboBox,
Group,
NumberIndicator,
Button,
)
from debye_bec.bec_widgets.widgets.digital_twin.move_widget import MoveWidget
from debye_bec.bec_widgets.widgets.digital_twin.move_widget import MoveWidget, AbsorberWidget
from debye_bec.bec_widgets.widgets.digital_twin.calc_positions import calc_positions
from debye_bec.bec_widgets.widgets.digital_twin.calc_sideview import calc_sideview
from debye_bec.bec_widgets.widgets.digital_twin.calc_surfaces import calc_surfaces
@@ -55,6 +56,7 @@ from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import (
wall_geometries,
pipe_geometries,
)
from debye_bec.devices.absorber import STATUS as ABS_STATUS
logger = bec_logger.logger
@@ -106,6 +108,8 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_focy.value_changed_connect(self.calc_assistant)
self.input.smpl.value_changed_connect(self.calc_assistant)
self.input.adapt_reality.clicked_connect(self.adapt_reality)
self.bragg_angle = 0
self.qy = 0
@@ -128,6 +132,7 @@ class DigitalTwin(BECWidget, QWidget):
identifier = kwargs['identifier']
match identifier:
case 'init':
self.update_mo1_mode()
self.calc_mo1_bragg_angle()
self.calc_cm_crit_pitch()
self.calc_cm_reflectivity()
@@ -215,11 +220,11 @@ class DigitalTwin(BECWidget, QWidget):
sldi_gapx = self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value']
sldi_gapy = self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value']
h_acc, v_acc = sldi_gap_to_acc(sldi_gapx, sldi_gapy)
cm_trx = -self.dev.cm_trx.read(cached=True)['cm_trx']['value']
cm_stripe = cm_trx_to_stripe(cm_trx)
cm_trx = self.dev.cm_trx.read(cached=True)['cm_trx']['value']
cm_stripe = cm_trx_to_stripe(-cm_trx)
cm_pitch = self.dev.cm_rotx.read(cached=True)['cm_rotx']['value']
fm_trx = -self.dev.fm_trx.read(cached=True)['fm_trx']['value']
fm_stripe = fm_trx_to_stripe(fm_trx)
fm_trx = self.dev.fm_trx.read(cached=True)['fm_trx']['value']
fm_stripe = fm_trx_to_stripe(-fm_trx)
fm_pitch = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value']
fm_pitch_real = 2 * cm_pitch - fm_pitch
smpl = self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value']
@@ -229,17 +234,31 @@ class DigitalTwin(BECWidget, QWidget):
'v_acc' : v_acc,
'cm_pitch' : -cm_pitch * 1e-3,
'cm_stripe' : cm_stripe,
'cm_trx' : cm_trx,
'cm_trx' : -cm_trx,
'mo1_mode' : mo1_mode,
'mo1_xtal' : mo1_bragg['mo1_bragg_crystal_current_xtal_string']['value'],
'mo1_bragg' : mo1_bragg['mo1_bragg_angle']['value']/180*np.pi,
'fm_pitch' : -fm_pitch_real * 1e-3,
'fm_stripe' : fm_stripe,
'fm_trx' : fm_trx,
'fm_trx' : -fm_trx,
'fm_gain_height' : 1,
'smpl' : smpl,
}
# logger.info(f'Config created: {config}')
abs_open = self.dev.abs.read(cached=True)['abs_status_string']['value'] == 'OPEN'
if not abs_open:
ready = True
for mover in self.mover.mover_widgets:
if mover.status in ('moving', 'error'):
ready = False
if ready:
self.mover.abs.enable_open(1) # Enable open button
else:
self.mover.abs.enable_open(0) # Disable open button
else:
self.mover.abs.enable_open(0) # Disable open button
self.mover.sldi_gapx.set_feedback(sldi_gapx)
self.mover.sldi_gapy.set_feedback(sldi_gapy)
self.mover.cm_trx.set_feedback(cm_trx)
@@ -250,17 +269,57 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.mo1_trx.set_feedback(mo1_trx)
self.mover.mo1_try.set_feedback(self.dev.mo1_try.read(cached=True)['mo1_try']['value'])
self.mover.sl1_centery.set_feedback(self.dev.sl1_centery.read(cached=True)['sl1_centery']['value'])
self.mover.sl1_gapy.set_feedback(self.dev.sl1_gapy.read(cached=True)['sl1_gapy']['value'])
self.mover.bm1_try.set_feedback(self.dev.bm1_try.read(cached=True)['bm1_try']['value'])
self.mover.fm_trx.set_feedback(fm_trx)
self.mover.fm_try.set_feedback(self.dev.fm_try.read(cached=True)['fm_try']['value'])
self.mover.fm_bnd.set_feedback(self.dev.fm_bnd_radius.read(cached=True)['fm_bnd_radius']['value'])
self.mover.fm_rotx.set_feedback(fm_pitch)
self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)['fm_roty']['value'])
self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)['fm_rotz']['value'])
self.mover.sl2_centery.set_feedback(self.dev.sl2_centery.read(cached=True)['sl2_centery']['value'])
self.mover.sl2_gapy.set_feedback(self.dev.sl2_gapy.read(cached=True)['sl2_gapy']['value'])
self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)['bm2_try']['value'])
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)['ot_try']['value'])
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)['ot_rotx']['value'])
self.mover.ot_es1_trz.set_feedback(smpl)
self.mover.es0wi_try.set_feedback(self.dev.es0wi_try.read(cached=True)['es0wi_try']['value'])
self.mover.abs.set_feedback(abs_open)
return config
def adapt_reality(self, *args):
self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)['mo1_bragg']['value'])
h_acc, v_acc = sldi_gap_to_acc(
self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value'],
self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value']
)
self.input.sldi_hacc.set_number(h_acc*1e3)
self.input.sldi_vacc.set_number(v_acc*1e3)
self.input.cm_stripe.set_current_text(
cm_trx_to_stripe(-self.dev.cm_trx.read(cached=True)['cm_trx']['value'])
)
cm_pitch = self.dev.cm_rotx.read(cached=True)['cm_rotx']['value']
self.input.cm_pitch.set_number(cm_pitch)
mo1_trx = self.dev.mo1_trx.read(cached=True)['mo1_trx']['value']
if abs(mo1_trx) > 5:
mo1_mode = 'Monochromatic'
else:
mo1_mode = 'Pinkbeam'
self.input.mo1_mode.set_current_text(mo1_mode)
self.input.mo1_xtal.set_current_text(
self.dev.mo1_bragg.read(cached=True)['mo1_bragg_crystal_current_xtal_string']['value']
)
self.input.fm_stripe.set_current_text(
fm_trx_to_stripe(-self.dev.fm_trx.read(cached=True)['fm_trx']['value'])
)
self.input.fm_focus.set_current_text('Manual')
fm_pitch = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value']
fm_pitch_real = 2 * cm_pitch - fm_pitch
self.input.fm_pitch.set_number(fm_pitch_real)
self.input.smpl.set_number(
self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value']
)
self.calc_assistant(identifier='init')
def update_fm_mode(self):
fm_focus = self.input.fm_focus.currentText()
@@ -288,6 +347,7 @@ class DigitalTwin(BECWidget, QWidget):
beam = calc_sideview(config)
data = {'x': beam['x'], 'y': beam['y']}
self.sideview_plot.update_curves('reality', data)
# logger.info('Calc reality surfaces')
surfaces = calc_surfaces(config)
self.surface_plots.update_surfaces(scene='reality', data=surfaces)
@@ -329,6 +389,7 @@ class DigitalTwin(BECWidget, QWidget):
self.sideview_plot.update_curves('assistant', data)
def calc_assistant_surfaces(self):
# logger.info('Calc assistant surfaces')
surfaces = calc_surfaces(self.get_assistant_config())
self.surface_plots.update_surfaces(scene='assistant', data=surfaces)
@@ -365,16 +426,19 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.mo1_trx.set_target(out['mo1_trx']['value'])
self.mover.mo1_try.set_target(out['mo1_try']['value'])
self.mover.sl1_centery.set_target(out['sl1_centery']['value'])
self.mover.sl1_gapy.set_target(out['sl1_gapy']['value'])
self.mover.bm1_try.set_target(out['bm1_try']['value'])
self.mover.fm_trx.set_target(out['fm_trx']['value'])
self.mover.fm_try.set_target(out['fm_try']['value'])
self.mover.fm_bnd.set_target(out['fm_bnd_radius']['value'])
self.mover.fm_rotx.set_target(out['fm_rotx']['value'])
self.mover.sl2_centery.set_target(out['sl2_centery']['value'])
self.mover.sl2_gapy.set_target(out['sl2_gapy']['value'])
self.mover.bm2_try.set_target(out['bm2_try']['value'])
self.mover.ot_try.set_target(out['ot_try']['value'])
self.mover.ot_rotx.set_target(out['ot_rotx']['value'])
self.mover.ot_es1_trz.set_target(out['ot_es1_trz']['value'])
self.mover.es0wi_try.set_target(out['es0wi_try']['value'])
def calc_mo1_bragg_angle(self):
"""
@@ -392,7 +456,7 @@ class DigitalTwin(BECWidget, QWidget):
energy = self.input.energy.value()
theta, theta_cor = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch)
self.bragg_angle = theta
self.input.mo1_bragg_angle.setValue(theta_cor / np.pi * 180)
self.input.mo1_bragg_angle.setValue(theta / np.pi * 180)
def update_mo1_mode(self):
if self.input.mo1_mode.currentText() in 'Monochromatic':
@@ -429,12 +493,15 @@ class InputPanel(QWidget):
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# Adapt to reality
self.adapt_reality = Button(label_button='Adapt to reality', enabled=True)
# Energy
self.energy = InputNumberField('energy', 'Energy', unit='eV', init=8979, decimals=0, single_step=100, ll=4000, hl=65000)
# FE Slits Acceptance
self.sldi_hacc = InputNumberField('h_acc', 'Horizontal', unit='mrad', prefix='±', init=0.25, decimals=2, single_step=0.01, ll=-0.1, hl=0.9)
self.sldi_vacc = InputNumberField('v_acc', 'Vertical', unit='mrad', prefix='±', init=0.1, decimals=2, single_step=0.01, ll=-0.1, hl=0.5)
self.sldi_hacc = InputNumberField('h_acc', 'Horizontal', unit='mrad', prefix='±', init=0.25, decimals=3, single_step=0.01, ll=-0.1, hl=0.9)
self.sldi_vacc = InputNumberField('v_acc', 'Vertical', unit='mrad', prefix='±', init=0.1, decimals=3, single_step=0.01, ll=-0.1, hl=0.5)
self.sldi_ass_group = Group(
'FE Slits Acceptance',
[
@@ -506,6 +573,7 @@ class InputPanel(QWidget):
self.input_group = Group(
'User Input',
[
self.adapt_reality,
self.energy,
self.sldi_ass_group,
self.cm_ass_group,
@@ -657,15 +725,12 @@ class MoverPanel(QWidget):
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self.mover_widgets = []
self.dev = dev
# FE Slits
mot = self.dev.sldi_gapx
self.sldi_gapx = MoveWidget(motor=mot, label='GAPX', unit='mm', decimals=2, deadband=0.01)
self.sldi_gapx = MoveWidget(dev=dev, motor='sldi_gapx', label='GAPX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.sldi_gapx)
mot = self.dev.sldi_gapy
self.sldi_gapy = MoveWidget(motor=mot, label='GAPY', unit='mm', decimals=2, deadband=0.01)
self.sldi_gapy = MoveWidget(dev=dev, motor='sldi_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.sldi_gapy)
self.sldi_mov_group = Group(
@@ -676,21 +741,27 @@ class MoverPanel(QWidget):
]
)
# Absorber
self.abs = AbsorberWidget(absorber=dev.abs, label='')
self.abs_group = Group(
'Absorber',
[
self.abs,
]
)
# Collimating mirror
mot = self.dev.cm_trx
self.cm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.cm_trx = MoveWidget(dev=dev, motor='cm_trx', label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.cm_trx)
mot = self.dev.cm_try
self.cm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.cm_try = MoveWidget(dev=dev, motor='cm_try', label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.cm_try)
mot = self.dev.cm_bnd
self.cm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2, deadband=0.2)
self.cm_bnd = MoveWidget(dev=dev, motor='cm_bnd', label='BENDER', unit='km', decimals=2, deadband=0.2)
self.mover_widgets.append(self.cm_bnd)
mot = self.dev.cm_rotx
self.cm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.cm_rotx = MoveWidget(dev=dev, motor='cm_rotx', label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.cm_rotx)
self.cm_mov_group = Group(
@@ -704,16 +775,13 @@ class MoverPanel(QWidget):
)
# Monochromator
mot = self.dev.mo1_bragg
self.mo1_bragg_angle = MoveWidget(motor=mot, label='Bragg Angle', unit='deg', decimals=3, deadband=0.01)
self.mo1_bragg_angle = MoveWidget(dev=dev, motor='mo1_bragg_angle', label='Bragg Angle', unit='deg', decimals=3, deadband=0.01)
self.mover_widgets.append(self.mo1_bragg_angle)
mot = self.dev.mo1_trx
self.mo1_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mo1_trx = MoveWidget(dev=dev, motor='mo1_trx', label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.mo1_trx)
mot = self.dev.mo1_try
self.mo1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mo1_try = MoveWidget(dev=dev, motor='mo1_try', label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.mo1_try)
self.mo1_mov_group = Group(
@@ -726,20 +794,22 @@ class MoverPanel(QWidget):
)
# OP Slits 1
mot = self.dev.sl1_centery
self.sl1_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.sl1_centery = MoveWidget(dev=dev, motor='sl1_centery', label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl1_centery)
self.sl1_gapy = MoveWidget(dev=dev, motor='sl1_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl1_gapy)
self.sl1_mov_group = Group(
'OP Slits 1',
[
self.sl1_centery,
self.sl1_gapy,
]
)
# OP Beam Monitor 1
mot = self.dev.bm1_try
self.bm1_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.1)
self.bm1_try = MoveWidget(dev=dev, motor='bm1_try', label='TRY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.bm1_try)
self.bm1_mov_group = Group(
@@ -750,22 +820,24 @@ class MoverPanel(QWidget):
)
# Focusing Mirror
mot = self.dev.fm_trx
self.fm_trx = MoveWidget(motor=mot, label='TRX', unit='mm', decimals=2, deadband=0.01)
self.fm_trx = MoveWidget(dev=dev, motor='fm_trx', label='TRX', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.fm_trx)
mot = self.dev.fm_try
self.fm_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.01)
self.fm_try = MoveWidget(dev=dev, motor='fm_try', label='TRY', unit='mm', decimals=2, deadband=0.01)
self.mover_widgets.append(self.fm_try)
mot = self.dev.fm_bnd
self.fm_bnd = MoveWidget(motor=mot, label='BENDER', unit='km', decimals=2, deadband=0.2)
self.fm_bnd = MoveWidget(dev=dev, motor='fm_bnd', label='BENDER', unit='km', decimals=2, deadband=0.2)
self.mover_widgets.append(self.fm_bnd)
mot = self.dev.fm_rotx
self.fm_rotx = MoveWidget(motor=mot, label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.fm_rotx = MoveWidget(dev=dev, motor='fm_rotx', label='PITCH', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.fm_rotx)
self.fm_roty = MoveWidget(dev=dev, motor='fm_roty', label='YAW', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.fm_roty)
self.fm_rotz = MoveWidget(dev=dev, motor='fm_rotz', label='ROLL', unit='mrad', decimals=3, deadband=0.01)
self.mover_widgets.append(self.fm_rotz)
self.fm_mov_group = Group(
'Focusing Mirror',
[
@@ -773,24 +845,28 @@ class MoverPanel(QWidget):
self.fm_try,
self.fm_bnd,
self.fm_rotx,
self.fm_roty,
self.fm_rotz,
]
)
# OP Slits 2
mot = self.dev.sl2_centery
self.sl2_centery = MoveWidget(motor=mot, label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.sl2_centery = MoveWidget(dev=dev, motor='sl2_centery', label='CENTERY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl2_centery)
self.sl2_gapy = MoveWidget(dev=dev, motor='sl2_gapy', label='GAPY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.sl2_gapy)
self.sl2_mov_group = Group(
'OP Slits 2',
[
self.sl2_centery,
self.sl2_gapy,
]
)
# OP Beam Monitor 2
mot = self.dev.bm2_try
self.bm2_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.1)
self.bm2_try = MoveWidget(dev=dev, motor='bm2_try', label='TRY', unit='mm', decimals=2, deadband=0.1)
self.mover_widgets.append(self.bm2_try)
self.bm2_mov_group = Group(
@@ -801,23 +877,38 @@ class MoverPanel(QWidget):
)
# Optical Table
mot = self.dev.ot_try
self.ot_try = MoveWidget(motor=mot, label='TRY', unit='mm', decimals=2, deadband=0.2)
self.ot_try = MoveWidget(dev=dev, motor='ot_try', label='TRY', unit='mm', decimals=2, deadband=0.2)
self.mover_widgets.append(self.ot_try)
mot = self.dev.ot_rotx
self.ot_rotx = MoveWidget(motor=mot, label='ROTX', unit='mrad', decimals=3, deadband=0.05)
self.ot_rotx = MoveWidget(dev=dev, motor='ot_rotx', label='ROTX', unit='mrad', decimals=3, deadband=0.05)
self.mover_widgets.append(self.ot_rotx)
mot = self.dev.ot_es1_trz
self.ot_es1_trz = MoveWidget(motor=mot, label='ES1 TRZ', unit='mm', decimals=0, deadband=5)
self.mover_widgets.append(self.ot_es1_trz)
self.ot_mov_group = Group(
'Optical Table',
[
self.ot_try,
self.ot_rotx,
]
)
# Experimental Station 0
self.es0wi_try = MoveWidget(dev=dev, motor='es0wi_try', label='ES0 WI', unit='mm', decimals=0, deadband=0.1)
self.mover_widgets.append(self.es0wi_try)
self.es0_mov_group = Group(
'Expperimental Station 0',
[
self.es0wi_try,
]
)
# Experimental Station 1
self.ot_es1_trz = MoveWidget(dev=dev, motor='ot_es1_trz', label='ES1 TRZ', unit='mm', decimals=0, deadband=5)
self.mover_widgets.append(self.ot_es1_trz)
self.es1_mov_group = Group(
'Expperimental Station 1',
[
self.ot_es1_trz,
]
)
@@ -827,6 +918,7 @@ class MoverPanel(QWidget):
'Mover',
[
self.sldi_mov_group,
self.abs_group,
self.cm_mov_group,
self.mo1_mov_group,
self.sl1_mov_group,
@@ -835,6 +927,8 @@ class MoverPanel(QWidget):
self.sl2_mov_group,
self.bm2_mov_group,
self.ot_mov_group,
self.es0_mov_group,
self.es1_mov_group,
]
)
@@ -868,10 +962,10 @@ class SurfacePlots(QWidget):
}
self.plots = {
'cm': {},
'mo1_1': {},
'mo1_2': {},
'fm': {},
'mo1_2': {},
'mo1_1': {},
'cm': {},
}
self.color_impenetrable = (0, 0, 0)
@@ -961,7 +1055,7 @@ class SurfacePlots(QWidget):
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1, style=Qt.DashLine)
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0)
self.plots[name][scene].setPen(pen)
self.plots[name][scene].setBrush(brush)
@@ -1020,6 +1114,7 @@ class SideviewPlot(QWidget):
self.plot_widget = pg.PlotWidget()
self.plot_widget.getAxis('bottom').enableAutoSIPrefix(False)
self.plot_widget.invertX(True)
self.plot_widget.addLegend()
self.color_impenetrable = (0, 0, 0)
@@ -1037,15 +1132,18 @@ class SideviewPlot(QWidget):
for idx, scene in enumerate(self.data.keys()):
if scene in "assistant":
pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DashLine)
pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DotLine)
z_value = 2
else:
pen = pg.mkPen(color=self.colors[idx], width=2)
z_value = 1
self.plots[scene] = self.plot_widget.plot(
[],
[],
pen=pen,
name=scene,
)
self.plots[scene].setZValue(z_value)
self.plot_group = Group(
'Side View',
@@ -1098,7 +1196,7 @@ class SideviewPlot(QWidget):
for idx, scene in enumerate(self.data):
if scene in 'assistant':
brush = QBrush(QColor(*self.colors[idx], 255), Qt.DiagCrossPattern)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.DashLine)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.DotLine)
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3)
@@ -1141,7 +1239,7 @@ if __name__ == "__main__":
from bec_widgets.utils.colors import apply_theme
app = QApplication(sys.argv)
apply_theme("dark")
apply_theme("light")
dispatcher = BECDispatcher(gui_id="digital_twin")
win = DigitalTwin()
win.show()
@@ -7,6 +7,8 @@ from bec_qthemes import material_icon
from bec_widgets.utils.colors import get_accent_colors
from bec_lib import bec_logger
from debye_bec.devices.absorber import STATUS as ABS_STATUS
from qtpy.QtCore import Qt, QThread, Signal, QObject, Property, QPropertyAnimation
from qtpy.QtWidgets import (
QGroupBox, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
@@ -85,17 +87,17 @@ class StatusIcon(QWidget):
class MotionWorker(QObject):
"""
Simulates moving a stage from current_pos to target_pos.
Emits position_changed and finished signals.
Executes motion on the specified motor and includes some safety during
motion for certain motors.
"""
position_changed = Signal(float)
error = Signal(bool) # True = error
finished = Signal(bool) # True = reached target, False = stopped
def __init__(self, motor, target_pos: float):
def __init__(self, dev, motor, target_pos: float):
super().__init__()
self.dev = dev
self.motor = motor
self.name = motor.dotted_name
self._target = target_pos
self._stop_flag = threading.Event()
@@ -103,7 +105,7 @@ class MotionWorker(QObject):
self._stop_flag.set()
def run(self):
logger.info(f'Would run motor {self.name}')
logger.info(f'Would run motor {self.motor}')
simulated_run_time = 3
start = time.time()
while (time.time() - start) < simulated_run_time:
@@ -119,22 +121,139 @@ class MotionWorker(QObject):
# time.sleep(0.1)
self.finished.emit(True)
def run2(self):
match self.name:
case 'sldi_gapx' | 'sldi_gapy' | 'sldi_centerx' | 'sldi_centery':
self.motion()
case 'cm_trx':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['cm_roty'], 'abs_tol': 0.05}
])
case 'cm_roty':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['cm_trx'], 'abs_tol': 0.05}
])
case 'cm_try':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['cm_rotx'], 'abs_tol': 0.05},
{'device': self.dev['cm_rotz'], 'abs_tol': 0.05},
])
case 'cm_rotx':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['cm_try'], 'abs_tol': 0.05},
{'device': self.dev['cm_rotz'], 'abs_tol': 0.05},
])
case 'cm_rotz':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['cm_try'], 'abs_tol': 0.05},
{'device': self.dev['cm_rotx'], 'abs_tol': 0.05},
])
case 'cm_bnd':
p1 = (1/(self.dev.cm_bnd_radius.read['cm_bnd_radius']['value']*1e3) + 0.0284)/2e-6
p2 = (1/(self._target*1e3) + 0.0284)/2e-6
self._target = p2 - p1
self.motion(relative=True, rb=
{'device': self.dev['cm_bnd_radius']}
)
case 'mo1_try' | 'mo1_trx' | 'mo1_roty':
self.motion(abs_closed=True)
case 'mo1_bragg_angle':
self.motion()
case 'sl1_centery' | 'sl1_gapy' | 'bm1_try':
self.motion()
case 'fm_trx':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['fm_roty'], 'abs_tol': 0.05}
])
case 'fm_roty':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['fm_trx'], 'abs_tol': 0.05}
])
case 'fm_try':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['fm_rotx'], 'abs_tol': 0.05},
{'device': self.dev['fm_rotz'], 'abs_tol': 0.05},
])
case 'fm_rotx':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['fm_try'], 'abs_tol': 0.05},
{'device': self.dev['fm_rotz'], 'abs_tol': 0.05},
])
case 'fm_rotz':
self.motion(abs_closed=True, surveyed_axes=[
{'device': self.dev['fm_try'], 'abs_tol': 0.05},
{'device': self.dev['fm_rotx'], 'abs_tol': 0.05},
])
case 'fm_bnd':
p1 = (1/(self.dev.fm_bnd_radius.read['fm_bnd_radius']['value']*1e3) + 4.28e-5)/1.84e-9
p2 = (1/(self._target*1e3) + 4.28e-5)/1.84e-9
self._target = p2 - p1
self.motion(relative=True, rb=
{'device': self.dev['fm_bnd_radius']}
)
case 'sl2_centery' | 'sl2_gapy' | 'bm2_try':
self.motion()
case 'ot_try' | 'ot_rotx' | 'ot_es1_trz':
self.motion()
case _:
logger.warning(f'Motor {self.motor} not integrated in digital twin!')
def motion(self, abs_closed=False, relative=False, rb=None, surveyed_axes = None):
"""
Moves an axis while surverying a set of axes (if set).
Example surveyed_axes:
[{'device': bec_device_object, 'abs_tol': 0.1},]
Args:
surveyed_axes (list): List of dictionaries of devices
"""
if abs_closed:
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
status = self.dev.abs.close()
# TODO Set timeout to 0.001 and check if it actually raises (it should not start motion).
# Check of behavior of digital twin afterwards.
status.wait(timeout=5)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
surv_ax['name'] = surv_ax['device'].dotted_name
surv_ax['old_value'] = surv_ax['device'].read()[surv_ax['name']]['value']
if rb is not None:
rb['name'] = rb['device'].dotted_name
self.dev[self.motor].move(self._target, relative=relative)
while self.dev[self.motor].motor_is_moving.get():
if self._stop_flag.is_set():
self.dev[self.motor].motor_stop()
if rb is not None:
self.position_changed.emit(rb['device'].read()[rb['name']]['value'])
else:
self.position_changed.emit(self.dev[self.motor].read[self.motor]['value'])
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
fb = surv_ax['device'].read()[surv_ax['name']]['value']
if abs(fb - surv_ax['old_value']) > surv_ax['abs_tol']:
self.dev[self.motor].motor_stop()
self.error.emit(1)
break
time.sleep(0.1)
self.finished.emit(True)
class MoveWidget(QWidget):
"""
One motor stage control group containing:
- Value spinbox (target position)
- Target label (target position)
- Feedback label (current position)
- Status icon (qtawesome)
- Status icon (bec_qthemes)
- Start / Stop button
"""
def __init__(self, motor, label: str = '', unit=None, decimals=3, deadband=0.0):
def __init__(self, dev, motor, label: str = '', unit=None, decimals=3, deadband=0.0):
super().__init__()
self.fb = 0.0
self.target = 0
self.dev = dev
self.motor = motor
self.deadband = deadband
self._status = Status.IN_POSITION
self.status = Status.IN_POSITION
self._thread: QThread | None = None
self._worker: MotionWorker | None = None
@@ -143,8 +262,6 @@ class MoveWidget(QWidget):
self.unit = unit
self.decimals = decimals
# self._set_status(Status.IN_POSITION)
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
@@ -206,7 +323,7 @@ class MoveWidget(QWidget):
self._on_target_or_fb_changed()
def set_feedback(self, fb):
if self._status != Status.MOVING:
if self.status != Status.MOVING:
self.fb = fb
text = f'{fb:.{int(self.decimals)}f}'
if self.unit is not None:
@@ -214,9 +331,6 @@ class MoveWidget(QWidget):
self.fb_label.setText(text)
self._on_target_or_fb_changed()
# ------------------------------------------------------------------
# Button style helpers
# ------------------------------------------------------------------
def _apply_button_style(self, mode: str):
if mode == "start":
self.btn_action.setText("▶ Move")
@@ -229,19 +343,13 @@ class MoveWidget(QWidget):
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
)
# ------------------------------------------------------------------
# Status management
# ------------------------------------------------------------------
def _set_status(self, status: str):
self._status = status
self.status = status
self.status_icon.set_status(status)
# ------------------------------------------------------------------
# Motion control
# ------------------------------------------------------------------
def _on_target_or_fb_changed(self):
"""Re-evaluate in-position status whenever the target value changes."""
if self._status in (Status.ERROR, Status.MOVING):
if self.status in (Status.ERROR, Status.MOVING):
return
if abs(self.fb - self.target) <= self.deadband:
self._set_status(Status.IN_POSITION)
@@ -263,11 +371,10 @@ class MoveWidget(QWidget):
self._set_status(Status.MOVING)
self._apply_button_style("stop")
self._worker = MotionWorker(self.motor, target)
self._worker = MotionWorker(self.dev, self.motor, target)
self._thread = QThread()
self._worker.moveToThread(self._thread)
# Wire signals
self._thread.started.connect(self._worker.run)
self._worker.position_changed.connect(self._on_position_changed)
self._worker.error.connect(self._on_error)
@@ -285,7 +392,6 @@ class MoveWidget(QWidget):
def _stop_motion(self):
if self._worker:
self._worker.stop()
# UI will update via finished signal
def _on_position_changed(self, pos: float):
self.fb = pos
@@ -296,10 +402,11 @@ class MoveWidget(QWidget):
def _on_motion_finished(self, reached: bool):
target = self.target
if abs(self.fb - target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
if self.status not in Status.ERROR:
if abs(self.fb - target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
self._apply_button_style("start")
def _cleanup_thread(self):
@@ -310,12 +417,82 @@ class MoveWidget(QWidget):
self._worker.deleteLater()
self._worker = None
# ------------------------------------------------------------------
# Called on application close — stop motion safely
# ------------------------------------------------------------------
def shutdown(self):
if self._worker:
self._worker.stop()
if self._thread:
self._thread.quit()
self._thread.wait(2000) # max 2 s grace period
class AbsorberWidget(QWidget):
"""
Control of the frontend absorber (only open)
"""
def __init__(self, absorber, label: str = 'Absorber'):
super().__init__()
self.absorber = absorber
self.fb = False
self.text_color = (0, 0, 0)
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
# Name
self.label = QLabel(label)
self.label.setFixedWidth(100)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
# Blank
self.blank_label = QLabel('')
self.blank_label.setFixedWidth(100)
layout.addWidget(self.blank_label)
# Feedback
self.fb_label = QLabel('-')
self.fb_label.setFixedWidth(100)
layout.addWidget(self.fb_label)
# Blank icon
self.blank_icon = QLabel('')
self.blank_icon.setFixedWidth(30)
self.blank_icon.setContentsMargins(0, 0, 10, 0)
layout.addWidget(self.blank_icon)
# Open
self.btn_action = QPushButton("Open")
self.btn_action.setFixedWidth(90)
self.btn_action.setFixedHeight(20)
self.btn_action.clicked.connect(self._on_button_clicked)
layout.addWidget(self.btn_action)
def set_feedback(self, fb: bool):
self.fb = fb
if fb:
self.fb_label.setText('Open')
self.fb_label.setStyleSheet(
f"QLabel {{color: {get_accent_colors().success.name()}}}"
)
else:
self.fb_label.setText('Closed')
self.fb_label.setStyleSheet(
f"QLabel {{color: {get_accent_colors().emergency.name()}}}"
)
def enable_open(self, enable: bool = False):
if enable:
self.btn_action.setStyleSheet(
f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}"
)
self.btn_action.setEnabled(True)
else: # disabled
self.btn_action.setStyleSheet(
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
)
self.btn_action.setDisabled(True)
def _on_button_clicked(self):
self.absorber.open()
+33 -61
View File
@@ -8,6 +8,8 @@ from qtpy.QtWidgets import (
from qtpy.QtGui import QFont
from qtpy.QtCore import Qt
from bec_widgets.utils.colors import get_accent_colors
class Group(QGroupBox):
def __init__(self, label, widgets):
super().__init__(label)
@@ -128,70 +130,40 @@ class ComboBox(QWidget):
def setDisabled(self, disable):
self.value.setDisabled(disable)
# class Mover(QWidget):
# def __init__(self, dev, egu, prec):
# super().__init__()
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.position = QLabel('-')
# self.position.setFixedWidth(150)
# layout.addWidget(self.position)
# self.led = QLabel()
# self.led.setFixedWidth(30)
# self.led.setStyleSheet("background-color: 0, 0, 0; border: 1px solid black;")
# layout.addWidget(self.led)
# self.start = QPushButton('Move')
# self.start.setStyleSheet("color: black; background-color: green;")
# self.start.setFixedWidth(80)
# self.stop = QPushButton('Stop')
# self.stop.setStyleSheet("color: black; background-color: firebrick;")
# self.stop.setFixedWidth(80)
# layout.addWidget(self.start)
# layout.addWidget(self.stop)
# self.dev = dev
# self.unit = egu
# self.decimals = prec
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)
if label is not None:
self.label = QLabel(label)
self.label.setFixedWidth(140)
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)
# def led_set_status(self, status):
# if status in 'out':
# self.led.setStyleSheet("background-color: 255, 0, 0; border: 1px solid black;")
# elif status in 'moving':
# self.led.setStyleSheet("background-color: 255, 255, 0; border: 1px solid black;")
# elif status in 'in':
# self.led.setStyleSheet("background-color: 0, 255, 0; border: 1px solid black;")
def clicked_connect(self, func):
"""Connect a function to the button press."""
self.button.clicked.connect(func)
# def position_setValue(self, number):
# text = f'{number:.{int(self.decimals)}f}'
# if self.unit is not None:
# text = text + ' ' + self.unit
# self.position.setText(text)
def enable_button(self, enable: bool = False):
if enable:
self.button.setStyleSheet(
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
)
self.button.setEnabled(True)
else: # disabled
self.button.setStyleSheet(
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
)
self.button.setDisabled(True)
# def start_clicked_connect(self, func):
# """Connect a function to the start button press."""
# self.start.clicked.connect(
# partial(func, dev=self.dev)
# )
# def stop_clicked_connect(self, func):
# """Connect a function to the stop button press."""
# self.stop.clicked.connect(
# partial(func, dev=self.dev)
# )
# def start_setEnabled(self, enable):
# self.start.setEnabled(enable)
# if enable:
# self.start.setStyleSheet("color: black; background-color: green;")
# else:
# self.start.setStyleSheet("color: black; background-color: grey;")
# def stop_setEnabled(self, enable):
# self.stop.setEnabled(enable)
# if enable:
# self.stop.setStyleSheet("color: black; background-color: firebrick;")
# else:
# self.stop.setStyleSheet("color: black; background-color: grey;")
def setText(self, text):
self.button.setText(text)
# class TextIndicator(QWidget):
# def __init__(self, label, unit=None, highlight=False):
@@ -33,15 +33,15 @@ mo1_bragg:
onFailure: retry
enabled: true
softwareTrigger: false
#mo1_bragg_angle:
# readoutPriority: baseline
# description: Positioner for the Monochromator
# deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg_angle.Mo1BraggAngle
# deviceConfig:
# prefix: "X01DA-OP-MO1:BRAGG:"
# onFailure: retry
# enabled: true
# softwareTrigger: false
mo1_bragg_angle:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg_angle.Mo1BraggAngle
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
enabled: true
softwareTrigger: false
## Remaining optics hutch
optics_config:
+2 -2
View File
@@ -41,8 +41,8 @@ class Absorber(PSIDeviceBase):
USER_ACCESS = ["open", "close"]
request = Cpt(EpicsSignal, suffix="REQUEST", kind="config", doc="Open/Close Absorber")
status = Cpt(EpicsSignalRO, suffix="STATUS", kind="config", doc="Absorber Status")
status_string = Cpt(EpicsSignalRO, suffix="STATUS", kind="config", string=True, doc="Absorber Status")
status = Cpt(EpicsSignalRO, suffix="STATUS", kind="normal", doc="Absorber Status")
status_string = Cpt(EpicsSignalRO, suffix="STATUS", kind="normal", string=True, 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)