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

This commit is contained in:
x01da
2026-05-13 13:29:09 +02:00
parent 5d862e1d5b
commit c04d829fc6
7 changed files with 301 additions and 94 deletions
@@ -155,15 +155,15 @@ def calc_positions(cfg):
# Bender radius
if cfg['fm_qy'] is None:
radius = 2 * q / np.sin(cfg['fm_pitch']) # ideal bending radius for focused beam
radius = 2 * q / np.sin(cfg['fm_rotx']) # ideal bending radius for focused beam
else:
radius = 2 * cfg['fm_qy'] / np.sin(cfg['fm_pitch']) # ideal bending radius for unfocused beam
radius = 2 * cfg['fm_qy'] / np.sin(cfg['fm_rotx']) # ideal bending radius for unfocused beam
pos['fm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km
# Pitch
d = bl.fm.center[1] - bl.cm.center[1] - dz
fm_pitch = 2 * cfg['cm_pitch'] - cfg['fm_pitch'] # calculate pitch in absolute values (according to horizontal plane)
pos['fm_rotx'] = {'value': -fm_pitch * 1e3} # invert and convert to mrad (same as EGU of rotx axis)
fm_rotx = 2 * cfg['cm_pitch'] - cfg['fm_rotx'] # calculate pitch in absolute values (according to horizontal plane)
pos['fm_rotx'] = {'value': -fm_rotx * 1e3} # invert and convert to mrad (same as EGU of rotx axis)
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
@@ -205,28 +205,31 @@ def calc_positions(cfg):
else:
raise Exception('FM Stripe selection not valid')
pos['fm_roty'] = {'value': 0}
pos['fm_rotz'] = {'value': 0}
## Slits 2
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
pos['sl2_centery'] = {'value': sl2_beam_height}
pos['sl2_gapy'] = {'value': beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 2
d = bl.opBM2.center[1] - bl.fm.center[1]
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
pos['bm2_try'] = {'value': bm2_beam_height}
## Optical Table
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
ot_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
ot_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx']))
# logger.info(fm_height)
# logger.info(d * np.tan((2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch'])))
# logger.info(d * np.tan((2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])))
pos['ot_try'] = {'value': ot_height}
# Pitch
ot_pitch = - (2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch'])
ot_pitch = - (2 * cfg['cm_pitch'] - 2 * cfg['fm_rotx'])
pos['ot_rotx'] = {'value': ot_pitch * 1e3}
# TRZ ES1
@@ -25,12 +25,12 @@ def calc_sideview(cfg):
beam['x'].append(bl.fm.center[1]) # FM
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy)
beam['x'].append(cfg['smpl']) # Experiment
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy+np.tan(2*(cfg['cm_pitch']-cfg['fm_pitch']))*(cfg['smpl']-bl.fm.center[1]))
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]-dz)+dy+np.tan(2*(cfg['cm_pitch']-cfg['fm_rotx']))*(cfg['smpl']-bl.fm.center[1]))
elif cfg['mo1_mode'] == 'Pinkbeam':
beam['x'].append(bl.fm.center[1]) # FM
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1]))
beam['x'].append(cfg['smpl']) # Experiment
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1])+np.tan(2*(cfg['cm_pitch']-cfg['fm_pitch']))*(cfg['smpl']-bl.fm.center[1]))
beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.fm.center[1]-bl.cm.center[1])+np.tan(2*(cfg['cm_pitch']-cfg['fm_rotx']))*(cfg['smpl']-bl.fm.center[1]))
dy_fm_ex = beam['y'][-1] - beam['y'][-2]
dz_fm_ex = beam['x'][-1] - beam['x'][-2]
@@ -81,16 +81,16 @@ def calc_surfaces(cfg):
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
l = heightBeam/np.sin(cfg['fm_pitch'])
l = heightBeam/np.sin(cfg['fm_rotx'])
alpha = np.arccos(1-widthBeam**2/(2*r**2))
h = r-(r*np.cos(alpha/2))
z = h/np.tan(cfg['fm_pitch'])
z = h/np.tan(cfg['fm_rotx'])
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'fm_rotx: {cfg["fm_rotx"]}')
# logger.info(f'h: {h}')
# logger.info(f'z: {z}')
# logger.info(f'r: {r}')
@@ -120,7 +120,7 @@ def calc_surfaces(cfg):
out['fm']['y'] = y
else: # flat surface, no toroid
l = heightBeam/np.sin(cfg['fm_pitch'])
l = heightBeam/np.sin(cfg['fm_rotx'])
w1 = 2 * (bl.fm.center[1]-l/2) * np.tan(cfg['h_acc'])
w2 = 2 * (bl.fm.center[1]+l/2) * np.tan(cfg['h_acc'])
@@ -4,6 +4,8 @@ Digital Twin: Custom BEC widget to support the beamline alignment.
import sys
import numpy as np
import yaml
from pathlib import Path
from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints
@@ -56,10 +58,13 @@ from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import (
wall_geometries,
pipe_geometries,
)
from debye_bec.bec_widgets.widgets.digital_twin.offset_settings import OffsetSettings
from debye_bec.devices.absorber import STATUS as ABS_STATUS
logger = bec_logger.logger
OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/x01da_offsets.yaml"
class DigitalTwin(BECWidget, QWidget):
"""
Main widget of Digital Twin
@@ -74,21 +79,28 @@ class DigitalTwin(BECWidget, QWidget):
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.input_widget = QWidget()
self.input_layout = QVBoxLayout(self.input_widget)
self.input = InputPanel()
self.settings = SettingsPanel()
self.input_layout.addWidget(self.input) # type: ignore
self.input_layout.addWidget(self.settings) # type: ignore
self.plot_widget = QWidget()
self.plot_layout = QVBoxLayout(self.plot_widget)
self.input = InputPanel()
self.sideview_plot = SideviewPlot()
self.surface_plots = SurfacePlots()
self.plot_layout.addWidget(self.sideview_plot) # type: ignore
self.plot_layout.addWidget(self.surface_plots) # type: ignore
self.positions = PositionsPanel()
self.mover = MoverPanel(self.dev)
self.root_layout.addWidget(self.input, stretch=1, alignment=Qt.AlignTop) # type: ignore
self.plot_layout.addWidget(self.sideview_plot) # type: ignore
self.plot_layout.addWidget(self.surface_plots) # type: ignore
self.root_layout.addWidget(self.plot_widget, stretch=1, alignment=Qt.AlignTop) # type: ignore
# self.root_layout.addWidget(self.positions, stretch=1, alignment=Qt.AlignTop) # type: ignore
self.root_layout.addWidget(self.mover, stretch=1, alignment=Qt.AlignTop)
self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignTop) # type: ignore
self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignTop) # type: ignore
# self.root_layout.addWidget(self.positions, alignment=Qt.AlignTop) # type: ignore
self.root_layout.addWidget(self.mover, alignment=Qt.AlignTop)
self.setLayout(self.root_layout)
self.setWindowTitle("Digital Twin")
@@ -103,17 +115,21 @@ class DigitalTwin(BECWidget, QWidget):
self.input.mo1_xtal.activated_connect(self.calc_assistant)
self.input.fm_stripe.activated_connect(self.calc_assistant)
self.input.fm_focus.activated_connect(self.calc_assistant)
self.input.fm_pitch.value_changed_connect(self.calc_assistant)
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)
self.input.adapt_reality.clicked_connect(self.adapt_reality)
self.settings.reload_offsets.clicked_connect(self.load_offsets)
self.settings.unload_offsets.clicked_connect(self.unload_offsets)
self.bragg_angle = 0
self.qy = 0
self.offsets = {}
# Initialize all values
self.load_offsets(recalculate=False)
self.calc_assistant(identifier='init')
# Timer: update plot every 1 second
@@ -181,13 +197,13 @@ class DigitalTwin(BECWidget, QWidget):
fm_focus = self.input.fm_focus.currentText()
if fm_focus in 'Manual':
fm_pitch = self.input.fm_pitch.value()
fm_rotx = self.input.fm_rotx.value()
fm_qy = None
elif fm_focus in 'Focused':
fm_pitch = self.input.fm_pitch_ideal.value()
fm_rotx = self.input.fm_rotx_ideal.value()
fm_qy = None
else: # Focused
fm_pitch = self.input.fm_pitch_ideal.value()
fm_rotx = self.input.fm_rotx_ideal.value()
fm_qy = self.qy
config = { # Config in SI units!
@@ -200,7 +216,7 @@ class DigitalTwin(BECWidget, QWidget):
'mo1_mode' : self.input.mo1_mode.currentText(),
'mo1_xtal' : self.input.mo1_xtal.currentText(),
'mo1_bragg' : self.bragg_angle,
'fm_pitch' : -fm_pitch * 1e-3,
'fm_rotx' : -fm_rotx * 1e-3,
'fm_stripe' : self.input.fm_stripe.currentText(),
'fm_trx' : None,
'fm_qy' : fm_qy,
@@ -225,8 +241,8 @@ class DigitalTwin(BECWidget, QWidget):
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_pitch = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value']
fm_pitch_real = 2 * cm_pitch - fm_pitch
fm_rotx = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value']
fm_rotx_real = 2 * cm_pitch - fm_rotx
smpl = self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value']
config = { # Config in SI units!
'energy' : mo1_bragg['mo1_bragg']['value'],
@@ -238,7 +254,7 @@ class DigitalTwin(BECWidget, QWidget):
'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_rotx' : -fm_rotx_real * 1e-3,
'fm_stripe' : fm_stripe,
'fm_trx' : -fm_trx,
'fm_gain_height' : 1,
@@ -253,11 +269,11 @@ class DigitalTwin(BECWidget, QWidget):
if mover.status in ('moving', 'error'):
ready = False
if ready:
self.mover.abs.enable_open(1) # Enable open button
self.mover.abs.enable_open(True) # Enable open button
else:
self.mover.abs.enable_open(0) # Disable open button
self.mover.abs.enable_open(False) # Disable open button
else:
self.mover.abs.enable_open(0) # Disable open button
self.mover.abs.enable_open(False) # Disable open button
self.mover.sldi_gapx.set_feedback(sldi_gapx)
self.mover.sldi_gapy.set_feedback(sldi_gapy)
@@ -274,7 +290,7 @@ class DigitalTwin(BECWidget, QWidget):
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_rotx.set_feedback(fm_rotx)
self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)['fm_roty']['value'])
self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)['fm_rotz']['value'])
self.mover.sl2_centery.set_feedback(self.dev.sl2_centery.read(cached=True)['sl2_centery']['value'])
@@ -288,20 +304,40 @@ class DigitalTwin(BECWidget, QWidget):
return config
def adapt_reality(self, *args):
pos = {}
pos['sldi_gapx'] = self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value']
pos['sldi_gapy'] = self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value']
pos['cm_trx'] = self.dev.cm_trx.read(cached=True)['cm_trx']['value']
pos['cm_rotx'] = self.dev.cm_rotx.read(cached=True)['cm_rotx']['value']
pos['mo1_trx'] = self.dev.mo1_trx.read(cached=True)['mo1_trx']['value']
pos['fm_trx'] = self.dev.fm_trx.read(cached=True)['fm_trx']['value']
pos['fm_rotx'] = self.dev.fm_rotx.read(cached=True)['fm_rotx']['value']
pos['ot_es1_trz'] = self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value']
# Removing offsets
for axis, value in pos.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
if 'modifier' in axis_offsets and 'offset' in axis_offsets:
for idx, rng in enumerate(axis_offsets['modifier']['range']):
if rng[0] < pos[axis_offsets['modifier']['axis']] < rng[1]:
pos[axis] -= axis_offsets['offset'][idx]
break
elif 'offset' in axis_offsets:
pos[axis] -= axis_offsets['offset']
self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)['mo1_bragg']['value'])
h_acc, v_acc = sldi_gap_to_acc(
self.dev.sldi_gapx.read(cached=True)['sldi_gapx']['value'],
self.dev.sldi_gapy.read(cached=True)['sldi_gapy']['value']
pos['sldi_gapx'],
pos['sldi_gapy']
)
self.input.sldi_hacc.set_number(h_acc*1e3)
self.input.sldi_vacc.set_number(v_acc*1e3)
self.input.cm_stripe.set_current_text(
cm_trx_to_stripe(-self.dev.cm_trx.read(cached=True)['cm_trx']['value'])
cm_trx_to_stripe(-pos['cm_trx'])
)
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:
self.input.cm_pitch.set_number(pos['cm_rotx'])
if abs(pos['mo1_trx']) > 5:
mo1_mode = 'Monochromatic'
else:
mo1_mode = 'Pinkbeam'
@@ -310,37 +346,56 @@ class DigitalTwin(BECWidget, QWidget):
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'])
fm_trx_to_stripe(-pos['fm_trx'])
)
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)
fm_rotx_real = 2 * pos['cm_rotx'] - pos['fm_rotx']
self.input.fm_rotx.set_number(fm_rotx_real)
self.input.smpl.set_number(
self.dev.ot_es1_trz.read(cached=True)['ot_es1_trz']['value']
pos['ot_es1_trz']
)
self.calc_assistant(identifier='init')
def load_offsets(self, recalculate=True, *args):
file = Path(OFFSET_FILE)
if not file.exists():
raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}")
with file.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
self.offsets = data
if recalculate:
self.calc_assistant(identifier='init')
def unload_offsets(self, *args):
self.offsets = {}
self.calc_assistant(identifier='init')
def update_fm_mode(self):
fm_focus = self.input.fm_focus.currentText()
if fm_focus in 'Manual':
self.input.fm_pitch.setVisible(True)
self.input.fm_pitch_ideal.setVisible(True)
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_pitch_ideal.setLabel('Incidence Angle for focused beam')
self.input.fm_rotx_ideal.setLabel('Incidence Angle for focused beam')
elif fm_focus in 'Focused':
self.input.fm_pitch.setVisible(False)
self.input.fm_pitch_ideal.setVisible(True)
self.input.fm_rotx.setVisible(False)
self.input.fm_rotx_ideal.setVisible(True)
self.input.fm_focx.setVisible(False)
self.input.fm_focy.setVisible(False)
self.input.fm_pitch_ideal.setLabel('Incidence Angle for focused beam')
self.input.fm_rotx_ideal.setLabel('Incidence Angle for focused beam')
else: # Defocused
self.input.fm_pitch.setVisible(False)
self.input.fm_pitch_ideal.setVisible(True)
self.input.fm_rotx.setVisible(False)
self.input.fm_rotx_ideal.setVisible(True)
self.input.fm_focx.setVisible(True)
self.input.fm_focy.setVisible(True)
self.input.fm_pitch_ideal.setLabel('Incidence Angle for defocused beam')
self.input.fm_rotx_ideal.setLabel('Incidence Angle for defocused beam')
def calc_reality(self):
config = self.get_reality_config()
@@ -369,13 +424,13 @@ class DigitalTwin(BECWidget, QWidget):
fm_stripe = self.input.fm_stripe.currentText()
fm_focus = self.input.fm_focus.currentText()
if fm_focus in 'Manual':
fm_pitch = -self.input.fm_pitch.value() * 1e-3
fm_rotx = -self.input.fm_rotx.value() * 1e-3
else:
fm_pitch = -self.input.fm_pitch_ideal.value() * 1e-3
fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3
energy = self.input.energy.value()
self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_pitch, energy))
self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, energy))
self.input.fm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV")
self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_pitch, 3*energy))
self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3*energy))
self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
def calc_cm_fm_harm_suppr(self):
@@ -395,6 +450,19 @@ class DigitalTwin(BECWidget, QWidget):
def calc_positions(self):
out = calc_positions(self.get_assistant_config())
# Apply offsets
for axis, axis_data in out.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
if 'modifier' in axis_offsets and 'offset' in axis_offsets:
for idx, rng in enumerate(axis_offsets['modifier']['range']):
if rng[0] < out[axis_offsets['modifier']['axis']]['value'] < rng[1]:
axis_data['value'] += axis_offsets['offset'][idx]
break
elif 'offset' in axis_offsets:
axis_data['value'] += axis_offsets['offset']
self.positions.sldi_gapx.setValue(out['sldi_gapx']['value'])
self.positions.sldi_gapy.setValue(out['sldi_gapy']['value'])
self.positions.cm_trx.setValue(out['cm_trx']['value'])
@@ -432,6 +500,8 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.fm_try.set_target(out['fm_try']['value'])
self.mover.fm_bnd.set_target(out['fm_bnd_radius']['value'])
self.mover.fm_rotx.set_target(out['fm_rotx']['value'])
self.mover.fm_roty.set_target(out['fm_roty']['value'])
self.mover.fm_rotz.set_target(out['fm_rotz']['value'])
self.mover.sl2_centery.set_target(out['sl2_centery']['value'])
self.mover.sl2_gapy.set_target(out['sl2_gapy']['value'])
self.mover.bm2_try.set_target(out['bm2_try']['value'])
@@ -476,9 +546,9 @@ class DigitalTwin(BECWidget, QWidget):
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
fm_focx = self.input.fm_focx.value()
fm_focy = self.input.fm_focy.value()
fm_pitch, qy = fm_ideal_pitch(fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy)
fm_rotx, qy = fm_ideal_pitch(fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy)
self.qy = qy
self.input.fm_pitch_ideal.setValue(-fm_pitch * 1e3)
self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3)
def calc_cm_crit_pitch(self):
cm_stripe = self.input.cm_stripe.currentText()
@@ -545,10 +615,10 @@ class InputPanel(QWidget):
# Focusing Mirror
self.fm_stripe = ComboBox('fm_stripe', 'Stripe', ['Rh (toroid)', 'Rh (flat)', 'Pt (toroid)', 'Pt (flat)'])
self.fm_focus = ComboBox('fm_focus', 'Focus Type', ['Manual', 'Focused', 'Defocused'])
self.fm_pitch = InputNumberField('fm_pitch', 'Incidence Angle', unit='mrad', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2)
self.fm_rotx = InputNumberField('fm_rotx', 'Incidence Angle', unit='mrad', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2)
self.fm_focx = InputNumberField('fm_focx', 'Beam Size Horizontal', unit='mm', init=1, decimals=1, single_step=0.1, ll=0, hl=30)
self.fm_focy = InputNumberField('fm_focy', 'Beam Size Vertical', unit='mm', init=1, decimals=1, single_step=0.1, ll=0, hl=10)
self.fm_pitch_ideal = NumberIndicator('Incidence Angle for focused beam', 'mrad', decimals=3)
self.fm_rotx_ideal = NumberIndicator('Incidence Angle for focused beam', 'mrad', decimals=3)
self.fm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.fm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.fm_ass_group = Group(
@@ -556,10 +626,10 @@ class InputPanel(QWidget):
[
self.fm_stripe,
self.fm_focus,
self.fm_pitch,
self.fm_rotx,
self.fm_focx,
self.fm_focy,
self.fm_pitch_ideal,
self.fm_rotx_ideal,
self.fm_refl,
self.fm_refl_harm,
]
@@ -587,6 +657,30 @@ class InputPanel(QWidget):
self._layout .addWidget(self.input_group)
self._layout .addStretch()
class SettingsPanel(QWidget):
"""Right-side control panel: input field, indicator, send, recording."""
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# Reload offsets
self.reload_offsets = Button(label='Reload Offsets', label_button='Reload', enabled=True)
self.unload_offsets = Button(label='Unload Offsets', label_button='Unload', enabled=True)
# Assemble complete offset group
self.offset_group = Group(
'Axes Offsets',
[
self.reload_offsets,
self.unload_offsets,
]
)
self._layout .addWidget(self.offset_group)
self._layout .addStretch()
class PositionsPanel(QWidget):
"""Right-side control panel: input field, indicator, send, recording."""
@@ -104,25 +104,25 @@ class MotionWorker(QObject):
def stop(self):
self._stop_flag.set()
# def run(self):
# logger.info(f'Would run motor {self.motor}')
# simulated_run_time = 3
# start = time.time()
# while (time.time() - start) < simulated_run_time:
# if self._stop_flag.is_set():
# break
# time.sleep(0.01)
# # self.motor.move(self._target, relative=False)
# # while self.motor.motor_is_moving.get():
# # if self._stop_flag.is_set():
# # self.motor.motor_stop()
# # self.position_changed.emit(self.motor.read[self.name]['value'])
# # time.sleep(0.1)
# self.finished.emit(True)
def run(self):
logger.info(f'Would run motor {self.motor}')
simulated_run_time = 3
start = time.time()
while (time.time() - start) < simulated_run_time:
if self._stop_flag.is_set():
break
time.sleep(0.01)
# self.motor.move(self._target, relative=False)
# while self.motor.motor_is_moving.get():
# if self._stop_flag.is_set():
# self.motor.motor_stop()
# self.position_changed.emit(self.motor.read[self.name]['value'])
# time.sleep(0.1)
self.finished.emit(True)
def run2(self):
match self.name:
match self.motor:
case 'sldi_gapx' | 'sldi_gapy' | 'sldi_centerx' | 'sldi_centery':
self.motion()
case 'cm_trx':
@@ -149,7 +149,7 @@ class MotionWorker(QObject):
{'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
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=
@@ -185,7 +185,7 @@ class MotionWorker(QObject):
{'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
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=
@@ -216,22 +216,24 @@ class MotionWorker(QObject):
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']
surv_ax['old_value'] = surv_ax['device'].read(cached=True)[surv_ax['name']]['value']
if rb is not None:
rb['name'] = rb['device'].dotted_name
self.dev[self.motor].move(self._target, relative=relative)
time.sleep(0.5)
while self.dev[self.motor].motor_is_moving.get():
if self._stop_flag.is_set():
self.dev[self.motor].motor_stop()
self.dev[self.motor].stop()
self._stop_flag.clear()
if rb is not None:
self.position_changed.emit(rb['device'].read()[rb['name']]['value'])
self.position_changed.emit(rb['device'].read(cached=True)[rb['name']]['value'])
else:
self.position_changed.emit(self.dev[self.motor].read[self.motor]['value'])
self.position_changed.emit(self.dev[self.motor].read(cached=True)[self.motor]['value'])
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
fb = surv_ax['device'].read()[surv_ax['name']]['value']
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].motor_stop()
self.dev[self.motor].stop()
self.error.emit(1)
break
time.sleep(0.1)
@@ -333,12 +335,12 @@ class MoveWidget(QWidget):
def _apply_button_style(self, mode: str):
if mode == "start":
self.btn_action.setText("▶ Move")
self.btn_action.setText("Move")
self.btn_action.setStyleSheet(
f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}"
)
else: # stop
self.btn_action.setText("■ Stop")
self.btn_action.setText("Stop")
self.btn_action.setStyleSheet(
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
)
@@ -0,0 +1,58 @@
import sys
import numpy as np
from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints
# pylint: disable=E0611
from qtpy.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QApplication,
QLayout,
QLabel,
QPushButton,
QDialog,
)
# pylint: disable=E0611
from qtpy.QtCore import (
Qt,
QTimer,
)
from qtpy.QtGui import (
QColor,
QBrush,
QCloseEvent,
)
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from debye_bec.bec_widgets.widgets.qt_widgets import (
InputNumberField,
ComboBox,
Group,
NumberIndicator,
Button,
)
logger = bec_logger.logger
class OffsetSettings(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Axes Offset Settings")
self.setMinimumSize(300, 150)
layout = QVBoxLayout()
label = QLabel("👋 Hello from the secondary window!")
label.setAlignment(Qt.AlignCenter)
close_btn = QPushButton("Close")
close_btn.clicked.connect(self.close)
layout.addWidget(label)
layout.addWidget(close_btn)
self.setLayout(layout)
@@ -0,0 +1,50 @@
cm_try:
offset: 0.15
mo1_trx:
modifier:
axis: mo1_trx
range: [[-30, -0.1], [0.1, 30]]
offset: [0, 2.21]
mo1_try:
modifier:
axis: mo1_trx
range: [[-30, -0.1], [0.1, 30]]
offset: [0, -1.6]
sl1_centery:
offset: -1.8
fm_trx:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.16]
fm_try:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.45]
fm_rotx:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, 0.063]
fm_roty:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.04]
sl2_centery:
offset: 1.2
ot_try:
offset: 0
ot_rotx:
offset: 0