From c04d829fc65cc776a6b94d0c81143189556313a1 Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 13 May 2026 13:29:09 +0200 Subject: [PATCH] wip: digital twin --- .../widgets/digital_twin/calc_positions.py | 21 +- .../widgets/digital_twin/calc_sideview.py | 4 +- .../widgets/digital_twin/calc_surfaces.py | 8 +- .../widgets/digital_twin/digital_twin.py | 196 +++++++++++++----- .../widgets/digital_twin/move_widget.py | 58 +++--- .../widgets/digital_twin/offset_settings.py | 58 ++++++ .../bec_widgets/widgets/x01da_offsets.yaml | 50 +++++ 7 files changed, 301 insertions(+), 94 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/offset_settings.py create mode 100644 debye_bec/bec_widgets/widgets/x01da_offsets.yaml diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py index 569145e..55a1cca 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py @@ -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 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py index 5a0f930..33487a2 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py @@ -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] diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py index 7162d45..013164b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py @@ -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']) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index 6337ca1..a8a5a4a 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -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.""" diff --git a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py index 27419af..5669385 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -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;}}" ) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/offset_settings.py b/debye_bec/bec_widgets/widgets/digital_twin/offset_settings.py new file mode 100644 index 0000000..337a091 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/offset_settings.py @@ -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) \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/x01da_offsets.yaml b/debye_bec/bec_widgets/widgets/x01da_offsets.yaml new file mode 100644 index 0000000..e1d40a0 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/x01da_offsets.yaml @@ -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 \ No newline at end of file