diff --git a/debye_bec/bec_ipython_client/plugins/move_to_label.py b/debye_bec/bec_ipython_client/plugins/move_to_label.py new file mode 100644 index 0000000..dd14970 --- /dev/null +++ b/debye_bec/bec_ipython_client/plugins/move_to_label.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import builtins +from typing import TYPE_CHECKING + +from bec_lib import bec_logger +from debye_bec.devices.absorber import STATUS as ABS_STATUS + +logger = bec_logger.logger +# import builtins to avoid linter errors +dev = builtins.__dict__.get("dev") + +class MoveToLabelError(Exception): + """Exception for the MoveToLabel function""" + +def move_to_label(): + """ + Function to move several motors to a specific position defined in the label dict. + """ + + label = get_device_conditions(label="digitalTwin") + + # Get absorber status and close if open + logger.info("Check Frontend Absorber Status") + abs_was_open = dev.abs.status.get() == ABS_STATUS.OPEN + if abs_was_open: + logger.info(" Close Frontend Absorber") + status = dev.abs.close() + status.wait() + + # Move Frontend Slits + logger.info("Move Frontend Slits into position") + devices = ["sldi_centerx", "sldi_centery", "sldi_gapx", "sldi_gapy"] + matches = {key: label[key] for key in devices if key in label} + statuses = [] + for device in matches.values(): + statuses.append(device['device'].move(device['value'])) + for status in statuses: + status.wait(timeout=30) + + # Move Collimating mirror + logger.info("Move Collimating Mirror into position") + if "cm_rotx" in label: # pitch + logger.info(" Move pitch into position") + surveyed_movement( + axis=label['cm_rotx'], + surveyed_axes= [ + {'device': dev.cm_rotz, 'abs_tol': 0.1}, + ] + ) + + # Restore absorber position + logger.info("Restore Frontend Absorber Status") + if abs_was_open: + status = dev.abs.open() + status.wait() + + +def surveyed_movement(axis, surveyed_axes): + """ + Moves an axis while surverying a set of axes. + + Args: + axis (DeviceCondition): Device condition + surveyed_axes (list): List of dicts (same format as DeviceCondition) + + Raises: + If during movement of axis, one of the surveyed axes moves out of tolerance. + """ + + for surv_ax in surveyed_axes: + surv_ax['old_value'] = surv_ax['device'].read() + status = axis['device'].move(axis['value']) + while status.status == 'RUNNING': + for surv_ax in surveyed_axes: + if abs(surv_ax['device'].read() - surv_ax['old_value']) > surv_ax['abs_tol']: + axis['device'].stop() + raise MoveToLabelError( + f"During movement of {axis['device'].name}, {surv_ax['device'].name} " + + f"started to move unexpectedly (old pos: {surv_ax['old_value']}, " + + f"current pos: {surv_ax['device'].read()})" + ) diff --git a/debye_bec/bec_widgets/widgets/client.py b/debye_bec/bec_widgets/widgets/client.py new file mode 100644 index 0000000..cb2df5d --- /dev/null +++ b/debye_bec/bec_widgets/widgets/client.py @@ -0,0 +1,41 @@ +# This file was automatically generated by generate_cli.py +# type: ignore + +from __future__ import annotations + +from bec_lib.logger import bec_logger + +from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout + +logger = bec_logger.logger + +# pylint: skip-file + + +_Widgets = { + "DigitalTwin": "DigitalTwin", +} + + +class DigitalTwin(RPCBase): + """Main widget of Digital Twin""" + + _IMPORT_MODULE = "debye_bec.bec_widgets.widgets.digital_twin.digital_twin" + + @rpc_call + def remove(self): + """ + Cleanup the BECConnector + """ + + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ diff --git a/debye_bec/bec_widgets/widgets/designer_plugins.py b/debye_bec/bec_widgets/widgets/designer_plugins.py new file mode 100644 index 0000000..c941b27 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/designer_plugins.py @@ -0,0 +1,13 @@ +# This file was automatically generated by generate_cli.py +# type: ignore +from __future__ import annotations + +# pylint: skip-file + +designer_plugins = { + "DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"), +} + +widget_icons = { + "DigitalTwin": "lightbulb", +} diff --git a/debye_bec/bec_widgets/widgets/digital_twin/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py new file mode 100644 index 0000000..55a1cca --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_positions.py @@ -0,0 +1,242 @@ +import os +import numpy as np +from bec_lib import bec_logger + +os.environ["USE_XRT"] = "False" +import debye_bec.bec_widgets.widgets.x01da_parameters as bl + +logger = bec_logger.logger + +def calc_positions(cfg): + + pos = {} + + ## FE slits + trxr = -np.arctan(cfg['h_acc'])*bl.feSlits.center1[1] + trxw = (np.arctan(cfg['h_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1] + 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] + + # 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: + index = bl.cm.surface.index(cfg['cm_stripe']) + except: + raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!") + cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 + pos['cm_trx'] = {'value': cm_trx} + + # TRY + height = obj_dist * np.tan(cfg['v_acc'])**2 * 1 / np.tan(cfg['cm_pitch']) + pos['cm_try'] = {'value': height} + + # Pitch + pos['cm_rotx'] = {'value': -cfg["cm_pitch"]*1e3} # invert and convert to mrad (same as EGU of rotx axis) + + # Bending Radius + radius = 2. * obj_dist / np.sin(cfg['cm_pitch']) # Elements of modern X-ray Physics, page 108 ff. + pos['cm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km + + ## Monochromator + # Bragg Angle + # 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 = cfg['mo1_bragg'] + elif cfg['mo1_mode'] == 'Pinkbeam': + # Align xtal surfaces parallel to beam + bragg = 0 + else: + raise Exception('Monochromator mode not supported') + 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']) + yhor = l*np.cos(2.*(cfg['mo1_bragg']+cfg['cm_pitch'])) + yver = yhor*np.tan(2.*cfg['cm_pitch']) + + if cfg['mo1_mode'] == 'Monochromatic': + beamOffsetCCM = l*np.sin(2.*(cfg['mo1_bragg']+cfg['cm_pitch']))-yver # Resultat ist korrekt! + elif cfg['mo1_mode'] == 'Pinkbeam': + beamOffsetCCM = 0 + else: + raise Exception('Monochromator mode not supported') + + def csc(a): + return 1/np.sin(a) + + def cot(a): + return 1/np.tan(a) + + # calculate height of center of first crystal surface + f = bl.mo1.rotOffset # rotation offset, mm + # logger.info(f'f = {f}') + d = bl.mo1.heightOffset # xtal height offset, mm + # logger.info(f'd = {d}') + c = d*csc(cfg['mo1_bragg'])-f*cot(cfg['mo1_bragg']) + # logger.info(f'c = {c}') + + # Calculate height of center of rotation + b = np.sqrt(d**2*csc(cfg['mo1_bragg'])**2-2*d*f*cot(cfg['mo1_bragg'])*csc(cfg['mo1_bragg'])+f**2*cot(cfg['mo1_bragg'])**2+f**2) + # logger.info(f'b = {b}') + h = np.cos(np.pi/2-np.arctan(f/c)-cfg['mo1_bragg']-2*cfg['cm_pitch'])*b + # logger.info(f'h = {h}') + h2 = ((bl.mo1.center[1] - bl.cm.center[1])-np.sqrt(b**2-h**2))*np.tan(2*cfg['cm_pitch']) + # logger.info(f'mo1 = {bl.mo1.center[1]}') + # logger.info(f'cm = {bl.cm.center[1]}') + # logger.info(f'pitch = {cfg["cm_pitch"]}') + # logger.info(f'h2 = {h2}') + #TODO Mono height not exactly the same as in raytracing + heightCCM1real = h + h2 # per design, the height should not change if the pitch of the CM is not changed! + # heightCCM1real = heightCCM1real - 30 # Zero position of stage is at 1430 mm from ground. + if cfg['mo1_mode'] == 'Monochromatic': + pass + elif cfg['mo1_mode'] == 'Pinkbeam': + heightCCM1real = heightCCM1real - 13 # Move down to let beam pass between both crystal without touching copper cooler + else: + raise Exception('Monochromator mode not supported') + pos['mo1_try'] = {'value': heightCCM1real} + + # TRX, Crystal selection + if cfg['mo1_mode'] == 'Monochromatic': + try: + xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + index = bl.mo1.xtal.index(xtal) + except: + raise ValueError(f"Requested xtal {xtal} not found in parameters!") + pos['mo1_trx'] = {'value': bl.mo1.xtalOffsetX[index]} + else: + pos['mo1_trx'] = {'value': 0} + + + #TODO move to mono, calc for beam Z-movement between crystal surfaces + 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 + # logger.info(f'distance: {d}') + # logger.info(f'cm pitch: {cfg["cm_pitch"]}') + # logger.info(f'mono offset: {beamOffsetCCM}') + bm1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM + pos['bm1_try'] = {'value': bm1_beam_height} + + ## Focusing Mirror + p = bl.fm.center[1] + q = cfg['smpl'] - bl.fm.center[1] + f = (p*q)/(p+q) # focal length + + # Bender radius + if cfg['fm_qy'] is None: + radius = 2 * q / np.sin(cfg['fm_rotx']) # ideal bending radius for focused beam + else: + 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_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)'): + + # TRY + if cfg['fm_stripe'] in 'Rh (toroid)': + r = bl.fm.r[0] + h_cyl = bl.fm.hToroid[0] + else: # PT toroid + r = bl.fm.r[1] + h_cyl = bl.fm.hToroid[1] + widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc'] * 1e-3) + alpha = np.arccos(1 - widthBeam**2 / (2 * r**2)) + h = r - (r * np.cos(alpha / 2)) + fm_beam_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height'] + fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - h_cyl + h / 2) * cfg['fm_gain_height'] + pos['fm_try'] = {'value': fm_height} + + # TRX + if cfg['fm_stripe'] in 'Rh (toroid)': + x_cyl = - bl.fm.xToroid[0] + else: + x_cyl = - bl.fm.xToroid[1] + pos['fm_trx'] = {'value': x_cyl} + + elif cfg['fm_stripe'] in ('Rh (flat)', 'Pt (flat)'): + + # TRY + fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height'] + fm_beam_height = fm_height + pos['fm_try'] = {'value': fm_height} + + # TRX + if cfg['fm_stripe'] in 'Rh (flat)': + x_flat = - bl.fm.xFlat[0] + else: + x_flat = - bl.fm.xFlat[1] + pos['fm_trx'] = {'value': x_flat} + + 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_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_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_rotx'])) + # logger.info(fm_height) + # 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_rotx']) + pos['ot_rotx'] = {'value': ot_pitch * 1e3} + + # TRZ ES1 + 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 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py new file mode 100644 index 0000000..33487a2 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_sideview.py @@ -0,0 +1,42 @@ +import numpy as np +import debye_bec.bec_widgets.widgets.x01da_parameters as bl + +def calc_sideview(cfg): + + # Calculate height of beam after CM + height = 2 * bl.cm.center[1] * np.tan(cfg['v_acc']) + + # beam height (Y=height, Z=along beam) + beam = {} + beam['x'] = [] + beam['y'] = [] + beam['x'].append(0) # Source + beam['y'].append(bl.sourceHeight) + beam['x'].append(bl.cm.center[1]) # CM + beam['y'].append(bl.sourceHeight) + if cfg['mo1_mode'] in 'Monochromatic': + diag = bl.mo1.xtalGap[0]/np.sin(cfg['mo1_bragg']) # Calculations for Mono + dy = diag*np.sin(2*(cfg['cm_pitch']+cfg['mo1_bragg'])) + dz = diag*np.cos(2*(cfg['cm_pitch']+cfg['mo1_bragg'])) + beam['x'].append(bl.mo1.center[1]-dz/2) # Mono 1.1 + beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1])) + beam['x'].append(bl.mo1.center[1]+dz/2) # Mono 1.2 + beam['y'].append(bl.sourceHeight+np.tan(2*cfg['cm_pitch'])*(bl.mo1.center[1]-dz/2-bl.cm.center[1])+dy) + 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_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_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] + dz_fm_win = bl.ehWindow.center[1] - beam['x'][-2] + h_at_win = beam['y'][-2] + dy_fm_ex / dz_fm_ex * dz_fm_win + + beam['heightWindow'] = h_at_win + + return beam diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py new file mode 100644 index 0000000..013164b --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py @@ -0,0 +1,131 @@ +import os +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 + +def calc_surfaces(cfg): + + out = { + 'cm': {'x': [], 'y': []}, + 'mo1_1': {'x': [], 'y': []}, + 'mo1_2': {'x': [], 'y': []}, + 'fm': {'x': [], 'y': []}, + } + + # Collimating mirror + l = 2 * bl.cm.center[1] * np.tan(cfg['v_acc'])/np.sin(cfg['cm_pitch']) + + w1 = 2 * (bl.cm.center[1]-l/2) * np.tan(cfg['h_acc']) + w2 = 2 * (bl.cm.center[1]+l/2) * np.tan(cfg['h_acc']) + + index = bl.cm.surface.index(cfg['cm_stripe']) + cen = (bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 + + if cfg['cm_trx'] is not None: + cen = cfg['cm_trx'] + + out['cm']['x'] = [cen-w1/2, cen-w2/2, cen+w2/2, cen+w1/2] + out['cm']['y'] = [-l/2, l/2, l/2, -l/2] + + + # Monochromator + # calculate height of center of first crystal surface + c = bl.mo1.heightOffset*1/np.sin(cfg['mo1_bragg'])-bl.mo1.rotOffset*1/np.tan(cfg['mo1_bragg']) + e = bl.mo1.xtalGap[0]/np.tan(cfg['mo1_bragg'])-c + + xtal = cfg['mo1_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + index = bl.mo1.xtal.index(xtal) + + xtalPos = bl.mo1.xtalOffsetX[index] + xtalLength1 = bl.mo1.xtalLength1[index] + xtalLength2 = bl.mo1.xtalLength2[index] + + widthBeam = 2 * bl.mo1.center[1] * np.tan(cfg['h_acc']) + + heightBeam = 2 * bl.cm.center[1] * np.tan(cfg['v_acc']) + w = heightBeam / np.sin(cfg['mo1_bragg']) + + if cfg['mo1_mode'] in 'Monochromatic': + out['mo1_1']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/2] + out['mo1_1']['y'] = [xtalLength1/2-c-w/2, xtalLength1/2-c-w/2, xtalLength1/2-c+w/2, xtalLength1/2-c+w/2] + out['mo1_2']['x'] = [xtalPos-widthBeam/2, xtalPos+widthBeam/2, xtalPos+widthBeam/2, xtalPos-widthBeam/2] + out['mo1_2']['y'] = [-xtalLength2/2+e-w/2, -xtalLength2/2+e-w/2, -xtalLength2/2+e+w/2, -xtalLength2/2+e+w/2] + else: # Pinkbeam + out['mo1_1']['x'] = [] + out['mo1_1']['y'] = [] + out['mo1_2']['x'] = [] + 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() + index = surface.index(stripe) + off = (bl.fm.limOptXToroid[0][index] + bl.fm.limOptXToroid[1][index]) / 2 + r = bl.fm.r[index] + else: + surface = bl.fm.surfaceFlat + stripe = re.sub(r'\s*\(.*?\)', '', cfg['fm_stripe']).strip() + index = surface.index(stripe) + off = (bl.fm.limOptXFlat[0][index] + bl.fm.limOptXFlat[1][index]) / 2 + r = bl.fm.r[index] + if cfg['fm_trx'] is not None: + off = cfg['fm_trx'] + + widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc']) + + if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'): + + 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_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_rotx: {cfg["fm_rotx"]}') + # 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) + xElipse = [-widthBeam/2*np.cos(i)+off for i in xElipse] + yElipse = [widthBeam*np.sin(i)*z/widthBeam-l/2-z/2 for i in yElipse] + + x.extend(xElipse) + y.extend(yElipse) + + x.extend([off+widthBeam/2, off+widthBeam/2]) + y.extend([-l/2-z/2, l/2-z/2]) + + res = 50 + xElipse = np.linspace(np.pi, 0, res) + yElipse = np.linspace(np.pi, 0, res) + xElipse = [-widthBeam/2*np.cos(i)+off for i in xElipse] + yElipse = [widthBeam*np.sin(i)*z/widthBeam+l/2-z/2 for i in yElipse] + + x.extend(xElipse) + y.extend(yElipse) + + out['fm']['x'] = x + out['fm']['y'] = y + + else: # flat surface, no toroid + 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']) + + out['fm']['x'] = [off-w1/2, off+w1/2, off+w2/2, off-w2/2] + out['fm']['y'] = [-l/2, -l/2, l/2, l/2] + + return out diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py new file mode 100644 index 0000000..029be5d --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_varia.py @@ -0,0 +1,206 @@ +import re +import numpy as np +from scipy.interpolate import UnivariateSpline +from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO +from bec_lib import bec_logger + +import debye_bec.bec_widgets.widgets.x01da_parameters as bl + +logger = bec_logger.logger + +def sldi_gap_to_acc(sldi_gapx, sldi_gapy): + d1 = bl.feSlits.center1[1] + 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): + cm_stripe = None + for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): + if low <= cm_trx <= high: + cm_stripe = name + return cm_stripe + +def fm_trx_to_stripe(fm_trx): + fm_stripe = None + for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): + if low <= fm_trx <= high: + fm_stripe = name + ' (flat)' + for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): + if low <= fm_trx <= high: + fm_stripe = name + ' (toroid)' + return fm_stripe + +def mo1_energy_resolution(xtal, energy): + index = bl.mo1.xtal.index(xtal) + crystal = bl.mo1.material1[index] + + dtheta = np.linspace(-30, 90, 601) + theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6 + refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0])**2 # single crystal + + refl2 = refl**2 # DCM with parallel crystals + + # FWHM of the DCM curve + spline = UnivariateSpline(dtheta, refl2 - refl2.max()/2, s=0) + r1, r2 = spline.roots() + fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad + + # Energy resolution + theta_B = crystal.get_Bragg_angle(energy) + dE_over_E = fwhm_rad / np.tan(theta_B) + dE = dE_over_E * energy + + # logger.info(f"DCM FWHM : {r2-r1:.2f} µrad") + # logger.info(f"ΔE/E : {dE_over_E:.2e}") + # logger.info(f"ΔE : {dE:.3f} eV at {E} eV") + + return dE + +def cm_reflectivity(cm_stripe, cm_pitch, energy): + index = bl.cm.surface.index(cm_stripe) + rs, rp = bl.cm.material[index].get_amplitude( + energy, + np.sin(cm_pitch) + )[0:2] + refl = abs(rs)**2 + return refl + +def fm_reflectivity(fm_stripe, fm_pitch, energy): + if fm_stripe in ('Rh (toroid)', 'Pt (toroid)'): + surface = bl.fm.surfaceToroid + material = bl.fm.materialToroid + stripe = re.sub(r'\s*\(.*?\)', '', fm_stripe).strip() + index = surface.index(stripe) + else: + surface = bl.fm.surfaceFlat + material = bl.fm.materialFlat + stripe = re.sub(r'\s*\(.*?\)', '', fm_stripe).strip() + index = surface.index(stripe) + rs, rp = material[index].get_amplitude( + energy, + np.sin(fm_pitch) + )[0:2] + refl = abs(rs)**2 + return refl + +def mo1_bragg_angle(mo_mode, d_spacing, energy, cm_pitch): + H = 6.62606957E-34 + E = 1.602176634E-19 + C = 299792458 + wl = C * H / (E * energy) + val = wl / (2 * d_spacing * 1e-10) + bragg_angle = 0 + if val > -1 and val < 1: + bragg_angle = np.asin(val) + if mo_mode in 'Monochromatic': + # Add 2x CM pitch to the bragg angle + bragg_angle_cor = ((2 * cm_pitch) + bragg_angle) + elif mo_mode in 'Pinkbeam': + # Align xtal surfaces parallel to beam + bragg_angle_cor = (2 * cm_pitch) + return bragg_angle, bragg_angle_cor + +def fm_ideal_pitch(fm_focus, fm_stripe, smpl, sldi_hacc=None, sldi_vacc=None, fm_focx=None, fm_focy=None): + p = bl.fm.center[1] # posFM + q = smpl - bl.fm.center[1] # dist posFM to posEX + if fm_focus in 'Defocused': + a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror + b = 2 * np.tan(sldi_vacc) * bl.cm.center[1] # Beam height at focusing mirror (collimated beam) + x = fm_focx + y = fm_focy + qx = q + x * p / a + qy = q + y * p / b + f = (p * qx) / (p + qx) # focal length + else: # Calculate for focused beam on sample in "manual" and "focused" mode + qy = None + f = (p * q) / (p + q) # focal length + pitch = 0 + if 'Rh' in fm_stripe: + pitch = np.arcsin(bl.fm.r[0]/(2*f))# ideal pitch for FM + if 'Pt' in fm_stripe: + pitch = np.arcsin(bl.fm.r[1]/(2*f)) # ideal pitch for FM + return pitch, qy + +def cm_critical_angle(cm_stripe, energy): + if cm_stripe in 'Si': + stripe = bl.stripeSi + elif cm_stripe in 'Pt': + stripe = bl.stripePt + elif cm_stripe in 'Rh': + stripe = bl.stripeRh + else: + raise Exception(f'Stripe {stripe} not found in beamline parameters!') + w = CHeVcm/100/energy # convert energy [eV] to wavelength [m] + # Calculate critical angle for mirror + f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy)) + numberDensity = stripe.rho*1e3*AVOGADRO/(stripe.elements[0].mass/1e3) + criticalAngle = np.sqrt(numberDensity*2.8179e-15*w**2*f1/np.pi) + return criticalAngle + + +def mirror_surface_geometries(mirror): + if mirror in "cm": + surface = bl.cm.surface + limOptX = bl.cm.limOptX + limOptY = bl.cm.limOptY + elif mirror in 'fm_toroid': + surface = bl.fm.surfaceToroid + limOptX = bl.fm.limOptXToroid + limOptY = bl.fm.limOptYToroid + elif mirror in 'fm_flat': + surface = bl.fm.surfaceFlat + limOptX = bl.fm.limOptXFlat + limOptY = bl.fm.limOptYFlat + else: + raise ValueError(f'Requested mirror {mirror} not available!') + geom = {} + for sf, lx, hx, ly, hy in zip(surface, limOptX[0], limOptX[1], limOptY[0], limOptY[1]): + geom[sf] = (lx, ly, hx-lx, hy-ly) + return geom + +def mo_surface_geometries(mo, plane): + if mo in 'mo1': + xtal = bl.mo1.xtal + xtal_width = bl.mo1.xtalWidth + xtal_offset_x = bl.mo1.xtalOffsetX + if plane == 0: + xtal_length = bl.mo1.xtalLength1 + else: + xtal_length = bl.mo1.xtalLength2 + else: + raise ValueError(f'Requested mono {mo} not available!') + geom = {} + for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length): + geom[sf] = (offx-w/2, -length/2, w, length) + return geom + +def wall_geometries(): + geom = [] + for i, _ in enumerate(bl.walls.start): + geom.append([ + bl.walls.start[i], + bl.walls.height[i][0], + bl.walls.end[i] - bl.walls.start[i], + bl.walls.height[i][1] - bl.walls.height[i][0], + ]) + return geom + +def pipe_geometries(): + pipes = [] + for i, _ in enumerate(bl.vacuum_pipes.center): + top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight + bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i]/2 + bl.sourceHeight + pipes.append({ + 'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + 'y': np.array([top, top]) + }) + pipes.append({ + 'x': np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + 'y': np.array([bottom, bottom]) + }) + return pipes diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py new file mode 100644 index 0000000..7e2b163 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -0,0 +1,1427 @@ +""" +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 + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QApplication, + QLayout, + QMessageBox, + QLabel, + QDialog, + QPushButton, + QStyle, +) +# pylint: disable=E0611 +from qtpy.QtCore import ( + Qt, + QTimer, +) +from qtpy.QtGui import ( + QColor, + QBrush, + QCloseEvent, +) +import pyqtgraph as pg + +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, +) +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 +from debye_bec.bec_widgets.widgets.digital_twin.calc_varia import ( + sldi_gap_to_acc, + cm_trx_to_stripe, + fm_trx_to_stripe, + mo1_energy_resolution, + cm_reflectivity, + fm_reflectivity, + mo1_bragg_angle, + fm_ideal_pitch, + cm_critical_angle, + mirror_surface_geometries, + mo_surface_geometries, + wall_geometries, + pipe_geometries, +) +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 + """ + + PLUGIN = True + ICON_NAME = "lightbulb" + + def __init__(self, parent=None, *arg, **kwargs): + super().__init__(parent=parent, theme_update=True, *arg, **kwargs) + self.get_bec_shortcuts() + + # Check if devices are all in config + self.check_config() + + 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.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_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") + self.resize(1800, 800) + + self.input.energy.value_changed_connect(self.calc_assistant) + self.input.sldi_hacc.value_changed_connect(self.calc_assistant) + self.input.sldi_vacc.value_changed_connect(self.calc_assistant) + self.input.cm_stripe.activated_connect(self.calc_assistant) + self.input.cm_pitch.value_changed_connect(self.calc_assistant) + self.input.mo1_mode.activated_connect(self.calc_assistant) + 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_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 + self._timer = QTimer(self) + self._timer.setInterval(100) + self._timer.timeout.connect(self.calc_reality) + self._timer.start() + + def apply_theme(self, theme): + self.sideview_plot.apply_theme(theme) + self.surface_plots.apply_theme(theme) + self.mover.apply_theme(theme) + + def check_config(self): + devices = [ + 'abs', + 'sldi_gapx', + 'sldi_gapy', + 'cm_trx', + 'cm_try', + 'cm_bnd_radius', + 'cm_rotx', + 'mo1_bragg', + 'mo1_trx', + 'mo1_try', + 'sl1_centery', + 'sl1_gapy', + 'bm1_try', + 'fm_trx', + 'fm_try', + 'fm_bnd_radius', + 'fm_rotx', + 'fm_roty', + 'fm_rotz', + 'sl2_centery', + 'sl2_gapy', + 'bm2_try', + 'ot_try', + 'ot_rotx', + 'es0wi_try', + 'ot_es1_trz', + ] + while True: + missing = [d for d in devices if d not in self.dev] + if not missing: + break + dialog = QDialog() + dialog.setWindowTitle("Digital Twin - Config Check") + dialog.setFixedWidth(400) + layout = QVBoxLayout() + + top = QHBoxLayout() + icon = QLabel() + icon_pixmap = QApplication.style().standardIcon( + QStyle.SP_MessageBoxWarning + ).pixmap(48, 48) + icon.setPixmap(icon_pixmap) + icon.setAlignment(Qt.AlignTop) + top.addWidget(icon) + + text = QLabel( + "The current config does not include all required devices to run Digital Twin." + + "Reload the config with the correct devices." + ) + text.setWordWrap(True) + text.setAlignment(Qt.AlignTop) + top.addWidget(text, stretch=1) + layout.addLayout(top) + + info = QLabel("Missing devices:\n" + ", ".join(missing)) + info.setWordWrap(True) + info.setAlignment(Qt.AlignTop) + layout.addWidget(info) + layout.addStretch() + + buttons = QHBoxLayout() + check_again = QPushButton("Check Again") + close_app = QPushButton("Close Application") + check_again.clicked.connect(dialog.accept) + close_app.clicked.connect(dialog.reject) + buttons.addWidget(check_again) + buttons.addWidget(close_app) + layout.addLayout(buttons) + + dialog.setLayout(layout) + dialog.show() + info.setMinimumHeight(info.heightForWidth(info.width())) + if dialog.exec_() == QDialog.Rejected: + QApplication.instance().exit(0) + sys.exit(0) + + @SafeSlot() + def calc_assistant(self, *args, **kwargs): + 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() + self.update_fm_mode() + self.calc_fm_reflectivity() + self.calc_cm_fm_harm_suppr() + self.calc_fm_ideal_pitch() + self.calc_mo1_energy_resolution() + case 'energy': + self.calc_mo1_bragg_angle() + self.calc_cm_crit_pitch() + self.calc_cm_reflectivity() + self.calc_fm_reflectivity() + self.calc_cm_fm_harm_suppr() + self.calc_mo1_energy_resolution() + case 'cm_stripe': + self.calc_cm_crit_pitch() + self.calc_cm_reflectivity() + self.calc_cm_fm_harm_suppr() + case 'cm_pitch': + self.calc_cm_reflectivity() + self.calc_cm_fm_harm_suppr() + case 'mo1_mode': + self.update_mo1_mode() + case 'mo1_xtal': + self.calc_mo1_bragg_angle() + self.calc_mo1_energy_resolution() + case 'fm_focus': + self.update_fm_mode() + self.calc_fm_ideal_pitch() + case 'fm_focx': + self.calc_fm_ideal_pitch() + case 'fm_focy': + self.calc_fm_ideal_pitch() + case 'fm_stripe': + self.calc_fm_reflectivity() + self.calc_cm_fm_harm_suppr() + self.calc_fm_ideal_pitch() + case 'smpl': + self.calc_fm_ideal_pitch() + self.calc_positions() + self.calc_assistant_sideview() + self.calc_assistant_surfaces() + + def get_assistant_config(self): + + fm_focus = self.input.fm_focus.currentText() + if fm_focus in 'Manual': + fm_rotx = self.input.fm_rotx.value() + fm_qy = None + elif fm_focus in 'Focused': + fm_rotx = self.input.fm_rotx_ideal.value() + fm_qy = None + else: # Focused + fm_rotx = self.input.fm_rotx_ideal.value() + fm_qy = self.qy + + config = { # Config in SI units! + 'energy' : self.input.energy.value(), + 'h_acc' : self.input.sldi_hacc.value() * 1e-3, + 'v_acc' : self.input.sldi_vacc.value() * 1e-3, + 'cm_pitch' : -self.input.cm_pitch.value() * 1e-3, + 'cm_stripe' : self.input.cm_stripe.currentText(), + 'cm_trx' : None, + 'mo1_mode' : self.input.mo1_mode.currentText(), + 'mo1_xtal' : self.input.mo1_xtal.currentText(), + 'mo1_bragg' : self.bragg_angle, + 'fm_rotx' : -fm_rotx * 1e-3, + 'fm_stripe' : self.input.fm_stripe.currentText(), + 'fm_trx' : None, + 'fm_qy' : fm_qy, + 'fm_gain_height' : 1, + 'smpl' : self.input.smpl.value(), + } + # logger.info(f'Config created: {config}') + return config + + def get_reality_config(self): + # Assure all devices are in the config + self.check_config() + mo1_trx = self.dev.mo1_trx.read(cached=True)['mo1_trx']['value'] + if abs(mo1_trx) > 5: + mo1_mode = 'Monochromatic' + else: + mo1_mode = 'Pinkbeam' + mo1_bragg = self.dev.mo1_bragg.read(cached=True) + 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_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_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'], + 'h_acc' : h_acc, + 'v_acc' : v_acc, + 'cm_pitch' : -cm_pitch * 1e-3, + 'cm_stripe' : cm_stripe, + '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_rotx' : -fm_rotx_real * 1e-3, + 'fm_stripe' : fm_stripe, + '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(True) # Enable open button + else: + self.mover.abs.enable_open(False) # Disable open button + else: + 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) + self.mover.cm_trx.set_feedback(cm_trx) + self.mover.cm_try.set_feedback(self.dev.cm_try.read(cached=True)['cm_try']['value']) + self.mover.cm_bnd.set_feedback(self.dev.cm_bnd_radius.read(cached=True)['cm_bnd_radius']['value']) + self.mover.cm_rotx.set_feedback(cm_pitch) + self.mover.mo1_bragg_angle.set_feedback(mo1_bragg['mo1_bragg_angle']['value']) + 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_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']) + 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): + 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( + 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(-pos['cm_trx']) + ) + self.input.cm_pitch.set_number(pos['cm_rotx']) + if abs(pos['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(-pos['fm_trx']) + ) + self.input.fm_focus.set_current_text('Manual') + fm_rotx_real = 2 * pos['cm_rotx'] - pos['fm_rotx'] + self.input.fm_rotx.set_number(fm_rotx_real) + self.input.smpl.set_number( + pos['ot_es1_trz'] + ) + 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_rotx.setVisible(True) + self.input.fm_rotx_ideal.setVisible(True) + self.input.fm_focx.setVisible(False) + self.input.fm_focy.setVisible(False) + self.input.fm_rotx_ideal.setLabel('Incidence Angle for focused beam') + elif fm_focus in 'Focused': + 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_rotx_ideal.setLabel('Incidence Angle for focused beam') + else: # Defocused + 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_rotx_ideal.setLabel('Incidence Angle for defocused beam') + + def calc_reality(self): + config = self.get_reality_config() + 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) + + def calc_mo1_energy_resolution(self): + xtal = self.input.mo1_xtal.currentText().translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters + energy = self.input.energy.value() + self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy)) + + def calc_cm_reflectivity(self): + cm_stripe = self.input.cm_stripe.currentText() + cm_pitch = -self.input.cm_pitch.value() * 1e-3 + energy = self.input.energy.value() + self.input.cm_refl.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, energy)) + self.input.cm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV") + self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3*energy)) + self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") + + def calc_fm_reflectivity(self): + fm_stripe = self.input.fm_stripe.currentText() + fm_focus = self.input.fm_focus.currentText() + if fm_focus in 'Manual': + fm_rotx = -self.input.fm_rotx.value() * 1e-3 + else: + 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_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_rotx, 3*energy)) + self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") + + def calc_cm_fm_harm_suppr(self): + harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value()) + self.input.cm_fm_harm_suppr.setValue(harm_suppr) + self.input.cm_fm_harm_suppr.setLabel(f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV") + + def calc_assistant_sideview(self): + beam = calc_sideview(self.get_assistant_config()) + data = {'x': beam['x'], 'y': beam['y']} + 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) + + 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']) + self.positions.cm_try.setValue(out['cm_try']['value']) + self.positions.cm_bnd.setValue(out['cm_bnd_radius']['value']) + self.positions.cm_rotx.setValue(out['cm_rotx']['value']) + self.positions.mo1_bragg_angle.setValue(out['mo1_bragg_angle']['value']) + self.positions.mo1_trx.setValue(out['mo1_trx']['value']) + self.positions.mo1_try.setValue(out['mo1_try']['value']) + self.positions.sl1_centery.setValue(out['sl1_centery']['value']) + self.positions.bm1_try.setValue(out['bm1_try']['value']) + self.positions.fm_trx.setValue(out['fm_trx']['value']) + self.positions.fm_try.setValue(out['fm_try']['value']) + self.positions.fm_bnd.setValue(out['fm_bnd_radius']['value']) + self.positions.fm_rotx.setValue(out['fm_rotx']['value']) + self.positions.sl2_centery.setValue(out['sl2_centery']['value']) + self.positions.bm2_try.setValue(out['bm2_try']['value']) + self.positions.ot_try.setValue(out['ot_try']['value']) + self.positions.ot_rotx.setValue(out['ot_rotx']['value']) + self.positions.ot_es1_trz.setValue(out['ot_es1_trz']['value']) + + self.mover.sldi_gapx.set_target(out['sldi_gapx']['value']) + self.mover.sldi_gapy.set_target(out['sldi_gapy']['value']) + self.mover.cm_trx.set_target(out['cm_trx']['value']) + self.mover.cm_try.set_target(out['cm_try']['value']) + self.mover.cm_bnd.set_target(out['cm_bnd_radius']['value']) + self.mover.cm_rotx.set_target(out['cm_rotx']['value']) + self.mover.mo1_bragg_angle.set_target(out['mo1_bragg_angle']['value']) + 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.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']) + 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): + """ + Calculates bragg angle in rad + """ + xtal = self.input.mo1_xtal.currentText() + if xtal in 'Si(111)': + d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)['mo1_bragg_crystal_d_spacing_si111']['value'] + elif xtal in 'Si(311)': + d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)['mo1_bragg_crystal_d_spacing_si311']['value'] + else: + raise Exception(f'Invalid xtal selection: {xtal}') + cm_pitch = -self.dev.cm_rotx.read(cached=True)['cm_rotx']['value'] * 1e-3 + mo1_mode = self.input.mo1_mode.currentText() + 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 / np.pi * 180) + + def update_mo1_mode(self): + if self.input.mo1_mode.currentText() in 'Monochromatic': + self.input.mo1_xtal.setVisible(True) + self.input.mo1_bragg_angle.setVisible(True) + self.input.mo1_eres.setVisible(True) + else: + self.input.mo1_xtal.setVisible(False) + self.input.mo1_bragg_angle.setVisible(False) + self.input.mo1_eres.setVisible(False) + + def calc_fm_ideal_pitch(self): # TODO: What happens if the flats are selected? + fm_focus = self.input.fm_focus.currentText() + fm_stripe = self.input.fm_stripe.currentText() + smpl = self.input.smpl.value() + sldi_hacc = self.input.sldi_hacc.value() * 1e-3 + sldi_vacc = self.input.sldi_vacc.value() * 1e-3 + fm_focx = self.input.fm_focx.value() + fm_focy = self.input.fm_focy.value() + 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_rotx_ideal.setValue(-fm_rotx * 1e3) + + def calc_cm_crit_pitch(self): + cm_stripe = self.input.cm_stripe.currentText() + energy = self.input.energy.value() + self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3) + +class InputPanel(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 + + # 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=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', + [ + self.sldi_hacc, + self.sldi_vacc, + ] + ) + + # Collimating mirror + self.cm_stripe = ComboBox('cm_stripe', 'Stripe', ['Si', 'Rh', 'Pt']) + self.cm_pitch = InputNumberField('cm_pitch', 'Pitch', unit='mrad', init=-2.391, decimals=3, single_step=0.01, ll=-4.6, hl=-1.2) + self.cm_pitch_critical = NumberIndicator('Critical Pitch', 'mrad', decimals=3) + self.cm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0) + self.cm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0) + self.cm_ass_group = Group( + 'Collimating Mirror', + [ + self.cm_stripe, + self.cm_pitch, + self.cm_pitch_critical, + self.cm_refl, + self.cm_refl_harm, + ] + ) + + # Monochromator + self.mo1_mode = ComboBox('mo1_mode', 'Mode', ['Monochromatic', 'Pinkbeam']) + self.mo1_xtal = ComboBox('mo1_xtal', 'Crystal', ['Si(111)', 'Si(311)']) + self.mo1_bragg_angle = NumberIndicator('Bragg Angle', 'deg', decimals=1) + self.mo1_eres = NumberIndicator('Energy Resolution', 'eV', decimals=2) + self.mo1_ass_group = Group( + 'Monochromator', + [ + self.mo1_mode, + self.mo1_xtal, + self.mo1_bragg_angle, + self.mo1_eres, + ] + ) + + # 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_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_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( + 'Focusing Mirror', + [ + self.fm_stripe, + self.fm_focus, + self.fm_rotx, + self.fm_focx, + self.fm_focy, + self.fm_rotx_ideal, + self.fm_refl, + self.fm_refl_harm, + ] + ) + + # Sample + self.cm_fm_harm_suppr = NumberIndicator('Total Suppression Factor at x eV', '', decimals=0) + self.smpl = InputNumberField('smpl', 'Sample Position', unit='mm', init=23511, decimals=0, single_step=100, ll=23000, hl=30000) + + # Assemble complete assitant group + self.input_group = Group( + 'User Input', + [ + self.adapt_reality, + self.energy, + self.sldi_ass_group, + self.cm_ass_group, + self.mo1_ass_group, + self.fm_ass_group, + self.cm_fm_harm_suppr, + self.smpl, + ] + ) + + 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.""" + + def __init__(self, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + # FE Slits + self.sldi_gapx = NumberIndicator('GAPX', 'mm', decimals=2) + self.sldi_gapy = NumberIndicator('GAPY', 'mm', decimals=2) + self.sldi_pos_group = Group( + 'FE Slits', + [ + self.sldi_gapx, + self.sldi_gapy, + ] + ) + + # Collimating mirror + self.cm_trx = NumberIndicator('TRX', 'mm', decimals=2) + self.cm_try = NumberIndicator('TRY', 'mm', decimals=2) + self.cm_bnd = NumberIndicator('BENDER', 'km', decimals=2) + self.cm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3) + self.cm_pos_group = Group( + 'Collimating Mirror', + [ + self.cm_trx, + self.cm_try, + self.cm_bnd, + self.cm_rotx, + ] + ) + + # Monochromator + self.mo1_bragg_angle = NumberIndicator('Bragg Angle', 'deg', decimals=3) + self.mo1_trx = NumberIndicator('TRX', 'mm', decimals=2) + self.mo1_try = NumberIndicator('TRY', 'mm', decimals=2) + self.mo1_pos_group = Group( + 'Monochromator', + [ + self.mo1_bragg_angle, + self.mo1_trx, + self.mo1_try, + ] + ) + + # OP Slits 1 + self.sl1_centery = NumberIndicator('CENTERY', 'mm', decimals=2) + self.sl1_pos_group = Group( + 'OP Slits 1', + [ + self.sl1_centery, + ] + ) + + # OP Beam Monitor 1 + self.bm1_try = NumberIndicator('TRY', 'mm', decimals=2) + self.bm1_pos_group = Group( + 'OP Beam Monitor 1', + [ + self.bm1_try, + ] + ) + + # Focusing Mirror + self.fm_trx = NumberIndicator('TRX', 'mm', decimals=2) + self.fm_try = NumberIndicator('TRY', 'mm', decimals=2) + self.fm_bnd = NumberIndicator('BENDER', 'km', decimals=2) + self.fm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3) + self.fm_pos_group = Group( + 'Focusing Mirror', + [ + self.fm_trx, + self.fm_try, + self.fm_bnd, + self.fm_rotx, + ] + ) + + # OP Slits 2 + self.sl2_centery = NumberIndicator('CENTERY', 'mm', decimals=2) + self.sl2_pos_group = Group( + 'OP Slits 2', + [ + self.sl2_centery, + ] + ) + + # OP Beam Monitor 2 + self.bm2_try = NumberIndicator('TRY', 'mm', decimals=2) + self.bm2_pos_group = Group( + 'OP Beam Monitor 2', + [ + self.bm2_try, + ] + ) + + # Optical Table + self.ot_try = NumberIndicator('TRY', 'mm', decimals=2) + self.ot_rotx = NumberIndicator('ROTX', 'mrad', decimals=3) + self.ot_es1_trz = NumberIndicator('ES1 TRZ', 'mm', decimals=0) + self.ot_pos_group = Group( + 'Optical Table', + [ + self.ot_try, + self.ot_rotx, + self.ot_es1_trz, + ] + ) + + # Assemble complete assitant group + self.position_group = Group( + 'Axes Positions Calculator', + [ + self.sldi_pos_group, + self.cm_pos_group, + self.mo1_pos_group, + self.sl1_pos_group, + self.bm1_pos_group, + self.fm_pos_group, + self.sl2_pos_group, + self.bm2_pos_group, + self.ot_pos_group, + ] + ) + + self._layout .addWidget(self.position_group) + self._layout .addStretch() + +class MoverPanel(QWidget): + + def __init__(self, dev, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + self.mover_widgets = [] + + # FE Slits + 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) + + 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( + 'FE Slits', + [ + self.sldi_gapx, + self.sldi_gapy, + ] + ) + + # Absorber + self.abs = AbsorberWidget(absorber=dev.abs, label='') + + self.abs_group = Group( + 'Absorber', + [ + self.abs, + ] + ) + + # Collimating mirror + 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) + + 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) + + 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) + + 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( + 'Collimating Mirror', + [ + self.cm_trx, + self.cm_try, + self.cm_bnd, + self.cm_rotx, + ] + ) + + # Monochromator + 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) + + 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) + + 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( + 'Monochromator', + [ + self.mo1_bragg_angle, + self.mo1_trx, + self.mo1_try, + ] + ) + + # OP Slits 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 + 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( + 'OP Beam Monitor 1', + [ + self.bm1_try, + ] + ) + + # Focusing Mirror + 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) + + 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) + + 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) + + 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', + [ + self.fm_trx, + self.fm_try, + self.fm_bnd, + self.fm_rotx, + self.fm_roty, + self.fm_rotz, + ] + ) + + # OP Slits 2 + 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 + 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( + 'OP Beam Monitor 2', + [ + self.bm2_try, + ] + ) + + # Optical Table + 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) + + 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) + + 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, + ] + ) + + # Assemble complete mover group + self.mover_group = Group( + 'Mover', + [ + self.sldi_mov_group, + self.abs_group, + self.cm_mov_group, + self.mo1_mov_group, + self.sl1_mov_group, + self.bm1_mov_group, + self.fm_mov_group, + self.sl2_mov_group, + self.bm2_mov_group, + self.ot_mov_group, + self.es0_mov_group, + self.es1_mov_group, + ] + ) + + self._layout .addWidget(self.mover_group) + self._layout .addStretch() + + def apply_theme(self, theme): + for widget in self.mover_widgets: + widget.apply_theme(theme) + +class SurfacePlots(QWidget): + """Plot widget with two curves and legend.""" + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._layout = QHBoxLayout(self) + + self.surfaces = { + 'assistant': { + 'cm': {'x': [], 'y': []}, + 'mo1_1': {'x': [], 'y': []}, + 'mo1_2': {'x': [], 'y': []}, + 'fm': {'x': [], 'y': []}, + }, + 'reality': { + 'cm': {'x': [], 'y': []}, + 'mo1_1': {'x': [], 'y': []}, + 'mo1_2': {'x': [], 'y': []}, + 'fm': {'x': [], 'y': []}, + }, + } + + self.plots = { + 'fm': {}, + 'mo1_2': {}, + 'mo1_1': {}, + 'cm': {}, + } + + self.color_impenetrable = (0, 0, 0) + self.colors = [(255, 255, 0), (255, 0, 255)] + self.text_color = (255, 255, 255) + + # Create plot widgets + for name, widget in self.plots.items(): + plot_widget = pg.PlotWidget() + plot_widget.getAxis('bottom').enableAutoSIPrefix(False) + + plot_group = Group( + 'Surface ' + name, + [ + plot_widget, + ] + ) + + plot_widget.setLabel('left', 'Z [mm]') + plot_widget.setLabel('bottom', 'X [mm]') + plot_widget.setMouseEnabled(x=False, y=False) + plot_widget.setMenuEnabled(False) + plot_widget.hideButtons() + + widget['widget'] = plot_widget + self._layout.addWidget(plot_group) + + # Create surfaces + for idx, scene in enumerate(self.surfaces): + for name, _ in self.surfaces[scene].items(): + if scene in 'assistant': + brush = QBrush(QColor(*self.colors[idx], 255), Qt.DiagCrossPattern) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1, style=Qt.DashLine) + z_value = 2 + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1) + z_value = 1 + widget = self.plots[name] + self.plots[name][scene] = widget['widget'].plot( + [], + [], + pen=pen, + name=scene, + brush=brush, + fillLevel=0, + ) + self.plots[name][scene].setZValue(z_value) + + self.walls = [] + self.texts = [] + + self.plot_walls() + + self.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + for _, plot in self.plots.items(): + # Background + plot['widget'].setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = plot['widget'].getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + if theme == "light": + self.color_impenetrable = (30, 30, 30) + self.colors = [(79, 163, 224), (240, 128, 60)] + self.text_color = (255, 255, 255) + else: # dark theme + self.color_impenetrable = (180, 180, 180) + self.colors = [(26, 111, 173), (212, 83, 10)] + self.text_color = (0, 0, 0) + + for idx, scene in enumerate(self.surfaces): + for name, _ in self.surfaces[scene].items(): + if scene in 'assistant': + brush = QBrush(QColor(*self.colors[idx], 255), Qt.DiagCrossPattern) + 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=0) + self.plots[name][scene].setPen(pen) + self.plots[name][scene].setBrush(brush) + + for wall in self.walls: + wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + + for text in self.texts: + text.setColor(self.text_color) + + def plot_walls(self): + + def plot_surface(widget, surfaces): + for name, surface in surfaces.items(): + rect = pg.QtWidgets.QGraphicsRectItem(*surface) # pylint: disable=E1101 + rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + widget.addItem(rect) + text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5)) + widget.addItem(text) + text.setPos(surface[0]+surface[2]/2, surface[1]+surface[3]/2) + text.setZValue(10) + self.walls.append(rect) + self.texts.append(text) + + for name, plot in self.plots.items(): + if name in 'cm': + plot_surface(plot['widget'], mirror_surface_geometries('cm')) + elif name in 'mo1_1': + plot_surface(plot['widget'], mo_surface_geometries ('mo1', 0)) + elif name in 'mo1_2': + plot_surface(plot['widget'], mo_surface_geometries ('mo1', 1)) + elif name in 'fm': + plot_surface(plot['widget'], mirror_surface_geometries('fm_flat')) + plot_surface(plot['widget'], mirror_surface_geometries('fm_toroid')) + else: + raise Exception(f'Plot {name} not found!') + for name, plot in self.plots.items(): + plot['widget'].disableAutoRange() + + def update_surfaces(self, scene, data): + self.surfaces[scene] = data + for name, device in self.surfaces[scene].items(): + plot = self.plots[name][scene] + x = np.array(device['x'] + [device['x'][0]]) if len(device['x']) != 0 else np.array([]) + y = np.array(device['y'] + [device['y'][0]]) if len(device['y']) != 0 else np.array([]) + plot.setData(x=x, y=y) + +class SideviewPlot(QWidget): + """Plot widget with two curves and legend.""" + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._layout = QVBoxLayout(self) + # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + + 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) + self.colors = [(255, 255, 0), (255, 0, 255)] + + self.data = { + 'assistant': {'x': [0, 1000, 2000], 'y': [0, 20, 30]}, + 'reality': {'x': [0, 1000, 2000], 'y': [0, 15, 50]}, + } + + self.plots = {} + + self.pipes = [] + self.walls = [] + + for idx, scene in enumerate(self.data.keys()): + if scene in "assistant": + 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', + [ + self.plot_widget, + ] + ) + + self.plot_widget.setLabel('left', 'Height [mm]') + self.plot_widget.setLabel('bottom', 'Distance [mm]') + self.plot_widget.setMouseEnabled(x=False, y=False) + self.plot_widget.setXRange(0, 25000, padding=0.1) + self.plot_widget.setYRange(-20, 120, padding=0.1) + self.plot_widget.setMenuEnabled(False) + self.plot_widget.hideButtons() + + self._layout.addWidget(self.plot_group) + self._layout.addStretch() + + self.plot_vacuum_pipes() + self.plot_walls() + + self.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + # Background + self.plot_widget.setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = self.plot_widget.getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + if theme == "light": + self.color_impenetrable = (30, 30, 30) + self.colors = [(79, 163, 224), (240, 128, 60)] + self.text_color = (255, 255, 255) + else: # dark theme + self.color_impenetrable = (180, 180, 180) + self.colors = [(26, 111, 173), (212, 83, 10)] + self.text_color = (0, 0, 0) + + 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.DotLine) + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3) + self.plots[scene].setPen(pen) + self.plots[scene].setBrush(brush) + + for wall in self.walls: + wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3)) + wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + + for pipe in self.pipes: + pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3)) + + def plot_vacuum_pipes(self): + pipes = pipe_geometries() + for pipe in pipes: + self.pipes.append(self.plot_widget.plot( + x=pipe['x'], + y=pipe['y'], + pen=pg.mkPen(color=self.color_impenetrable, width=2), + )) + + def plot_walls(self): + walls = wall_geometries() + for wall in walls: + rect = pg.QtWidgets.QGraphicsRectItem(*wall) # pylint: disable=E1101 + rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + self.plot_widget.addItem(rect) + self.walls.append(rect) + + def update_curves(self, scene, data): + self.data[scene] = data + plot = self.plots[scene] + plot.setData(x=self.data[scene]['x'], y=self.data[scene]['y']) + + +if __name__ == "__main__": + from bec_widgets.utils.bec_dispatcher import BECDispatcher + from bec_widgets.utils.colors import apply_theme + + app = QApplication(sys.argv) + apply_theme("light") + dispatcher = BECDispatcher(gui_id="digital_twin") + win = DigitalTwin() + win.show() + sys.exit(app.exec_()) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.pyproject b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.pyproject new file mode 100644 index 0000000..226dc5f --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.pyproject @@ -0,0 +1 @@ +{'files': ['digital_twin.py']} \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin_plugin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin_plugin.py new file mode 100644 index 0000000..2decf97 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin_plugin.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from bec_widgets.utils.bec_designer import designer_material_icon +from qtpy.QtDesigner import QDesignerCustomWidgetInterface +from qtpy.QtWidgets import QWidget + +from debye_bec.bec_widgets.widgets.digital_twin.digital_twin import DigitalTwin + +DOM_XML = """ + + + + +""" + + +class DigitalTwinPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + if parent is None: + return QWidget() + t = DigitalTwin(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "" + + def icon(self): + return designer_material_icon(DigitalTwin.ICON_NAME) + + def includeFile(self): + return "digital_twin" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "DigitalTwin" + + def toolTip(self): + return "DigitalTwin" + + def whatsThis(self): + return self.toolTip() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py new file mode 100644 index 0000000..7834a29 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/move_widget.py @@ -0,0 +1,511 @@ +import time +import random +import threading + +# import qtawesome as qta +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, + QDoubleSpinBox, QFrame, QWidget, QApplication +) +from qtpy.QtGui import QTransform + +logger = bec_logger.logger + +class Status: + IN_POSITION = "in_position" # green mdi.check-circle + NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle + MOVING = "moving" # blue mdi.loading (spinning) + ERROR = "error" # red mdi.alert-circle + +class StatusIcon(QWidget): + """ + Displays a status icon using bec_qthemes Material Design Icons. + Handles its own spin animation for the MOVING state via QPropertyAnimation. + """ + + ICON_SIZE = 20 + + _ICON_MAP = { + Status.IN_POSITION: ("check_circle", "#27ae60"), + Status.NOT_IN_POSITION: ("cancel", "#e6d922"), + Status.ERROR: ("warning", "#e74c3c"), + Status.MOVING: ("cycle", "#2980b9"), + } + + def __init__(self, parent=None): + super().__init__(parent=parent) + self._status = None + self._rotation = 0.0 + + self._label = QLabel(self) + self._label.setFixedSize(self.ICON_SIZE, self.ICON_SIZE) + self._label.setAlignment(Qt.AlignCenter) + self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE) + + self._spin_anim = QPropertyAnimation(self, b"rotation") + self._spin_anim.setStartValue(0) + self._spin_anim.setEndValue(360) + self._spin_anim.setDuration(1000) + self._spin_anim.setLoopCount(-1) # Loop indefinitely + + self.set_status(Status.NOT_IN_POSITION) + + def get_rotation(self): + return self._rotation + + def set_rotation(self, angle): + self._rotation = angle + if self._current_pixmap_base is not None: + cx = self._current_pixmap_base.width() / 2 + cy = self._current_pixmap_base.height() / 2 + t = QTransform().translate(cx, cy).rotate(angle).translate(-cx, -cy) + self._label.setPixmap(self._current_pixmap_base.transformed(t, Qt.SmoothTransformation)) + + rotation = Property(float, get_rotation, set_rotation) + + def set_status(self, status: str): + if status == self._status: + return + self._status = status + + icon_name, color = self._ICON_MAP[status] + icon = material_icon(icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True) + self._current_pixmap_base = icon + + if status == Status.MOVING: + self._spin_anim.start() + else: + self._spin_anim.stop() + self._label.setPixmap(icon) + +class MotionWorker(QObject): + """ + 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, dev, motor, target_pos: float): + super().__init__() + self.dev = dev + self.motor = motor + self._target = target_pos + self._stop_flag = threading.Event() + + 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): + match self.motor: + 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(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].stop() + self._stop_flag.clear() + if rb is not None: + self.position_changed.emit(rb['device'].read(cached=True)[rb['name']]['value']) + else: + 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(cached=True)[surv_ax['name']]['value'] + if abs(fb - surv_ax['old_value']) > surv_ax['abs_tol']: + self.dev[self.motor].stop() + self.error.emit(1) + break + time.sleep(0.1) + self.finished.emit(True) + +class MoveWidget(QWidget): + """ + One motor stage control group containing: + - Target label (target position) + - Feedback label (current position) + - Status icon (bec_qthemes) + - Start / Stop button + """ + + 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._thread: QThread | None = None + self._worker: MotionWorker | None = None + + self.text_color = (0, 0, 0) + + self.unit = unit + self.decimals = decimals + + 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) + + # Target + self.target_label = QLabel('-') + self.target_label.setFixedWidth(100) + layout.addWidget(self.target_label) + + # Feedback + self.fb_label = QLabel('-') + self.fb_label.setFixedWidth(100) + layout.addWidget(self.fb_label) + + # Status icon + self.status_icon = StatusIcon() + self.status_icon.setFixedWidth(30) + self.status_icon.setContentsMargins(0, 0, 10, 0) + layout.addWidget(self.status_icon) + + # Start / Stop button + self.btn_action = QPushButton("Move") + self.btn_action.setFixedWidth(90) + self.btn_action.setFixedHeight(20) + self.btn_action.clicked.connect(self._on_button_clicked) + layout.addWidget(self.btn_action) + self.btn_mode = 'start' + + self._apply_button_style("start") + + self.apply_theme() + + def apply_theme(self, theme=None): + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + if theme == "light": + self.text_color = {'target': (79, 163, 224), 'fb': (240, 128, 60)} + else: # dark theme + self.text_color = {'target': (26, 111, 173), 'fb': (212, 83, 10)} + r, g, b = self.text_color['target'] + self.target_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}') + r, g, b = self.text_color['fb'] + self.fb_label.setStyleSheet(f'QLabel {{color: rgb({r}, {g}, {b})}}') + + if self.btn_mode == 'start': + self.btn_action.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().success.name()}; color: white;}}" + ) + else: + self.btn_action.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}" + ) + + def set_target(self, target): + self.target = target + text = f'{target:.{int(self.decimals)}f}' + if self.unit is not None: + text = text + ' ' + self.unit + self.target_label.setText(text) + self._on_target_or_fb_changed() + + def set_feedback(self, fb): + if self.status != Status.MOVING: + self.fb = fb + text = f'{fb:.{int(self.decimals)}f}' + if self.unit is not None: + text = text + ' ' + self.unit + self.fb_label.setText(text) + self._on_target_or_fb_changed() + + def _apply_button_style(self, mode: str): + self.btn_mode = mode + if mode == "start": + 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.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}" + ) + + def _set_status(self, status: str): + self.status = status + self.status_icon.set_status(status) + + 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): + return + if abs(self.fb - self.target) <= self.deadband: + self._set_status(Status.IN_POSITION) + else: + self._set_status(Status.NOT_IN_POSITION) + + def _on_button_clicked(self): + if self._thread and self._thread.isRunning(): + self._stop_motion() + else: + self._start_motion() + + def _start_motion(self): + target = self.target + if abs(target - self.fb) <= self.deadband: + self._set_status(Status.IN_POSITION) + return + + self._set_status(Status.MOVING) + self._apply_button_style("stop") + + self._worker = MotionWorker(self.dev, self.motor, target) + self._thread = QThread() + self._worker.moveToThread(self._thread) + + self._thread.started.connect(self._worker.run) + self._worker.position_changed.connect(self._on_position_changed) + self._worker.error.connect(self._on_error) + self._worker.error.connect(self._thread.quit) + self._worker.finished.connect(self._on_motion_finished) + self._worker.finished.connect(self._thread.quit) + self._thread.finished.connect(self._cleanup_thread) + + self._thread.start() + + def _on_error(self): + self._set_status(Status.ERROR) + self._apply_button_style("start") + + def _stop_motion(self): + if self._worker: + self._worker.stop() + + def _on_position_changed(self, pos: float): + self.fb = pos + text = f'{pos:.{int(self.decimals)}f}' + if self.unit is not None: + text = text + ' ' + self.unit + self.fb_label.setText(text) + + def _on_motion_finished(self, reached: bool): + target = self.target + 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): + if self._thread: + self._thread.deleteLater() + self._thread = None + if self._worker: + self._worker.deleteLater() + self._worker = None + + 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() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/register_digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/register_digital_twin.py new file mode 100644 index 0000000..0c5d315 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/register_digital_twin.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from debye_bec.bec_widgets.widgets.digital_twin.digital_twin_plugin import DigitalTwinPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(DigitalTwinPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/debye_bec/bec_widgets/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/qt_widgets.py new file mode 100644 index 0000000..201e72f --- /dev/null +++ b/debye_bec/bec_widgets/widgets/qt_widgets.py @@ -0,0 +1,272 @@ + +from functools import partial +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QGroupBox, QComboBox, QApplication, QDoubleSpinBox +) +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) + self.layout = QVBoxLayout(self) # type: ignore + for widget in widgets: + self.layout.addWidget(widget) # type: ignore + +class NumberIndicator(QWidget): + def __init__(self, label='', unit=None, highlight=False, decimals=3): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 0, 0, 0) + layout.setSpacing(0) + self.label = QLabel(label) + self.label.setFixedWidth(140) + self.label.setContentsMargins(0, 0, 10, 0) + self.label.setWordWrap(True) + layout.addWidget(self.label) + self.val = QLabel('-') + self.val.setAlignment(Qt.AlignTop) # type: ignore + # self.val.setFixedWidth(140) + layout.addWidget(self.val) + self.unit = unit + self.highlight = highlight + self.decimals = decimals + self.number = 0 + if highlight: + font = QFont() + font.setBold(True) + font.setPointSize(14) + self.label.setFont(font) + self.val.setFont(font) + + def value(self) -> float: + return self.number + + def setLabel(self, label) -> None: + self.label.setText(label) + + def setValue(self, number): + self.number = number + text = f'{number:.{int(self.decimals)}f}' + if self.unit is not None: + text = text + ' ' + self.unit + self.val.setText(text) + +class InputNumberField(QWidget): + def __init__(self, identifier='', label='', unit=None, prefix=None, init=0.0, decimals=1, single_step=0.1, ll=-1e6, hl=1e6): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 0, 0, 0) + layout.setSpacing(0) + self.identifier = identifier + self.label = QLabel(label) + self.label.setFixedWidth(140) + self.label.setContentsMargins(0, 0, 10, 0) + self.label.setWordWrap(True) + layout.addWidget(self.label) + self.val = QDoubleSpinBox() + self.val.setRange(ll, hl) + self.val.setDecimals(decimals) + self.val.setSingleStep(single_step) + self.val.setValue(init) + if unit is not None: + self.val.setSuffix(' ' + unit) + if prefix is not None: + self.val.setPrefix(prefix + ' ') + # self.val.setFixedWidth(140) + layout.addWidget(self.val) + + def set_number(self, number): + self.val.setValue(number) + + def has_focus(self) -> bool: + return self.val.hasFocus() + + def value(self) -> float: + return self.val.value() + + def value_changed_connect(self, func): + """Connect a function to the Enter/Return key press.""" + self.val.valueChanged.connect( + partial(func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value()) + ) + +class ComboBox(QWidget): + def __init__(self, identifier='', label='', enums=[]): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 0, 0, 0) + layout.setSpacing(0) + self.identifier = identifier + self.label = QLabel(label) + self.label.setFixedWidth(140) + self.label.setContentsMargins(0, 0, 10, 0) + self.label.setWordWrap(True) + layout.addWidget(self.label) + self.value = QComboBox() + for entry in enums: + self.value.addItem(entry) + layout.addWidget(self.value) + + def set_current_text(self, text): + self.value.setCurrentText(text) + + def currentText(self) -> str: + return self.value.currentText() + + def has_focus(self) -> bool: + return QApplication.focusWidget() is self.value.view() + + def activated_connect(self, func): + """Connect a function to the Enter/Return key press.""" + self.value.activated.connect( + partial(func, identifier=self.identifier, value_obj=self.value, value=lambda: self.value.currentText()) + ) + + def setDisabled(self, disable): + self.value.setDisabled(disable) + +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 clicked_connect(self, func): + """Connect a function to the button press.""" + self.button.clicked.connect(func) + + 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 setText(self, text): + self.button.setText(text) + +# class TextIndicator(QWidget): +# def __init__(self, label, unit=None, highlight=False): +# super().__init__() +# layout = QHBoxLayout(self) +# layout.setContentsMargins(10, 0, 0, 0) +# layout.setSpacing(0) +# self.label = QLabel(label) +# self.label.setFixedWidth(150) +# layout.addWidget(self.label) +# self.value = QLabel('-') +# self.value.setFixedWidth(160) +# layout.addWidget(self.value) +# self.unit = unit +# self.highlight = highlight +# if highlight: +# font = QFont() +# font.setBold(True) +# font.setPointSize(14) +# self.label.setFont(font) +# self.value.setFont(font) + +# def set_text(self, text): +# if self.unit is not None: +# text = text + ' ' + self.unit +# self.value.setText(text) + +# class Button(QWidget): +# def __init__(self, label, label_button): +# super().__init__() +# layout = QHBoxLayout(self) +# layout.setContentsMargins(10, 0, 0, 0) +# layout.setSpacing(0) +# self.label = QLabel(label) +# self.label.setFixedWidth(150) +# layout.addWidget(self.label) +# self.button = QPushButton(label_button) +# self.button.setStyleSheet("color: black; background-color: dodgerblue;") +# self.button.setFixedWidth(160) +# layout.addWidget(self.button) + +# def set_on_press(self, func): +# """Connect a function to the button press.""" +# self.button.clicked.connect(func) + +# def enable_button(self): +# self.button.setEnabled(True) +# self.button.setStyleSheet("color: black; background-color: dodgerblue;") + +# def disable_button(self): +# self.button.setEnabled(False) +# self.button.setStyleSheet("color: black; background-color: grey;") + +# def set_button_text(self, text): +# self.button.setText(text) + +# class LED(QWidget): +# def __init__(self, states, colors, label): +# super().__init__() +# self.states = states +# self.colors = colors +# layout = QHBoxLayout(self) +# layout.setContentsMargins(10, 0, 0, 0) +# layout.setSpacing(0) +# self.label = QLabel(label) +# self.label.setFixedWidth(150) +# layout.addWidget(self.label) +# self.led = QLabel() +# self.led.setFixedWidth(160) +# layout.addWidget(self.led) + +# def apply_color(self, val): +# color = self.colors[self.states.index(val)] +# self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;") + +# class InputTextField(QWidget): +# def __init__(self, topic, label): +# super().__init__() +# self.topic = topic +# layout = QHBoxLayout(self) +# layout.setContentsMargins(10, 0, 0, 0) +# layout.setSpacing(0) +# self.label = QLabel(label) +# self.label.setFixedWidth(140) +# self.label.setContentsMargins(0, 0, 10, 0) +# self.label.setWordWrap(True) +# layout.addWidget(self.label) +# self.val = QLineEdit() +# self.val.setPlaceholderText('0') +# # self.val.setFixedWidth(140) +# layout.addWidget(self.val) + +# def set_text(self, text): +# self.val.setText(text) + +# def has_focus(self) -> bool: +# return self.val.hasFocus() + +# def text(self) -> str: +# return self.val.text() + +# def set_on_return(self, func): +# """Connect a function to the Enter/Return key press.""" +# self.val.returnPressed.connect( +# partial(func, self.val, self.topic, lambda: self.val.text()) +# ) diff --git a/debye_bec/bec_widgets/widgets/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 diff --git a/debye_bec/bec_widgets/widgets/x01da_parameters.py b/debye_bec/bec_widgets/widgets/x01da_parameters.py new file mode 100644 index 0000000..8a97e5a --- /dev/null +++ b/debye_bec/bec_widgets/widgets/x01da_parameters.py @@ -0,0 +1,311 @@ +""" +X01DA / Debye Beamline Parameters. +This file describes the parameter of each component of the Debye beamline +to be used for raytracing and geometrical calculations. +""" + +import os +import numpy as np +from collections import namedtuple + +import xrt.backends.raycing.materials as rm + +# if os.environ.get("USE_XRT", "True").lower() in ("1", "true", "yes"): +# import xrt.backends.raycing.materials as rm # type: ignore +# else: +# class _DummyClass: +# def __init__(self, *args, **kwargs): +# pass +# class _DummyMaterials: +# Material = _DummyClass +# CrystalSi = _DummyClass +# rm = _DummyMaterials() + +# XRT definitions +filterBeryl = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType] +filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType] + +stripeSi = rm.Material('Si', rho=2.33) # pyright: ignore[reportArgumentType] +stripePt = rm.Material('Pt', rho=21.45) # pyright: ignore[reportArgumentType] +stripeRh = rm.Material('Rh', rho=12.41) # pyright: ignore[reportArgumentType] +stripeCr = rm.Material('Cr', rho=7.14) # pyright: ignore[reportArgumentType] +stripePyrex = rm.Material('Si', rho=2.20) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType] + +si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface +si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface +si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface +si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface +si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface +si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface +si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface +si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface + +filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType] +filterBe = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType] +filterSi3N4 = rm.Material(['Si', 'N'], quantities=[3, 4], rho=3.44, kind='plate') # pyright: ignore[reportArgumentType] +filterAl = rm.Material('Al', rho=2.69, kind='plate') # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType] + +# General parameters +sourceHeight = 0 + +#Synchrotron +synchrotron = namedtuple('synchrotron', ['eE', 'eI', 'eEspread', + 'eEpsilonX', 'eEpsilonZ', 'betaX', 'betaZ']) + +sls1 = synchrotron( + eE = 2.4, + eI = 0.4, + eEspread=0.878e-3, + eEpsilonX=5.63, + eEpsilonZ=0.007, + betaX=0.45, + betaZ=14.4, + ) + +sls2 = synchrotron( + eE=2.7, + eI=0.4, + eEspread=1.147e-3, + eEpsilonX=0.156, + eEpsilonZ=0.01, + betaX=0.18, + betaZ=4.6, + ) + +# Source +bendingMagnet = namedtuple('bendingMagnet', ['name', 'center', 'sync', 'B0']) + +sls1_14t = bendingMagnet( + name='FE-BM-SLS1-1.4T', + center=(0, 0, 0), + sync=sls1, + B0=1.4,) + +sls2_21t = bendingMagnet( + name='FE-BM-SLS2-2.1T', + center=(0, 0, 0), + sync=sls2, + B0=2.1,) + +sls2_35t = bendingMagnet( + name='FE-BM-SLS2-3.5T', + center=(0, 0, 0), + sync=sls2, + B0=3.5,) + +sls2_50t = bendingMagnet( + name='FE-BM-SLS2-5.0T', + center=(0, 0, 0), + sync=sls2, + B0=5.0,) + +# FE slits +fe_slits = namedtuple('slits', ['name', 'center', 'center1', 'center2', 'maxDivH', 'maxDivV']) + +feSlits = fe_slits( + name='FE-SLITS', + center=(0, 6117, sourceHeight), + center1=(0, 5045, sourceHeight), + center2=(0, 5289.5, sourceHeight), + maxDivH=1.8e-3, + maxDivV=0.8e-3,) + +# FE Window +filt = namedtuple('filt', ['name', 'center', 'pitch', 'limPhysX', 'limPhysY', 'surface', 'material', 'thickness']) + +feWindow = filt( + name='FE-WINDOW', + center=(0., 7020, sourceHeight), + pitch=np.pi/2, + limPhysX=(-6, 6), + limPhysY=(-3., 3.), + surface='None', + material=filterDiamond, + thickness=0.1,) +feWindow = feWindow._replace(surface=f'CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m') + +# Collimating mirror +collimatingMirror = namedtuple('collimatingMirror', ['name', + 'center', 'surface', 'material', 'limPhysX', 'limPhysY', + 'limOptX', 'limOptY', 'R', 'pitch', 'jack1', 'jack2', 'jack3', + 'tx1', 'tx2']) + +cm = collimatingMirror( + name='FE-CM', + center=[0, 6890, sourceHeight], + surface=('Si','Pt','Rh'), + material=(stripeSi, stripePt, stripeRh), + limPhysX=(-34, 34), + limPhysY=(-600, 600), + limOptX=((-21, -7, 14), (-11, 11, 23)), + limOptY=((-500, -500, -500), (500, 500, 500)), + R=[3e6, 15e6], + pitch=[-5.0e-3, -0.0e-3], + jack1=[0., 7210., 0.], #Tripod X, Y, Z (global) + jack2=[-210., 8310., 0.], + jack3=[210., 8310., 0.], + tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local) + tx2=[0.0, 575],) # X-Stage 2 + +apertures = namedtuple('apertures', ['name', 'center', 'opening']) + +fePS = apertures( + name='FE-PS', + center=[0, 8815, sourceHeight], + opening=[-20., 20., -20.+12.5, 20.+12.5]) # left, right, bottom, top + +opWbBsBlock = apertures( + name='OP-WB-BS-BLOCK', + center=[0., 13860, sourceHeight], + opening=[-18., 18., 25, 85.5]) # left, right, bottom, top + # opening=[-18., 18., 42, 76], # X10DA + +# Monochromator +monochromator = namedtuple('monochromator', ['name', 'center', + 'xtal', 'material1', 'material2', 'xtalWidth', 'xtalOffsetX', + 'xtalLength1', 'xtalLength2', 'xtalGap', 'rotOffset', + 'heightOffset', 'braggLim', 'jack1', 'jack2', 'jack3', 'tx']) + +mo1 = monochromator( + name='OP-MO1', + center=[0., 11750, sourceHeight], + xtal=('Si311','Si111'), + material1=(si311_1, si111_1), + material2=(si311_2, si111_2), + xtalWidth = (24, 24), + xtalOffsetX=(-21.2, 21.2), + xtalLength1 = (55, 55), + xtalLength2 = (105, 105), + xtalGap = (8, 8), + rotOffset = 6, + heightOffset = 8.5, + braggLim = [3.6, 33], + jack1=[0., 11350., 0.], #Tripod maybe not available! + jack2=[-400., 12350., 0.], + jack3=[400., 12350., 0.], + tx=0.0,) # X-Stage [x] + +mo2 = monochromator( + name='OP-CCM2', + center=[0., 13250, sourceHeight], + xtal=('Si311','Si111'), + material1=(si311_1, si111_1), + material2=(si311_2, si111_2), + xtalWidth = (24, 24), + xtalOffsetX=(-21, 21), + xtalLength1 = (55, 55), + xtalLength2 = (105, 105), + xtalGap = (8, 8), + rotOffset = 6, + heightOffset = 8.5, + braggLim = [3.6, 33], + jack1=[0., 13350., 0.], #Tripod maybe not available! + jack2=[-400., 14350., 0.], + jack3=[400., 14350., 0.], + tx=0.0,) # X-Stage [x] + +# OP Slits +op_slits = namedtuple('op_slits', ['name', 'center']) + +opSlits1 = op_slits( + name='OP-SLITS 1', + center=(0, 14349.6, sourceHeight), +) + +opSlits2 = op_slits( + name='OP-SLITS 2', + center=(0, 18134.8, sourceHeight), +) + +# OP Beam Monitors +op_bm = namedtuple('op_bm', ['name', 'center']) + +opBM1 = op_bm( + name='OP Beam Monitor 1', + center=(0, 14599.6, sourceHeight), +) + +opBM2 = op_bm( + name='OP Beam Monitor 2', + center=(0, 18384.8, sourceHeight), +) + +# Focusing mirror +focusingMirror = namedtuple('focusingMirror', ['name', 'center', + 'surfaceToroid', 'materialToroid', 'surfaceFlat', 'materialFlat', + 'limPhysXToroid', 'limPhysYToroid', 'limPhysXFlat', 'limPhysYFlat', + 'limOptXToroid', 'limOptYToroid', 'limOptXFlat', 'limOptYFlat', + 'R', 'pitch', 'r', 'xToroid', 'xFlat', 'hToroid', 'jack1', 'jack2', 'jack3', + 'tx1', 'tx2']) + +fm = focusingMirror( + name='OP-FM', + center=[0., 15670, sourceHeight], # nominal height 58 mm above ring, SLS1! + surfaceToroid=('Rh', 'Pt'), + materialToroid=(stripeRh, stripePt), + surfaceFlat=('Rh', 'Pt'), + materialFlat=(stripeRh, stripePt), + limPhysXToroid=(-79., 79.), + limPhysYToroid=(-575., 575.), + limPhysXFlat=(-79., 79.), + limPhysYFlat=(-575., 575.), + limOptXToroid=((-38, 66), (-66, 31)), + limOptYToroid=((-500., -500.), (500., 500.)), + limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)), + limOptYFlat=((-500., -500.), (500., 500.)), + R=[3e6, 15e6], + pitch=[-5.0e-3, 0e-3], + r=[35.510, 24.986], + xToroid=[-52, 48.5], # offset in local x + xFlat = [-20.95, 8.55], + hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. + jack1=[-130., 15535-538., 0.], + jack2=[130., 15535+538., 0.], + jack3=[0., 15535+538., 0.], + tx1=[0., -575.], # X-Stage 1 [x, y] + tx2=[0., 575.],) # X-Stage 2 [x, y] + +# EH Window +ehWindow = filt( + name='EH-WINDOW', + center=(0., 19998.3, sourceHeight), + pitch=np.pi/2, + limPhysX=(-20., 20.), + limPhysY=(-4, 4), + surface='None', + material=filterSi3N4, + thickness=0.002,) +ehWindow = ehWindow._replace(surface=f'Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m') + +# Sample +sample = namedtuple('sample', ['name', 'center']) + +smpl = sample( + name='EH-SMPL', + center=[0, 23365, sourceHeight],) + +smpl2 = sample( + name='EH-SMPL2', + center=[0, 27500, sourceHeight],) + +# Vacuum pipes +# DN40CF ID = 35 mm oder 37 mm +# DN50CF ID = 47.5 mm +# DN63CF ID = 60.2 mm oder 66 mm +# DN100CF ID = 97.4 mm oder 104 mm +pipe = namedtuple('pipes', ['center', 'diameter', 'start', 'end']) +vacuum_pipes = pipe( + center= [27.5, (37.5+27.5)/2, 37.5, 62.5, 72.5], + diameter=[97.4, 97.4, 97.4, 97.4, 97.4], + start= [10952.88, 11750+250, mo2.center[1]+250, 14000, fm.center[1]], + end= [11750-250, mo2.center[1]-250, 14000, fm.center[1], ehWindow.center[1]], +) + +Walls = namedtuple('walls', ['start', 'end', 'height']) +walls = Walls( + start= [13999.30], + end= [13999+75.5+30], + height= [[-20, 25]], +) diff --git a/debye_bec/device_configs/x01da_experimental_hutch.yaml b/debye_bec/device_configs/x01da_experimental_hutch.yaml index f14d1aa..f2c241e 100644 --- a/debye_bec/device_configs/x01da_experimental_hutch.yaml +++ b/debye_bec/device_configs/x01da_experimental_hutch.yaml @@ -5,7 +5,7 @@ ot_tryu: readoutPriority: baseline description: Optical Table Y-Translation Upstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES-OT:TRYU onFailure: retry @@ -15,7 +15,7 @@ ot_tryu: ot_tryd: readoutPriority: baseline description: Optical Table Y-Translation Downstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES-OT:TRYD onFailure: retry @@ -25,7 +25,7 @@ ot_tryd: ot_es1_trz: readoutPriority: baseline description: Optical Table ES1 Z-Translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-OT:TRZ onFailure: retry @@ -35,7 +35,7 @@ ot_es1_trz: ot_es2_trz: readoutPriority: baseline description: Optical Table ES2 Z-Translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-OT:TRZ onFailure: retry @@ -45,17 +45,17 @@ ot_es2_trz: ot_try: readoutPriority: baseline description: Optical Table Y-Translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES-OT:TRY onFailure: retry enabled: true softwareTrigger: false -ot_pitch: +ot_rotx: readoutPriority: baseline description: Optical Table Pitch - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES-OT:ROTX onFailure: retry @@ -69,7 +69,7 @@ ot_pitch: es0wi_try: readoutPriority: baseline description: End Station 0 Exit Window Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-WI:TRY onFailure: retry @@ -97,7 +97,7 @@ es0filter: es0sl_trxr: readoutPriority: baseline description: End Station slits X-translation Ring-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:TRXR onFailure: retry @@ -107,7 +107,7 @@ es0sl_trxr: es0sl_trxw: readoutPriority: baseline description: End Station slits X-translation Wall-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:TRXW onFailure: retry @@ -117,7 +117,7 @@ es0sl_trxw: es0sl_tryb: readoutPriority: baseline description: End Station slits Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:TRYB onFailure: retry @@ -127,7 +127,7 @@ es0sl_tryb: es0sl_tryt: readoutPriority: baseline description: End Station slits X-translation Top-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:TRYT onFailure: retry @@ -137,7 +137,7 @@ es0sl_tryt: es0sl_center: readoutPriority: baseline description: End Station slits X-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:CENTERX onFailure: retry @@ -147,7 +147,7 @@ es0sl_center: es0sl_gapx: readoutPriority: baseline description: End Station slits X-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:GAPX onFailure: retry @@ -157,7 +157,7 @@ es0sl_gapx: es0sl_centery: readoutPriority: baseline description: End Station slits Y-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:CENTERY onFailure: retry @@ -167,7 +167,7 @@ es0sl_centery: es0sl_gapy: readoutPriority: baseline description: End Station slits Y-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES0-SL:GAPY onFailure: retry @@ -195,7 +195,7 @@ es1_alignment_laser: es1man_trx: readoutPriority: baseline description: End Station sample manipulator X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-MAN1:TRX onFailure: retry @@ -205,7 +205,7 @@ es1man_trx: es1man_try: readoutPriority: baseline description: End Station sample manipulator Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-MAN1:TRY onFailure: retry @@ -215,7 +215,7 @@ es1man_try: es1man_trz: readoutPriority: baseline description: End Station sample manipulator Z-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-MAN1:TRZ onFailure: retry @@ -225,7 +225,7 @@ es1man_trz: es1man_roty: readoutPriority: baseline description: End Station sample manipulator Y-rotation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-MAN1:ROTY onFailure: retry @@ -239,7 +239,7 @@ es1man_roty: es1arc_roty: readoutPriority: baseline description: End Station segmented arc Y-rotation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-ARC:ROTY onFailure: retry @@ -249,7 +249,7 @@ es1arc_roty: es1det1_trx: readoutPriority: baseline description: End Station SDD 1 X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-DET1:TRX onFailure: retry @@ -259,7 +259,7 @@ es1det1_trx: es1bm1_trx: readoutPriority: baseline description: End Station X-ray Eye X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-BM1:TRX onFailure: retry @@ -269,7 +269,7 @@ es1bm1_trx: es1det2_trx: readoutPriority: baseline description: End Station SDD 2 X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-DET2:TRX onFailure: retry @@ -283,7 +283,7 @@ es1det2_trx: es2ma2_try: readoutPriority: baseline description: End Station ionization chamber 1+2 Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-MA2:TRY onFailure: retry @@ -293,7 +293,7 @@ es2ma2_try: es2ma2_trz: readoutPriority: baseline description: End Station ionization chamber 1+2 Z-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-MA2:TRZ onFailure: retry @@ -307,7 +307,7 @@ es2ma2_trz: es2ma3_try: readoutPriority: baseline description: End Station XRD detector Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-MA3:TRY onFailure: retry @@ -386,4 +386,64 @@ es_light_toggle: read_pv: "X01DA-EH-LIGHT:TOGGLE" onFailure: retry enabled: true + softwareTrigger: false + +es_gas_sensor_o2: + readoutPriority: baseline + description: ES Gas Sensor O2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-O2" + onFailure: retry + enabled: true + softwareTrigger: false + +es_gas_sensor_h2s: + readoutPriority: baseline + description: ES Gas Sensor H2S + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-H2S" + onFailure: retry + enabled: true + softwareTrigger: false + +es_gas_sensor_no2: + readoutPriority: baseline + description: ES Gas Sensor NO2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-NO2" + onFailure: retry + enabled: true + softwareTrigger: false + +es_gas_sensor_co: + readoutPriority: baseline + description: ES Gas Sensor CO + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-CO" + onFailure: retry + enabled: true + softwareTrigger: false + +es_gas_sensor_h2: + readoutPriority: baseline + description: ES Gas Sensor H2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-H2" + onFailure: retry + enabled: true + softwareTrigger: false + +es_gas_sensor_nh3: + readoutPriority: baseline + description: ES Gas Sensor NH3 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-KIMESSA2:EH-NH3" + onFailure: retry + enabled: true softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_frontend.yaml b/debye_bec/device_configs/x01da_frontend.yaml index bc097a3..3a9edb7 100644 --- a/debye_bec/device_configs/x01da_frontend.yaml +++ b/debye_bec/device_configs/x01da_frontend.yaml @@ -1,4 +1,18 @@ +################################### +## Frontend Absorber ## +################################### + +abs: + readoutPriority: baseline + description: Frontend Absorber + deviceClass: debye_bec.devices.absorber.Absorber + deviceConfig: + prefix: "X01DA-FE-ABS1:" + onFailure: retry + enabled: true + softwareTrigger: false + ################################### ## Frontend Slits ## ################################### @@ -6,7 +20,7 @@ sldi_trxr: readoutPriority: baseline description: Front-end slit diaphragm X-translation Ring-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:TRXR onFailure: retry @@ -16,7 +30,7 @@ sldi_trxr: sldi_trxw: readoutPriority: baseline description: Front-end slit diaphragm X-translation Wall-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:TRXW onFailure: retry @@ -26,7 +40,7 @@ sldi_trxw: sldi_tryb: readoutPriority: baseline description: Front-end slit diaphragm Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:TRYB onFailure: retry @@ -36,7 +50,7 @@ sldi_tryb: sldi_tryt: readoutPriority: baseline description: Front-end slit diaphragm X-translation Top-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:TRYT onFailure: retry @@ -46,7 +60,7 @@ sldi_tryt: sldi_centerx: readoutPriority: baseline description: Front-end slit diaphragm X-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:CENTERX onFailure: retry @@ -56,7 +70,7 @@ sldi_centerx: sldi_gapx: readoutPriority: baseline description: Front-end slit diaphragm X-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:GAPX onFailure: retry @@ -66,7 +80,7 @@ sldi_gapx: sldi_centery: readoutPriority: baseline description: Front-end slit diaphragm Y-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:CENTERY onFailure: retry @@ -76,7 +90,7 @@ sldi_centery: sldi_gapy: readoutPriority: baseline description: Front-end slit diaphragm Y-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-SLDI:GAPY onFailure: retry @@ -90,7 +104,7 @@ sldi_gapy: cm_trxu: readoutPriority: baseline description: Collimating Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:TRXU onFailure: retry @@ -100,7 +114,7 @@ cm_trxu: cm_trxd: readoutPriority: baseline description: Collimating Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:TRXD onFailure: retry @@ -110,7 +124,7 @@ cm_trxd: cm_tryu: readoutPriority: baseline description: Collimating Mirror Y-translation upstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:TRYU onFailure: retry @@ -120,7 +134,7 @@ cm_tryu: cm_trydr: readoutPriority: baseline description: Collimating Mirror Y-translation downstream ring - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:TRYDR onFailure: retry @@ -130,7 +144,7 @@ cm_trydr: cm_trydw: readoutPriority: baseline description: Collimating Mirror Y-translation downstream wall - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:TRYDW onFailure: retry @@ -140,17 +154,28 @@ cm_trydw: cm_bnd: readoutPriority: baseline description: Collimating Mirror bender - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:BND onFailure: retry enabled: true softwareTrigger: false +cm_bnd_radius: + readoutPriority: baseline + description: Collimating Mirror Bending Radius + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: X01DA-CPCL-CM:BNDFORCE + onFailure: retry + readOnly: true + enabled: true + softwareTrigger: false + cm_rotx: readoutPriority: baseline description: Collimating Morror Pitch - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:ROTX onFailure: retry @@ -160,7 +185,7 @@ cm_rotx: cm_roty: readoutPriority: baseline description: Collimating Morror Yaw - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:ROTY onFailure: retry @@ -170,7 +195,7 @@ cm_roty: cm_rotz: readoutPriority: baseline description: Collimating Morror Roll - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:ROTZ onFailure: retry @@ -180,7 +205,7 @@ cm_rotz: cm_trx: readoutPriority: baseline description: Collimating Morror Center Point X - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:XTCP onFailure: retry @@ -190,7 +215,7 @@ cm_trx: cm_try: readoutPriority: baseline description: Collimating Morror Center Point Y - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:YTCP onFailure: retry @@ -200,7 +225,7 @@ cm_try: cm_ztcp: readoutPriority: baseline description: Collimating Morror Center Point Z - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:ZTCP onFailure: retry @@ -210,7 +235,7 @@ cm_ztcp: cm_xstripe: readoutPriority: baseline description: Collimating Morror X Stripe - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-FE-CM:XSTRIPE onFailure: retry diff --git a/debye_bec/device_configs/x01da_hutch_cameras.yaml b/debye_bec/device_configs/x01da_hutch_cameras.yaml new file mode 100644 index 0000000..63efbc5 --- /dev/null +++ b/debye_bec/device_configs/x01da_hutch_cameras.yaml @@ -0,0 +1,34 @@ + +################################### +## Hutch Cameras ## +################################### + +hutch_cam_1: + readoutPriority: baseline + description: Hutch Camera 1 + deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam + deviceConfig: + prefix: "pcp085420" + onFailure: retry + enabled: true + softwareTrigger: false + +hutch_cam_2: + readoutPriority: baseline + description: Hutch Camera 2 + deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam + deviceConfig: + prefix: "pcp085436" + onFailure: retry + enabled: true + softwareTrigger: false + +hutch_cam_3: + readoutPriority: baseline + description: Hutch Camera 3 + deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam + deviceConfig: + prefix: "pcp085435" + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_optics.yaml b/debye_bec/device_configs/x01da_optics.yaml index 6d77341..a168e92 100644 --- a/debye_bec/device_configs/x01da_optics.yaml +++ b/debye_bec/device_configs/x01da_optics.yaml @@ -3,30 +3,30 @@ ## Monochromator ## ################################### -mo_try: +mo1_try: readoutPriority: baseline description: Monochromator Y Translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-MO1:TRY onFailure: retry enabled: true softwareTrigger: false -mo_trx: +mo1_trx: readoutPriority: baseline description: Monochromator X Translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-MO1:TRX onFailure: retry enabled: true softwareTrigger: false -mo_roty: +mo1_roty: readoutPriority: baseline description: Monochromator Yaw - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-MO1:ROTY onFailure: retry @@ -40,7 +40,7 @@ mo_roty: sl1_trxr: readoutPriority: baseline description: Optics slits 1 X-translation Ring-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:TRXR onFailure: retry @@ -53,7 +53,7 @@ sl1_trxr: sl1_trxw: readoutPriority: baseline description: Optics slits 1 X-translation Wall-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:TRXW onFailure: retry @@ -66,7 +66,7 @@ sl1_trxw: sl1_tryb: readoutPriority: baseline description: Optics slits 1 Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:TRYB onFailure: retry @@ -79,7 +79,7 @@ sl1_tryb: sl1_tryt: readoutPriority: baseline description: Optics slits 1 X-translation Top-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:TRYT onFailure: retry @@ -92,7 +92,7 @@ sl1_tryt: bm1_try: readoutPriority: baseline description: Beam Monitor 1 Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-BM1:TRY onFailure: retry @@ -105,7 +105,7 @@ bm1_try: sl1_centerx: readoutPriority: baseline description: Optics slits 1 X-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:CENTERX onFailure: retry @@ -118,7 +118,7 @@ sl1_centerx: sl1_gapx: readoutPriority: baseline description: Optics slits 1 X-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:GAPX onFailure: retry @@ -131,7 +131,7 @@ sl1_gapx: sl1_centery: readoutPriority: baseline description: Optics slits 1 Y-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:CENTERY onFailure: retry @@ -144,7 +144,7 @@ sl1_centery: sl1_gapy: readoutPriority: baseline description: Optics slits 1 Y-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL1:GAPY onFailure: retry @@ -161,62 +161,78 @@ sl1_gapy: fm_trxu: readoutPriority: baseline description: Focusing Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:TRXU onFailure: retry enabled: true softwareTrigger: false + fm_trxd: readoutPriority: baseline description: Focusing Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:TRXD onFailure: retry enabled: true softwareTrigger: false + fm_tryd: readoutPriority: baseline description: Focusing Mirror Y-translation downstream - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:TRYD onFailure: retry enabled: true softwareTrigger: false + fm_tryur: readoutPriority: baseline description: Focusing Mirror Y-translation upstream ring - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:TRYUR onFailure: retry enabled: true softwareTrigger: false + fm_tryuw: readoutPriority: baseline description: Focusing Mirror Y-translation upstream wall - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:TRYUW onFailure: retry enabled: true softwareTrigger: false + fm_bnd: readoutPriority: baseline description: Focusing Mirror bender - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:BND onFailure: retry enabled: true softwareTrigger: false +fm_bnd_radius: + readoutPriority: baseline + description: Focusing Mirror Bending Radius + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: X01DA-CPCL-FM:BNDFORCE + onFailure: retry + readOnly: true + enabled: true + softwareTrigger: false + fm_rotx: readoutPriority: baseline description: Focusing Morror Pitch - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:ROTX onFailure: retry @@ -226,7 +242,7 @@ fm_rotx: fm_roty: readoutPriority: baseline description: Focusing Morror Yaw - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:ROTY onFailure: retry @@ -236,27 +252,27 @@ fm_roty: fm_rotz: readoutPriority: baseline description: Focusing Morror Roll - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:ROTZ onFailure: retry enabled: true softwareTrigger: false -fm_xctp: +fm_trx: readoutPriority: baseline description: Focusing Morror Center Point X - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:XTCP onFailure: retry enabled: true softwareTrigger: false -fm_ytcp: +fm_try: readoutPriority: baseline description: Focusing Morror Center Point Y - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:YTCP onFailure: retry @@ -266,7 +282,7 @@ fm_ytcp: fm_ztcp: readoutPriority: baseline description: Focusing Morror Center Point Z - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-FM:ZTCP onFailure: retry @@ -280,7 +296,7 @@ fm_ztcp: sl2_trxr: readoutPriority: baseline description: Optics slits 2 X-translation Ring-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:TRXR onFailure: retry @@ -293,7 +309,7 @@ sl2_trxr: sl2_trxw: readoutPriority: baseline description: Optics slits 2 X-translation Wall-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:TRXW onFailure: retry @@ -306,7 +322,7 @@ sl2_trxw: sl2_tryb: readoutPriority: baseline description: Optics slits 2 Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:TRYB onFailure: retry @@ -319,7 +335,7 @@ sl2_tryb: sl2_tryt: readoutPriority: baseline description: Optics slits 2 X-translation Top-edge - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:TRYT onFailure: retry @@ -332,7 +348,7 @@ sl2_tryt: bm2_try: readoutPriority: baseline description: Beam Monitor 2 Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-BM2:TRY onFailure: retry @@ -345,7 +361,7 @@ bm2_try: sl2_centerx: readoutPriority: baseline description: Optics slits 2 X-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:CENTERX onFailure: retry @@ -358,7 +374,7 @@ sl2_centerx: sl2_gapx: readoutPriority: baseline description: Optics slits 2 X-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:GAPX onFailure: retry @@ -371,7 +387,7 @@ sl2_gapx: sl2_centery: readoutPriority: baseline description: Optics slits 2 Y-center - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:CENTERY onFailure: retry @@ -384,7 +400,7 @@ sl2_centery: sl2_gapy: readoutPriority: baseline description: Optics slits 2 Y-gap - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-OP-SL2:GAPY onFailure: retry diff --git a/debye_bec/device_configs/x01da_standard_config.yaml b/debye_bec/device_configs/x01da_standard_config.yaml index 5a0a701..a154a73 100644 --- a/debye_bec/device_configs/x01da_standard_config.yaml +++ b/debye_bec/device_configs/x01da_standard_config.yaml @@ -25,7 +25,7 @@ frontend_config: ## Bragg Monochromator mo1_bragg: - readoutPriority: monitored + readoutPriority: baseline description: Positioner for the Monochromator deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg deviceConfig: @@ -34,14 +34,14 @@ mo1_bragg: 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 + 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: @@ -51,7 +51,7 @@ optics_config: ## Experimental Hutch ## ################################### -## NIDAQ +# ## NIDAQ nidaq: readoutPriority: monitored description: NIDAQ backend for data reading for debye scans @@ -67,8 +67,13 @@ xas_config: - !include ./x01da_xas.yaml ## XRD (Pilatus, pinhole, beamstop) -xrd_config: - - !include ./x01da_xrd.yaml +#xrd_config: +# - !include ./x01da_xrd.yaml + +# Commented out because too slow +## Hutch cameras +# hutch_cams: +# - !include ./x01da_hutch_cameras.yaml ## Remaining experimental hutch es_config: diff --git a/debye_bec/device_configs/x01da_xas.yaml b/debye_bec/device_configs/x01da_xas.yaml index 42e2876..4fb696d 100644 --- a/debye_bec/device_configs/x01da_xas.yaml +++ b/debye_bec/device_configs/x01da_xas.yaml @@ -3,35 +3,45 @@ ## Ionization Chambers ## ################################### -# ic0: -# readoutPriority: baseline -# description: Ionization chamber 0 -# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 -# deviceConfig: -# prefix: "X01DA-" -# onFailure: retry -# enabled: true -# softwareTrigger: false +ic0: + readoutPriority: baseline + description: Ionization chamber 0 + deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 + deviceConfig: + prefix: "X01DA-" + onFailure: retry + enabled: true + softwareTrigger: false -# ic1: -# readoutPriority: baseline -# description: Ionization chamber 1 -# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 -# deviceConfig: -# prefix: "X01DA-" -# onFailure: retry -# enabled: true -# softwareTrigger: false +ic1: + readoutPriority: baseline + description: Ionization chamber 1 + deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 + deviceConfig: + prefix: "X01DA-" + onFailure: retry + enabled: true + softwareTrigger: false -# ic2: -# readoutPriority: baseline -# description: Ionization chamber 2 -# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 -# deviceConfig: -# prefix: "X01DA-" -# onFailure: retry -# enabled: true -# softwareTrigger: false +ic2: + readoutPriority: baseline + description: Ionization chamber 2 + deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 + deviceConfig: + prefix: "X01DA-" + onFailure: retry + enabled: true + softwareTrigger: false + +pips: + readoutPriority: baseline + description: Pips diode + deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.Pips + deviceConfig: + prefix: "X01DA-" + onFailure: retry + enabled: true + softwareTrigger: false ################################### ## Reference Foil Changer ## diff --git a/debye_bec/device_configs/x01da_xrd.yaml b/debye_bec/device_configs/x01da_xrd.yaml index 22ffdba..c363ff8 100644 --- a/debye_bec/device_configs/x01da_xrd.yaml +++ b/debye_bec/device_configs/x01da_xrd.yaml @@ -6,7 +6,7 @@ pin1_trx: readoutPriority: baseline description: Pinhole X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-PIN1:TRX onFailure: retry @@ -17,7 +17,7 @@ pin1_trx: pin1_try: readoutPriority: baseline description: Pinhole Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-PIN1:TRY onFailure: retry @@ -28,7 +28,7 @@ pin1_try: pin1_rotx: readoutPriority: baseline description: Pinhole X-rotation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-PIN1:ROTX onFailure: retry @@ -39,7 +39,7 @@ pin1_rotx: pin1_roty: readoutPriority: baseline description: Pinhole Y-rotation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES1-PIN1:ROTY onFailure: retry @@ -54,7 +54,7 @@ pin1_roty: es2bs_trx: readoutPriority: baseline description: End Station beamstop X-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-BS:TRX onFailure: retry @@ -64,7 +64,7 @@ es2bs_trx: es2bs_try: readoutPriority: baseline description: End Station beamstop Y-translation - deviceClass: ophyd.EpicsMotor + deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: prefix: X01DA-ES2-BS:TRY onFailure: retry @@ -86,7 +86,7 @@ pilatus_curtain: softwareTrigger: false pilatus: - readoutPriority: async + readoutPriority: baseline description: Pilatus deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus deviceTags: @@ -97,12 +97,12 @@ pilatus: enabled: true softwareTrigger: true -# sampl_pil: -# readoutPriority: baseline -# description: Sample to pilatus distance -# deviceClass: ophyd.EpicsSignalRO -# deviceConfig: -# read_pv: "X01DA-SAMPL-PIL" -# onFailure: retry -# enabled: true -# softwareTrigger: false +pilatus_smpl: + readoutPriority: baseline + description: Sample to pilatus distance + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-ES2-DET:SMPLDIST" + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/devices/absorber.py b/debye_bec/devices/absorber.py new file mode 100644 index 0000000..484054d --- /dev/null +++ b/debye_bec/devices/absorber.py @@ -0,0 +1,72 @@ +"""Frontend Absorber""" + +from __future__ import annotations + +import enum +from typing import TYPE_CHECKING + +from ophyd import Component as Cpt +from ophyd import EpicsSignal, EpicsSignalRO +from ophyd_devices import CompareStatus, DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + +if TYPE_CHECKING: + from bec_lib.devicemanager import ScanInfo + +class AbsorberError(Exception): + """Absorber specific exception""" + +class STATUS(int, enum.Enum): + """Absorber States""" + + MOVING_CLOSE = 0 + OPEN = 1 + MOVING_OPEN = 2 + CLOSED = 3 + NOT_ENABLED = 4 + TIMEOUT_CLOSE = 5 + TIMEOUT_OPEN = 6 + CLOSE_LS_LOST = 7 + OPEN_LS_LOST = 8 + CLOSE_LS_NOT_FREE = 9 + OPEN_LS_NOT_FREE = 10 + ERROR_LS = 11 + TO_CONNECT = 12 + MAN_OPEN = 13 + UNDEFINED = 14 + +class Absorber(PSIDeviceBase): + """Class for the Frontend Absorber""" + + USER_ACCESS = ["open", "close"] + + request = Cpt(EpicsSignal, suffix="REQUEST", kind="config", doc="Open/Close Absorber") + 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) + + self.timeout_for_move = 10 + # Wait for connection on all components, ensure IOC is connected + self.wait_for_connection(all_signals=True, timeout=5) + + def open(self) -> DeviceStatus | None: + """Open the Absorber""" + if self.status.get() == STATUS.CLOSED: + self.request.put(1) + status_open = CompareStatus(self.status, STATUS.OPEN, timeout=self.timeout_for_move) + status = status_open + return status + else: + return None + + def close(self) -> DeviceStatus | None: + """Close the Absorber""" + if self.status.get() == STATUS.OPEN: + self.request.put(1) + status_close = CompareStatus(self.status, STATUS.CLOSED, timeout=self.timeout_for_move) + status = status_close + return status + else: + return None diff --git a/debye_bec/devices/cameras/basler_cam.py b/debye_bec/devices/cameras/basler_cam.py index fbf0477..5c5f7ce 100644 --- a/debye_bec/devices/cameras/basler_cam.py +++ b/debye_bec/devices/cameras/basler_cam.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from ophyd import ADBase +from ophyd import ADBase, EpicsSignalRO from ophyd import ADComponent as ADCpt from ophyd import Component as Cpt from ophyd_devices import PreviewSignal @@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover class BaslerCamBase(ADBase): """BaslerCam Base class.""" + cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True) + + _default_configuration_attrs = [ + 'cam1.acquire_time', + 'cam1.detector_state', + 'cam_detector_state_string', + 'cam1.gain', + 'cam1.model', + ] + cam1 = ADCpt(AravisDetectorCam, "cam1:") image1 = ADCpt(ImagePlugin_V35, "image1:") diff --git a/debye_bec/devices/cameras/hutch_cam.py b/debye_bec/devices/cameras/hutch_cam.py new file mode 100644 index 0000000..633b8dc --- /dev/null +++ b/debye_bec/devices/cameras/hutch_cam.py @@ -0,0 +1,79 @@ +"""EH Hutch Cameras""" + +from __future__ import annotations + +import cv2 +import threading +from typing import TYPE_CHECKING + +from bec_lib.logger import bec_logger +from bec_lib.file_utils import get_full_path +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from ophyd_devices import DeviceStatus + +if TYPE_CHECKING: # pragma: no cover + from bec_lib.devicemanager import ScanInfo + from bec_lib.messages import ScanStatusMessage + +logger = bec_logger.logger + +CAM_USERNAME = "camera_user" +CAM_PASSWORD = "camera_user1" +CAM_PORT = 554 + +class HutchCam(PSIDeviceBase): + """Class for the Hutch Cameras""" + + # image = Cpt(Signal, name='image', kind='config') + + def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): + super().__init__(name=name, scan_info=scan_info, **kwargs) + + self.hostname = prefix + self.status = None + + # pylint: disable=E1101 + def on_connected(self) -> None: + """ + Called after the device is connected and its signals are connected. + Default values for signals should be set here. + """ + rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1" + cap = cv2.VideoCapture(f"{rtsp_url}?tcp") + if not cap.isOpened(): + logger.error(self, "Connection Failed", "Could not connect to the camera stream.") + return + cap.release() + + def on_stage(self) -> DeviceStatus: + """Called while staging the device.""" + + scan_msg: ScanStatusMessage = self.scan_info.msg + file_path = get_full_path(scan_msg, name='hutch_cam_' + self.hostname).removesuffix('h5') + + self.status = DeviceStatus(self) + + thread = threading.Thread(target=self._save_picture, args=(file_path, self.status), daemon=True) + thread.start() + + return self.status + + def _save_picture(self, file_path, status): + try: + logger.info(f'Capture from camera {self.hostname}') + rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1" + cap = cv2.VideoCapture(f"{rtsp_url}?tcp") + if not cap.isOpened(): + logger.error("Connection Failed", "Could not connect to the camera stream.") + return + logger.info(f'Connection to camera {self.hostname} established') + ret, frame = cap.readAsync() + cap.release() + if not ret: + logger.error("Capture Failed", "Failed to capture image from camera.") + return + cv2.imwrite(file_path + 'png', frame) + status.set_finished() + logger.info(f'Capture from camera {self.hostname} done') + except Exception as e: + status.set_exception(e) diff --git a/debye_bec/devices/cameras/prosilica_cam.py b/debye_bec/devices/cameras/prosilica_cam.py index 69846dd..92f842d 100644 --- a/debye_bec/devices/cameras/prosilica_cam.py +++ b/debye_bec/devices/cameras/prosilica_cam.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from ophyd import ADBase +from ophyd import ADBase, EpicsSignalRO from ophyd import ADComponent as ADCpt from ophyd import Component as Cpt from ophyd_devices import PreviewSignal @@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover class ProsilicaCamBase(ADBase): """Base class for Prosilica cameras.""" + cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True) + + _default_configuration_attrs = [ + 'cam1.acquire_time', + 'cam1.detector_state', + 'cam_detector_state_string', + 'cam1.gain', + 'cam1.model', + ] + cam1 = ADCpt(ProsilicaDetectorCam, "cam1:") image1 = ADCpt(ImagePlugin_V35, "image1:") diff --git a/debye_bec/devices/ionization_chambers/ionization_chamber.py b/debye_bec/devices/ionization_chambers/ionization_chamber.py index 52a6a78..25b81d8 100644 --- a/debye_bec/devices/ionization_chambers/ionization_chamber.py +++ b/debye_bec/devices/ionization_chambers/ionization_chamber.py @@ -33,22 +33,24 @@ class EpicsSignalSplit(EpicsSignal): class GasMixSetupControl(Device): """GasMixSetup Control for Inonization Chamber 0""" - gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="config", doc="Gas 1 requirement") + gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="omitted", doc="Gas 1 requirement") conc1_req = Cpt( - EpicsSignalWithRBV, suffix="Conc1Req", kind="config", doc="Concentration 1 requirement" + EpicsSignalWithRBV, suffix="Conc1Req", kind="omitted", doc="Concentration 1 requirement" ) - gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="config", doc="Gas 2 requirement") + gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="omitted", doc="Gas 2 requirement") conc2_req = Cpt( - EpicsSignalWithRBV, suffix="Conc2Req", kind="config", doc="Concentration 2 requirement" + EpicsSignalWithRBV, suffix="Conc2Req", kind="omitted", doc="Concentration 2 requirement" ) press_req = Cpt( - EpicsSignalWithRBV, suffix="PressReq", kind="config", doc="Pressure requirement" + EpicsSignalWithRBV, suffix="PressReq", kind="omitted", doc="Pressure requirement" ) fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber") status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status") gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1") + gas1_string = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1", string=True) conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1") gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2") + gas2_string = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2", string=True) conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2") press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure") @@ -84,10 +86,25 @@ class IonizationChamber0(PSIDeviceBase): (f"ES:AMP5004:cFilter{num}_ENUM"), {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, ), + "cOnOff_string": ( + EpicsSignal, + (f"ES:AMP5004.cOnOff{num}"), + {"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True}, + ), + "cGain_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cGain{num}_ENUM"), + {"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True}, + ), + "cFilter_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cFilter{num}_ENUM"), + {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True}, + ), } amp = Dcpt(amp_signals) gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") - gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") + gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:") hv_en_signals = { "ext_ena": ( @@ -275,10 +292,25 @@ class IonizationChamber1(IonizationChamber0): (f"ES:AMP5004:cFilter{num}_ENUM"), {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, ), + "cOnOff_string": ( + EpicsSignal, + (f"ES:AMP5004.cOnOff{num}"), + {"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True}, + ), + "cGain_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cGain{num}_ENUM"), + {"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True}, + ), + "cFilter_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cFilter{num}_ENUM"), + {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True}, + ), } amp = Dcpt(amp_signals) gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") - gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") + gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:") hv_en_signals = { "ext_ena": ( @@ -311,10 +343,25 @@ class IonizationChamber2(IonizationChamber0): (f"ES:AMP5004:cFilter{num}_ENUM"), {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, ), + "cOnOff_string": ( + EpicsSignal, + (f"ES:AMP5004.cOnOff{num}"), + {"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True}, + ), + "cGain_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cGain{num}_ENUM"), + {"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True}, + ), + "cFilter_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cFilter{num}_ENUM"), + {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True}, + ), } amp = Dcpt(amp_signals) gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") - gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") + gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:") hv_en_signals = { "ext_ena": ( @@ -325,3 +372,63 @@ class IonizationChamber2(IonizationChamber0): "ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}), } hv_en = Dcpt(hv_en_signals) + +class Pips(IonizationChamber0): + """Pips, prefix should be 'X01DA-'.""" + + USER_ACCESS = ["set_gain", "set_filter"] + + num = 4 + amp_signals = { + "cOnOff": ( + EpicsSignal, + (f"ES:AMP5004.cOnOff{num}"), + {"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"}, + ), + "cGain_ENUM": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cGain{num}_ENUM"), + {"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"}, + ), + "cFilter_ENUM": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cFilter{num}_ENUM"), + {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, + ), + "cOnOff_string": ( + EpicsSignal, + (f"ES:AMP5004.cOnOff{num}"), + {"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True}, + ), + "cGain_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cGain{num}_ENUM"), + {"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True}, + ), + "cFilter_ENUM_string": ( + EpicsSignalWithRBV, + (f"ES:AMP5004:cFilter{num}_ENUM"), + {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True}, + ), + } + amp = Dcpt(amp_signals) + gmes = None + gmes_status_msg = None + hv = None + hv_en_signals = None + hv_en = None + + @typechecked + def set_hv(self, *_) -> None: + """Not available for the PIPS""" + return None + + @typechecked + def set_grid(self, *_) -> None: + """Not available for the PIPS""" + return None + + @typechecked + def fill(self, *_) -> None: + """Not available for the PIPS""" + return None diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 6a4fe1a..fe1d5e5 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -76,8 +76,14 @@ class Mo1BraggEncoder(Device): class Mo1BraggCrystal(Device): """Mo1 Bragg PVs to set the crystal parameters""" - offset_si111 = Cpt(EpicsSignalWithRBV, suffix="offset_si111", kind="config") - offset_si311 = Cpt(EpicsSignalWithRBV, suffix="offset_si311", kind="config") + bragg_off_si111 = Cpt(EpicsSignalWithRBV, suffix="bragg_off_si111", kind="config") + bragg_off_si311 = Cpt(EpicsSignalWithRBV, suffix="bragg_off_si311", kind="config") + phi_off_si111 = Cpt(EpicsSignalWithRBV, suffix="phi_off_si111", kind="config") + phi_off_si311 = Cpt(EpicsSignalWithRBV, suffix="phi_off_si311", kind="config") + azm_off_si111 = Cpt(EpicsSignalWithRBV, suffix="azm_off_si111", kind="config") + azm_off_si311 = Cpt(EpicsSignalWithRBV, suffix="azm_off_si311", kind="config") + miscut_si111 = Cpt(EpicsSignalWithRBV, suffix="miscut_si111", kind="config") + miscut_si311 = Cpt(EpicsSignalWithRBV, suffix="miscut_si311", kind="config") xtal_enum = Cpt(EpicsSignalWithRBV, suffix="xtal_ENUM", kind="config") d_spacing_si111 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si111", kind="config") d_spacing_si311 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si311", kind="config") @@ -85,13 +91,21 @@ class Mo1BraggCrystal(Device): current_d_spacing = Cpt( EpicsSignalRO, suffix="current_d_spacing_RBV", kind="normal", auto_monitor=True ) - current_offset = Cpt( - EpicsSignalRO, suffix="current_offset_RBV", kind="normal", auto_monitor=True + current_bragg_off = Cpt( + EpicsSignalRO, suffix="current_bragg_off_RBV", kind="normal", auto_monitor=True + ) + current_phi_off = Cpt( + EpicsSignalRO, suffix="current_phi_off_RBV", kind="normal", auto_monitor=True + ) + current_azm_off = Cpt( + EpicsSignalRO, suffix="current_azm_off_RBV", kind="normal", auto_monitor=True + ) + current_miscut = Cpt( + EpicsSignalRO, suffix="current_miscut_RBV", kind="normal", auto_monitor=True ) current_xtal = Cpt( EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True ) - current_xtal_string = Cpt( EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True ) @@ -240,6 +254,8 @@ class Mo1BraggPositioner(Device, PositionerBase): high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_energy_RBV", kind="config", auto_monitor=True) velocity = Cpt(EpicsSignalWithRBV, suffix="move_velocity", kind="config", auto_monitor=True) + angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True) + ########## Move Command PVs ########## move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True) @@ -392,8 +408,8 @@ class Mo1BraggPositioner(Device, PositionerBase): def set_xtal( self, xtal_enum: Literal["111", "311"], - offset_si111: float = None, - offset_si311: float = None, + bragg_off_si111: float = None, + bragg_off_si311: float = None, d_spacing_si111: float = None, d_spacing_si311: float = None, ) -> None: @@ -401,15 +417,15 @@ class Mo1BraggPositioner(Device, PositionerBase): Args: xtal_enum (Literal["111", "311"]) : Enum to set the crystal orientation - offset_si111 (float) : Offset for the 111 crystal - offset_si311 (float) : Offset for the 311 crystal + bragg_off_si111 (float) : Offset for the 111 crystal + bragg_off_si311 (float) : Offset for the 311 crystal d_spacing_si111 (float) : d-spacing for the 111 crystal d_spacing_si311 (float) : d-spacing for the 311 crystal """ - if offset_si111 is not None: - self.crystal.offset_si111.put(offset_si111) - if offset_si311 is not None: - self.crystal.offset_si311.put(offset_si311) + if bragg_off_si111 is not None: + self.crystal.bragg_off_si111.put(bragg_off_si111) + if bragg_off_si311 is not None: + self.crystal.bragg_off_si311.put(bragg_off_si311) if d_spacing_si111 is not None: self.crystal.d_spacing_si111.put(d_spacing_si111) if d_spacing_si311 is not None: diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 8d6dcd9..c1564ba 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -33,6 +33,107 @@ class NidaqControl(Device): """Nidaq control class with all PVs""" ### Readback PVs for EpicsEmitter ### + energy = Cpt(SetableSignal, value=0, kind=Kind.normal) + + smpl_abs = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption" + ) + smpl_fluo = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence" + ) + ref_abs = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption" + ) + cisum = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum" + ) + + ai0_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN" + ) + ai1_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN" + ) + ai2_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN" + ) + ai3_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN" + ) + ai4_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN" + ) + ai5_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN" + ) + ai6_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN" + ) + ai7_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN" + ) + + di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") + di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") + di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX") + di3_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 3, MAX") + di4_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 4, MAX") + + ci0_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN" + ) + ci1_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN" + ) + ci2_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN" + ) + ci3_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN" + ) + ci4_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN" + ) + ci5_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN" + ) + ci6_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN" + ) + ci7_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN" + ) + ci8_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN" + ) + ci9_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN" + ) + ci10_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN" + ) + ci11_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN" + ) + ci12_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN" + ) + ci13_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN" + ) + ci14_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN" + ) + ci15_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN" + ) + ci16_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN" + ) + ci17_mean = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN" + ) + ai0 = Cpt( EpicsSignalRO, suffix="NIDAQ-AI0", @@ -146,6 +247,76 @@ class NidaqControl(Device): doc="EPICS counter input 7", auto_monitor=True, ) + ci8 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI8", + kind=Kind.normal, + doc="EPICS counter input 8", + auto_monitor=True, + ) + ci9 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI9", + kind=Kind.normal, + doc="EPICS counter input 9", + auto_monitor=True, + ) + ci10 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI10", + kind=Kind.normal, + doc="EPICS counter input 0", + auto_monitor=True, + ) + ci11 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI11", + kind=Kind.normal, + doc="EPICS counter input 1", + auto_monitor=True, + ) + ci12 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI12", + kind=Kind.normal, + doc="EPICS counter input 2", + auto_monitor=True, + ) + ci13 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI13", + kind=Kind.normal, + doc="EPICS counter input 3", + auto_monitor=True, + ) + ci14 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI14", + kind=Kind.normal, + doc="EPICS counter input 4", + auto_monitor=True, + ) + ci15 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI15", + kind=Kind.normal, + doc="EPICS counter input 5", + auto_monitor=True, + ) + ci16 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI16", + kind=Kind.normal, + doc="EPICS counter input 6", + auto_monitor=True, + ) + ci17 = Cpt( + EpicsSignalRO, + suffix="NIDAQ-CI17", + kind=Kind.normal, + doc="EPICS counter input 7", + auto_monitor=True, + ) di0 = Cpt( EpicsSignalRO, @@ -200,32 +371,6 @@ class NidaqControl(Device): ) ### Readback for BEC emitter ### - - ai0_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN" - ) - ai1_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN" - ) - ai2_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN" - ) - ai3_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN" - ) - ai4_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN" - ) - ai5_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN" - ) - ai6_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN" - ) - ai7_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN" - ) - ai0_std_dev = Cpt( SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD" ) @@ -251,31 +396,6 @@ class NidaqControl(Device): SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD" ) - ci0_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN" - ) - ci1_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN" - ) - ci2_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN" - ) - ci3_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN" - ) - ci4_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN" - ) - ci5_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN" - ) - ci6_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN" - ) - ci7_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN" - ) - ci0_std_dev = Cpt( SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD" ) @@ -300,44 +420,95 @@ class NidaqControl(Device): ci7_std_dev = Cpt( SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD" ) + ci8_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD" + ) + ci9_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD" + ) + ci10_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD" + ) + ci11_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD" + ) + ci12_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD" + ) + ci13_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD" + ) + ci14_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD" + ) + ci15_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD" + ) + ci16_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD" + ) + ci17_std_dev = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD" + ) xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp") - xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp") - + xrd_angle = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD angle") xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy") - - di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") - di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") - di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX") - di3_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 3, MAX") - di4_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 4, MAX") + xrd_ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 mean") + xrd_ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 std dev") enc = Cpt(SetableSignal, value=0, kind=Kind.normal) - energy = Cpt(SetableSignal, value=0, kind=Kind.normal) rle = Cpt(SetableSignal, value=0, kind=Kind.normal) ### Control PVs ### - enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config) + enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config, auto_monitor=True) + enable_dead_time_correction = Cpt(EpicsSignal, suffix="NIDAQ-EnableDTC", kind=Kind.config, auto_monitor=True) kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config) stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config) state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True) server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config) compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config) scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config) - sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config) + scan_type_string = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config, string=True) + sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, auto_monitor=True) + sampling_rate_string = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, string=True, auto_monitor=True) scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config) - readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config) - encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config) + readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, auto_monitor=True) + readout_range_string = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, string=True, auto_monitor=True) + encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, auto_monitor=True) + encoder_factor_string = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, string=True, auto_monitor=True) stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config) power = Cpt(EpicsSignal, suffix="NIDAQ-Power", kind=Kind.config) heartbeat = Cpt(EpicsSignal, suffix="NIDAQ-Heartbeat", kind=Kind.config, auto_monitor=True) time_left = Cpt(EpicsSignalRO, suffix="NIDAQ-TimeLeft", kind=Kind.config, auto_monitor=True) - ai_chans = Cpt(EpicsSignal, suffix="NIDAQ-AIChans", kind=Kind.config) - ci_chans = Cpt(EpicsSignal, suffix="NIDAQ-CIChans", kind=Kind.config) - di_chans = Cpt(EpicsSignal, suffix="NIDAQ-DIChans", kind=Kind.config) + ai_chans = Cpt(EpicsSignal, suffix="NIDAQ-AIChans", kind=Kind.config, auto_monitor=True) + ci_chans = Cpt(EpicsSignal, suffix="NIDAQ-CIChans", kind=Kind.config, auto_monitor=True) + di_chans = Cpt(EpicsSignal, suffix="NIDAQ-DIChans", kind=Kind.config, auto_monitor=True) + add_chans = Cpt(EpicsSignal, suffix="NIDAQ-AddChans", kind=Kind.config, auto_monitor=True) + + smpl_abs_ln = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_ln", kind=Kind.config, auto_monitor=True) + ref_abs_ln = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_ln", kind=Kind.config, auto_monitor=True) + + smpl_abs_no = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_no", kind=Kind.config, auto_monitor=True) + smpl_abs_no_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_no", kind=Kind.config, string=True, auto_monitor=True) + + smpl_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_de", kind=Kind.config, auto_monitor=True) + smpl_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_de", kind=Kind.config, string=True, auto_monitor=True) + + smpl_fluo_no = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_no", kind=Kind.config, auto_monitor=True) + smpl_fluo_no_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_no", kind=Kind.config, string=True, auto_monitor=True) + + smpl_fluo_de = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_de", kind=Kind.config, auto_monitor=True) + smpl_fluo_de_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_de", kind=Kind.config, string=True, auto_monitor=True) + + ref_abs_no = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_no", kind=Kind.config, auto_monitor=True) + ref_abs_no_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_no", kind=Kind.config, string=True, auto_monitor=True) + + ref_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, auto_monitor=True) + ref_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, string=True, auto_monitor=True) class Nidaq(PSIDeviceBase, NidaqControl): @@ -357,7 +528,7 @@ class Nidaq(PSIDeviceBase, NidaqControl): super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) self.scan_info: ScanInfo self.timeout_wait_for_signal = 5 # put 5s firsts - self._timeout_wait_for_pv = 3 # 3s timeout for pv calls + self._timeout_wait_for_pv = 5 # 5s timeout for pv calls. editted due to timeout issues persisting self.valid_scan_names = [ "xas_simple_scan", "xas_simple_scan_with_xrd", @@ -556,7 +727,11 @@ class Nidaq(PSIDeviceBase, NidaqControl): # Stage call to IOC status = CompareStatus(self.state, NidaqState.STAGE) self.cancel_on_stop(status) - self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv) + # TODO 11.11.25/HS64 + # Switched from set to put in the hope to get rid of the rare event where nidaq is stopped at the start of a scan + # Problems consistently persisting, testing changing back to set, unconvinced this is the actual cause 14.11.25/AHC + # self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv) + self.stage_call.put(1) status.wait(timeout=self.timeout_wait_for_signal) if self.scan_info.msg.scan_name != "nidaq_continuous_scan": status = self.on_kickoff() diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 3db1c4c..2a7668f 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -6,13 +6,13 @@ import enum import threading import time import traceback -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple import numpy as np from bec_lib.file_utils import get_full_path from bec_lib.logger import bec_logger from ophyd import Component as Cpt -from ophyd import EpicsSignal, Kind +from ophyd import EpicsSignal, EpicsSignalRO, Kind from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin @@ -145,6 +145,19 @@ class Pilatus(PSIDeviceBase, ADBase): # USER_ACCESS = ["start_live_mode", "stop_live_mode"] + cam_gain_menu_string = Cpt(EpicsSignalRO, suffix='cam1:GainMenu', string=True) + + _default_configuration_attrs = [ + 'cam.threshold_energy', + 'cam.threshold_auto_apply', + 'cam.gain_menu', + 'cam_gain_menu_string', + 'cam.pixel_cut_off', + 'cam.acquire_time', + 'cam.num_exposures', + 'cam.model', + ] + cam = Cpt(PilatusDetectorCam, "cam1:") hdf = Cpt(HDF5Plugin, "HDF1:") image1 = Cpt(ImagePlugin, "image1:") @@ -203,22 +216,11 @@ class Pilatus(PSIDeviceBase, ADBase): PreviewSignal, name="preview", ndim=2, - num_rotation_90=0, # Check this + num_rotation_90=3, doc="Preview signal for the Pilatus Detector", ) file_event = Cpt(FileEventSignal, name="file_event") - @property - def baseline_signals(self): - """Define baseline signals""" - return [ - self.cam.acquire_time, - self.cam.num_exposures, - self.cam.threshold_energy, - self.cam.gain_menu, - self.cam.pixel_cut_off, - ] - def __init__( self, *, @@ -366,68 +368,51 @@ class Pilatus(PSIDeviceBase, ADBase): status = status_acquire & status_writing & status_cam_server return status - def _calculate_trigger(self, scan_msg: ScanStatusMessage): + def _calculate_trigger(self, scan_msg: ScanStatusMessage) -> Tuple[float, float]: self._update_scan_parameter() total_osc = 0 + calc_duration = 0 total_trig_lo = 0 total_trig_hi = 0 - calc_duration = 0 - n_trig_lo = 1 - n_trig_hi = 1 - init_lo = 1 - init_hi = 1 - lo_done = 0 - hi_done = 0 - if not self.scan_parameter.break_enable_low: - lo_done = 1 - if not self.scan_parameter.break_enable_high: - hi_done = 1 - start_time = time.time() - while True: - # TODO, we should not use infinite loops, for now let's add the escape Timeout of 20s, but should eventually be reviewed. - if time.time() - start_time > 20: - raise RuntimeError( - f"Calculating the number of triggers for scan {scan_msg.scan_name} took more than 20 seconds, aborting." - ) + # Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy + loc_break_enable_low = self.scan_parameter.break_enable_high + loc_break_time_low = self.scan_parameter.break_time_high + loc_cycle_low = self.scan_parameter.cycle_high + loc_break_enable_high = self.scan_parameter.break_enable_low + loc_break_time_high = self.scan_parameter.break_time_low + loc_cycle_high = self.scan_parameter.cycle_low + + if not loc_break_enable_low: + loc_break_time_low = 0 + loc_cycle_low = 1 + if not loc_break_enable_high: + loc_break_time_high = 0 + loc_cycle_high = 1 + + total_osc = self.scan_parameter.scan_duration / ( + self.scan_parameter.scan_time + + loc_break_time_low / (2 * loc_cycle_low) + + loc_break_time_high / (2 * loc_cycle_high) + ) + total_osc = np.ceil(total_osc) + total_osc = total_osc + total_osc % 2 # round up to the next even number + + if loc_break_enable_low: + total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low)) + if loc_break_enable_high: + total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high)) + calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high + + if calc_duration < self.scan_parameter.scan_duration: + # Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers total_osc = total_osc + 2 - calc_duration = calc_duration + 2 * self.scan_parameter.scan_time + if loc_break_enable_low: + total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low)) + if loc_break_enable_high: + total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high)) + calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high - if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low: - n_trig_lo = 1 - calc_duration = calc_duration + self.scan_parameter.break_time_low - if init_lo: - lo_done = 1 - init_lo = 0 - else: - n_trig_lo += 1 - - if ( - self.scan_parameter.break_enable_high - and n_trig_hi >= self.scan_parameter.cycle_high - ): - n_trig_hi = 1 - calc_duration = calc_duration + self.scan_parameter.break_time_high - if init_hi: - hi_done = 1 - init_hi = 0 - else: - n_trig_hi += 1 - - if lo_done and hi_done: - n = np.floor(self.scan_parameter.scan_duration / calc_duration) - total_osc = total_osc * n - if self.scan_parameter.break_enable_low: - total_trig_lo = n + 1 - if self.scan_parameter.break_enable_high: - total_trig_hi = n + 1 - calc_duration = calc_duration * n - lo_done = 0 - hi_done = 0 - - if calc_duration >= self.scan_parameter.scan_duration: - break - - return total_trig_lo + total_trig_hi + return total_trig_lo, total_trig_hi ######################################## # Beamline Specific Implementations # @@ -480,6 +465,14 @@ class Pilatus(PSIDeviceBase, ADBase): """ # self.stop_live_mode() # Make sure that live mode is stopped if scan runs + # If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition + if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value: + self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5) + if self.cam.acquire.get() == ACQUIREMODE.ACQUIRING.value: + self.cam.acquire.put(0) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_cam.wait(timeout=5) + scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name in self.xas_xrd_scan_names: self._update_scan_parameter() diff --git a/debye_bec/devices/pilatus_curtain.py b/debye_bec/devices/pilatus_curtain.py index d129673..b607ae9 100644 --- a/debye_bec/devices/pilatus_curtain.py +++ b/debye_bec/devices/pilatus_curtain.py @@ -69,11 +69,11 @@ class PilatusCurtain(PSIDeviceBase): def on_unstage(self) -> DeviceStatus | None: """Called while unstaging the device.""" - return self.close() + # return self.close() def on_stop(self) -> DeviceStatus | None: """Called when the device is stopped.""" - return self.close() + # return self.close() def open(self) -> DeviceStatus | None: """Open the cover""" diff --git a/debye_bec/devices/reffoilchanger.py b/debye_bec/devices/reffoilchanger.py index d22c997..b59970d 100644 --- a/debye_bec/devices/reffoilchanger.py +++ b/debye_bec/devices/reffoilchanger.py @@ -52,9 +52,15 @@ class Reffoilchanger(PSIDeviceBase): status = Cpt( EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status" ) + status_string = Cpt( + EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status", string=True + ) op_mode = Cpt( EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status" ) + op_mode_string = Cpt( + EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status", string=True + ) ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference") ref_rb = Cpt( EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference" diff --git a/debye_bec/file_writer/debye_nexus_structure.py b/debye_bec/file_writer/debye_nexus_structure.py index ebabcd7..5a44ad9 100644 --- a/debye_bec/file_writer/debye_nexus_structure.py +++ b/debye_bec/file_writer/debye_nexus_structure.py @@ -1,5 +1,6 @@ from bec_server.file_writer.default_writer import DefaultFormat +import debye_bec.bec_widgets.widgets.x01da_parameters as bl class DebyeNexusStructure(DefaultFormat): """Nexus Structure for Debye""" @@ -12,102 +13,6 @@ class DebyeNexusStructure(DefaultFormat): instrument = entry.create_group(name="instrument") instrument.attrs["NX_class"] = "NXinstrument" - ################### - ## mo1_bragg specific information - ################### - - # Logic if device exist - if "mo1_bragg" in self.device_manager.devices: - - monochromator = instrument.create_group(name="monochromator") - monochromator.attrs["NX_class"] = "NXmonochromator" - crystal = monochromator.create_group(name="crystal") - crystal.attrs["NX_class"] = "NXcrystal" - - # Create a dataset - chemical_formular = crystal.create_dataset(name="chemical_formular", data="Si") - chemical_formular.attrs["NX_class"] = "NX_CHAR" - - # Create a softlink - d_spacing = crystal.create_soft_link( - name="d_spacing", - target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value", - ) - d_spacing.attrs["NX_class"] = "NX_FLOAT" - - offset = crystal.create_soft_link( - name="offset", - target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_offset/value", - ) - offset.attrs["NX_class"] = "NX_FLOAT" - - reflection = crystal.create_soft_link( - name="reflection", - target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value", - ) - reflection.attrs["NX_class"] = "NX_CHAR" - - ################## - ## cm mirror specific information - ################### - - collimating_mirror = instrument.create_group(name="collimating_mirror") - collimating_mirror.attrs["NX_class"] = "NXmirror" - - cm_substrate_material = collimating_mirror.create_dataset( - name="substrate_material", data="Si" - ) - cm_substrate_material.attrs["NX_class"] = "NX_CHAR" - - cm_bending_radius = collimating_mirror.create_soft_link( - name="sagittal radius", - target="/entry/collection/devices/cm_bnd_radius/cm_bnd_radius/value", - ) - cm_bending_radius.attrs["NX_class"] = "NX_FLOAT" - cm_bending_radius.attrs["units"] = "km" - - cm_incidence_angle = collimating_mirror.create_soft_link( - name="incidence angle", target="/entry/collection/devices/cm_rotx/cm_rotx/value" - ) - cm_incidence_angle.attrs["NX_class"] = "NX_FLOAT" - - cm_yaw_angle = collimating_mirror.create_soft_link( - name="incident angle", target="/entry/collection/devices/cm_roty/cm_roty/value" - ) - cm_yaw_angle.attrs["NX_class"] = "NX_FLOAT" - - ################## - ## fm mirror specific information - ################### - - focusing_mirror = instrument.create_group(name="focusing_mirror") - focusing_mirror.attrs["NX_class"] = "NXmirror" - - fm_substrate_material = focusing_mirror.create_dataset(name="substrate_material", data="Si") - fm_substrate_material.attrs["NX_class"] = "NX_CHAR" - - fm_bending_radius = focusing_mirror.create_soft_link( - name="sagittal radius", - target="/entry/collection/devices/fm_bnd_radius/fm_bnd_radius/value", - ) - fm_bending_radius.attrs["NX_class"] = "NX_FLOAT" - - fm_incidence_angle = focusing_mirror.create_soft_link( - name="incidence angle", - target="/entry/collection/devices/fm_incidence_angle/fm_incidence_angle/value", - ) - fm_incidence_angle.attrs["NX_class"] = "NX_FLOAT" - - fm_yaw_angle = focusing_mirror.create_soft_link( - name="yaw angle", target="/entry/collection/devices/fm_roty/fm_roty/value" - ) - fm_yaw_angle.attrs["NX_class"] = "NX_FLOAT" - - fm_roll_angle = focusing_mirror.create_soft_link( - name="roll angle", target="/entry/collection/devices/fm_rotz/fm_rotz/value" - ) - fm_roll_angle.attrs["NX_class"] = "NX_FLOAT" - ################## ## source specific information ################### @@ -123,3 +28,326 @@ class DebyeNexusStructure(DefaultFormat): probe = source.create_dataset(name="probe", data="X-ray") probe.attrs["NX_class"] = "NX_CHAR" + + if "curr" in self.device_manager.devices: + ring_current = source.create_soft_link( + name="ring_current", + target="/entry/collection/devices/curr/curr/value", + ) + ring_current.attrs["NX_class"] = "NX_FLOAT" + ring_current.attrs["units"] = "mA" + + ################### + ## mo1_bragg specific information + ################### + + ## Logic if device exist + if "mo1_bragg" in self.device_manager.devices: + + monochromator = instrument.create_group(name="monochromator") + monochromator.attrs["NX_class"] = "NXmonochromator" + crystal = monochromator.create_group(name="crystal") + crystal.attrs["NX_class"] = "NXcrystal" + + # Create a dataset + chemical_formular = crystal.create_dataset(name="chemical_formular", data="Si") + chemical_formular.attrs["NX_class"] = "NX_CHAR" + + reflection = crystal.create_soft_link( + name="reflection", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value", + ) + reflection.attrs["NX_class"] = "NX_CHAR" + + # Create a softlink + d_spacing = crystal.create_soft_link( + name="d_spacing", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value", + ) + d_spacing.attrs["NX_class"] = "NX_FLOAT" + d_spacing.attrs["units"] = "angstrom" + + bragg_offset = crystal.create_soft_link( + name="bragg_offset", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_bragg_off/value", + ) + bragg_offset.attrs["NX_class"] = "NX_FLOAT" + bragg_offset.attrs["units"] = "degree" + + phi_offset = crystal.create_soft_link( + name="phi_offset", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_phi_off/value", + ) + phi_offset.attrs["NX_class"] = "NX_FLOAT" + phi_offset.attrs["units"] = "degree" + + ## Logic if device exist + if "mo1_roty" in self.device_manager.devices: + + # Create a softlink + azimuthal_angle = crystal.create_soft_link( + name="azimuthal_angle", + target="/entry/collection/devices/mo1_roty/mo1_roty/value", + ) + azimuthal_angle.attrs["NX_class"] = "NX_FLOAT" + azimuthal_angle.attrs["units"] = "degree" + + azm_offset = crystal.create_soft_link( + name="azm_offset", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_azm_off/value", + ) + azm_offset.attrs["NX_class"] = "NX_FLOAT" + azm_offset.attrs["units"] = "degree" + + miscut = crystal.create_soft_link( + name="miscut", + target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_miscut/value", + ) + miscut.attrs["NX_class"] = "NX_FLOAT" + miscut.attrs["units"] = "degree" + + ################### + ### cm mirror specific information + #################### + + collimating_mirror = instrument.create_group(name="collimating_mirror") + collimating_mirror.attrs["NX_class"] = "NXmirror" + + cm_substrate_material = collimating_mirror.create_dataset( + name="substrate_material", data="Si" + ) + cm_substrate_material.attrs["NX_class"] = "NX_CHAR" + + #previous error due to space in name field + + if "cm_bnd_radius" in self.device_manager.devices: + cm_bending_radius = collimating_mirror.create_soft_link( + name="sagittal_radius", + target="/entry/collection/devices/cm_bnd_radius/cm_bnd_radius/value", + ) + cm_bending_radius.attrs["NX_class"] = "NX_FLOAT" + cm_bending_radius.attrs["units"] = "km" + + if "cm_rotx" in self.device_manager.devices: + cm_incidence_angle = collimating_mirror.create_soft_link( + name="incidence_angle", target="/entry/collection/devices/cm_rotx/cm_rotx/value" + ) + cm_incidence_angle.attrs["NX_class"] = "NX_FLOAT" + cm_incidence_angle.attrs["units"] = "mrad" + + if "cm_roty" in self.device_manager.devices: + cm_yaw_angle = collimating_mirror.create_soft_link( + name="yaw_angle", target="/entry/collection/devices/cm_roty/cm_roty/value" + ) + cm_yaw_angle.attrs["NX_class"] = "NX_FLOAT" + cm_yaw_angle.attrs["units"] = "mrad" + + if "cm_rotz" in self.device_manager.devices: + cm_roll_angle = collimating_mirror.create_soft_link( + name="roll_angle", target="/entry/collection/devices/cm_rotz/cm_rotz/value" + ) + cm_roll_angle.attrs["NX_class"] = "NX_FLOAT" + cm_roll_angle.attrs["units"] = "mrad" + + if 'cm_trx' in self.device_manager.devices: + cm_trx = - self.device_manager.devices.cm_trx.read(cached=True).get('cm_trx').get('value') + stripe = 'Unknown' + for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): + if low <= cm_trx <= high: + stripe = name + cm_stripe = collimating_mirror.create_dataset( + name="stripe", data=stripe + ) + cm_stripe.attrs["NX_class"] = "NX_CHAR" + + ################### + ### fm mirror specific information + #################### + + focusing_mirror = instrument.create_group(name="focusing_mirror") + focusing_mirror.attrs["NX_class"] = "NXmirror" + + fm_substrate_material = focusing_mirror.create_dataset( + name="substrate_material", data="Si" + ) + fm_substrate_material.attrs["NX_class"] = "NX_CHAR" + + if "fm_bnd_radius" in self.device_manager.devices: + fm_bending_radius = focusing_mirror.create_soft_link( + name="sagittal_radius", + target="/entry/collection/devices/fm_bnd_radius/fm_bnd_radius/value", + ) + fm_bending_radius.attrs["NX_class"] = "NX_FLOAT" + fm_bending_radius.attrs["units"] = "km" + + if "fm_rotx" in self.device_manager.devices: + fm_incidence_angle = focusing_mirror.create_soft_link( + name="incidence_angle", target="/entry/collection/devices/fm_rotx/fm_rotx/value" + ) + fm_incidence_angle.attrs["NX_class"] = "NX_FLOAT" + fm_incidence_angle.attrs["units"] = "mrad" + + if "fm_roty" in self.device_manager.devices: + fm_yaw_angle = focusing_mirror.create_soft_link( + name="yaw_angle", target="/entry/collection/devices/fm_roty/fm_roty/value" + ) + fm_yaw_angle.attrs["NX_class"] = "NX_FLOAT" + fm_yaw_angle.attrs["units"] = "mrad" + + if "fm_rotz" in self.device_manager.devices: + fm_roll_angle = focusing_mirror.create_soft_link( + name="roll_angle", target="/entry/collection/devices/fm_rotz/fm_rotz/value" + ) + fm_roll_angle.attrs["NX_class"] = "NX_FLOAT" + fm_roll_angle.attrs["units"] = "mrad" + + if 'fm_trx' in self.device_manager.devices: + fm_trx = - self.device_manager.devices.fm_trx.read(cached=True).get('fm_trx').get('value') + stripe = 'Unknown' + for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]): + if low <= fm_trx <= high: + stripe = name + ' (flat)' + for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]): + if low <= fm_trx <= high: + stripe = name + ' (toroid)' + fm_stripe = focusing_mirror.create_dataset( + name="stripe", data=stripe + ) + fm_stripe.attrs["NX_class"] = "NX_CHAR" + + ################### + ## nidaq specific information + ################### + + ## Logic if device exist + if "nidaq" in self.device_manager.devices: + + #ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value") + ai_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value") + ci_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value") + #add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value") + add_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value") + + measurement_mode = entry.create_group(name="mode") + measurement_mode.attrs["NX_class"] = "NX_CHAR" + + if (int(ci_chans_bits) & 0x7F) != 0: + # Create a dataset + rayspec_sdd_active = measurement_mode.create_group(name="Multi_Element_Partial_Fluorescence_Yield") + me_sdd = rayspec_sdd_active.create_dataset(name="Detector", data="Rayspec 7 element Silicon Drift Detector") + me_sdd.attrs["NX_class"] = "NX_CHAR" + + if (int(ci_chans_bits) & (1<<8)) != 0: + # Create a dataset + ketek_sdd_active = measurement_mode.create_group(name="Single_Element_Partial_Fluorescence_Yield") + se_sdd = ketek_sdd_active.create_dataset(name="Detector", data="Ketex mini single element Silicon Drift Detector") + se_sdd.attrs["NX_class"] = "NX_CHAR" + + if ((int(ai_chans_bits) & (1<<6)) != 0): + # Create a dataset + pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield") + tfy = pips_active.create_dataset(name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector") + tfy.attrs["NX_class"] = "NX_CHAR" + + if ((int(ai_chans_bits) & (1<<0)) != 0) & ((int(ai_chans_bits) & (1<<2)) != 0): + # Create a dataset + ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission") + sam_trans = ai0ai2_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers") + sam_trans.attrs["NX_class"] = "NX_CHAR" + + if ((int(ai_chans_bits) & (1<<2)) != 0) & ((int(ai_chans_bits) & (1<<4)) != 0): + # Create a dataset + ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission") + ref_trans = ai2ai4_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers") + ref_trans.attrs["NX_class"] = "NX_CHAR" + + main_data = entry.create_group(name="data") + main_data.attrs["NX_class"] = "NXdata" + + ################## + ## energy, test whether the signal exists. how to check from config? + ################### + + energy = main_data.create_group(name="energy") + energy.attrs["NX_class"] = "NXdata" + energy.attrs["units"] = "eV" + + main_data.create_soft_link(name="energy", target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value") + + ################## + ## i0 + ################### + + if (int(ai_chans_bits) & (1<<0)) !=0: + i0 = main_data.create_group(name="i0") + i0.attrs["NX_class"] = "NXdata" + i0.attrs["units"] = "V" + + main_data.create_soft_link(name="i0", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value") + + ################## + ## i1 + ################### + + if (int(ai_chans_bits) & (1<<2)) !=0: + i1 = main_data.create_group(name="i1") + i1.attrs["NX_class"] = "NXdata" + i1.attrs["units"] = "V" + + main_data.create_soft_link(name="i1", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value") + + ################## + ## i2 + ################### + + if (int(ai_chans_bits) & (1<<4)) !=0: + i2 = main_data.create_group(name="i2") + i2.attrs["NX_class"] = "NXdata" + i2.attrs["units"] = "V" + + main_data.create_soft_link(name="i2", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value") + + ################## + ## ci sum + ################### + + if int(ci_chans_bits) > 0: + ci_sum = main_data.create_group(name="Fluorescence_Sum") + ci_sum.attrs["NX_class"] = "NXdata" + ci_sum.attrs["units"] = "counts" + + main_data.create_soft_link(name="Fluorescence_Sum", target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value") + + ################## + ## mu sample, test whether the signal exists. how to check from config? + ################### + + if (int(add_chans_bits) & (1<<0)) !=0: + mu_sample = main_data.create_group(name="mu_sample") + mu_sample.attrs["NX_class"] = "NXdata" + + main_data.create_soft_link(name="mu_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value") + + ################## + ## fluo sample, test whether the signal exists. how to check from config? + ################### + + if (int(add_chans_bits) & (1<<1)) !=0: + mu_sample = main_data.create_group(name="fluo_sample") + mu_sample.attrs["NX_class"] = "NXdata" + + main_data.create_soft_link(name="fluo_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value") + + ################## + ## mu reference, test whether the signal exists. how to check from config? + ################### + + if (int(add_chans_bits) & (1<<2)) !=0: + mu_reference = main_data.create_group(name="mu_reference") + mu_reference.attrs["NX_class"] = "NXdata" + + main_data.create_soft_link(name="mu_reference", target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value") + + + + diff --git a/pyproject.toml b/pyproject.toml index 10cab05..e5873cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,15 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices"] +dependencies = [ + "numpy", + "scipy", + "bec_lib", + "h5py", + "ophyd_devices", + "opencv-python==4.11.0.86", + "xrt", +] [project.optional-dependencies] dev = [ diff --git a/tests/tests_devices/test_mo1_bragg.py b/tests/tests_devices/test_mo1_bragg.py index 9dc2e31..e1fd819 100644 --- a/tests/tests_devices/test_mo1_bragg.py +++ b/tests/tests_devices/test_mo1_bragg.py @@ -52,7 +52,7 @@ def test_init(mock_bragg): dev = mock_bragg assert dev.name == "bragg" assert dev.prefix == "X01DA-OP-MO1:BRAGG:" - assert dev.crystal.offset_si111._read_pvname == "X01DA-OP-MO1:BRAGG:offset_si111_RBV" + assert dev.crystal.bragg_off_si111._read_pvname == "X01DA-OP-MO1:BRAGG:bragg_off_si111_RBV" assert dev.move_abs._read_pvname == "X01DA-OP-MO1:BRAGG:move_abs" @@ -106,14 +106,14 @@ def test_set_xtal(mock_bragg): dev = mock_bragg dev.set_xtal("111") # Default values for mock - assert dev.crystal.offset_si111.get() == 0 - assert dev.crystal.offset_si311.get() == 0 + assert dev.crystal.bragg_off_si111.get() == 0 + assert dev.crystal.bragg_off_si311.get() == 0 assert dev.crystal.d_spacing_si111.get() == 0 assert dev.crystal.d_spacing_si311.get() == 0 assert dev.crystal.xtal_enum.get() == 0 - dev.set_xtal("311", offset_si111=1, offset_si311=2, d_spacing_si111=3, d_spacing_si311=4) - assert dev.crystal.offset_si111.get() == 1 - assert dev.crystal.offset_si311.get() == 2 + dev.set_xtal("311", bragg_off_si111=1, bragg_off_si311=2, d_spacing_si111=3, d_spacing_si311=4) + assert dev.crystal.bragg_off_si111.get() == 1 + assert dev.crystal.bragg_off_si311.get() == 2 assert dev.crystal.d_spacing_si111.get() == 3 assert dev.crystal.d_spacing_si311.get() == 4 assert dev.crystal.xtal_enum.get() == 1