diff --git a/config_params.py b/config_params.py index 21d765b3..f72ac853 100644 --- a/config_params.py +++ b/config_params.py @@ -37,6 +37,7 @@ # GUI default configuration BEAM_CHECK = "beamCheck" UNMOUNT_COLD_CHECK = "unmountColdCheck" +SET_ENERGY_CHECK = "setEnergyCheck" # raster request status updates diff --git a/daq_macros.py b/daq_macros.py index e43f2394..3e3af66c 100644 --- a/daq_macros.py +++ b/daq_macros.py @@ -38,6 +38,7 @@ import bluesky.plans as bp from bluesky.preprocessors import finalize_wrapper from fmx_annealer import govStatusGet, govStateSet, fmxAnnealer, amxAnnealer # for using annealer specific to FMX and AMX +from setenergy_lsdc import setELsdc try: import ispybLib @@ -85,6 +86,14 @@ def abortBS(): RE.abort() except super_state_machine.errors.TransitionError: logger.error("caught BS") + +def set_energy(energy): + try: + daq_lib.set_field("program_state","Setting Energy") + RE(setELsdc(energy)) + except Exception as e: + logger.error(f"Exception while running set_energy: {e}") + daq_lib.set_field("program_state","Program Ready") def move_omega(omega, relative=True): """Moves omega by a certain amount""" diff --git a/daq_main_common.py b/daq_main_common.py index 132a86fc..d303e26d 100755 --- a/daq_main_common.py +++ b/daq_main_common.py @@ -85,7 +85,8 @@ def setGovState(state): unlatchGov, backoffDetector, enableMount, - robotOn + robotOn, + set_energy ] whitelisted_functions: "Dict[str, Callable]" = { diff --git a/gui/control_main.py b/gui/control_main.py index 31666465..82540b31 100644 --- a/gui/control_main.py +++ b/gui/control_main.py @@ -30,6 +30,7 @@ RASTER_GUI_XREC_FILL_DELAY, SAMPLE_TIMER_DELAY, SERVER_CHECK_DELAY, + SET_ENERGY_CHECK, VALID_DET_DIST, VALID_EXP_TIMES, VALID_TOTAL_EXP_TIMES, @@ -48,6 +49,7 @@ PuckDialog, RasterExploreDialog, ScreenDefaultsDialog, + SetEnergyDialog, SnapCommentDialog, StaffScreenDialog, UserScreenDialog, @@ -562,13 +564,20 @@ def createSampleTab(self): ) self.energy_ledit = self.energyMoveLedit.getEntry() self.energy_ledit.setValidator(QtGui.QDoubleValidator()) - self.energy_ledit.returnPressed.connect(self.moveEnergyCB) + self.energy_ledit.returnPressed.connect(self.moveEnergyMaxDeltaCB) moveEnergyButton = QtWidgets.QPushButton("Move Energy") moveEnergyButton.clicked.connect(self.moveEnergyCB) hBoxColParams3.addWidget(colEnergyLabel) hBoxColParams3.addWidget(self.energyReadback) hBoxColParams3.addWidget(energySPLabel) - hBoxColParams3.addWidget(self.energy_ledit) + if daq_utils.beamline == "fmx": + if getBlConfig(SET_ENERGY_CHECK): + hBoxColParams3.addWidget(moveEnergyButton) + else: + hBoxColParams3.addWidget(self.energy_ledit) + else: + hBoxColParams3.addWidget(self.energy_ledit) + hBoxColParams22.addWidget(colTransmissionLabel) hBoxColParams22.addWidget(self.transmissionReadback_ledit) hBoxColParams22.addWidget(transmisionSPLabel) @@ -2702,13 +2711,24 @@ def moveOmegaCB(self): {"relative": False} ) - def moveEnergyCB(self): + def moveEnergyMaxDeltaCB(self, max_delta=10.0): energyRequest = float(str(self.energy_ledit.text())) - if abs(energyRequest - self.energy_pv.get()) > 10.0: - self.popupServerMessage("Energy change must be less than 10 ev") - return + if self.controlEnabled(): + if abs(energyRequest - self.energy_pv.get()) > max_delta: + self.popupServerMessage(f"Energy change must be less than or equal to {max_delta:.2f} ev") + return + else: + self.send_to_server("mvaDescriptor", ["energy", float(self.energy_ledit.text())]) + comm_s = 'mvaDescriptor("energy",' + str(self.energy_ledit.text()) + ")" + logger.info(comm_s) else: - self.send_to_server("mvaDescriptor", ["energy", float(self.energy_ledit.text())]) + self.popupServerMessage("You don't have control") + + def moveEnergyCB(self): + if self.controlEnabled(): + set_energy = SetEnergyDialog(parent=self) + else: + self.popupServerMessage("You don't have control") def setLifetimeCB(self, lifetime): if hasattr(self, "sampleLifetimeReadback_ledit"): @@ -5167,6 +5187,11 @@ def printServerMessage(self, message_s): print(message_s) def colorProgramState(self, programState_s): + if programState_s == "Setting Energy": + self.setEnabled(False) + else: + self.setEnabled(True) + if programState_s.find("Ready") == -1: self.statusLabel.setColor("yellow") else: diff --git a/gui/dialog/__init__.py b/gui/dialog/__init__.py index 4a490c57..b50f1daa 100644 --- a/gui/dialog/__init__.py +++ b/gui/dialog/__init__.py @@ -5,3 +5,4 @@ from .puck_dialog import PuckDialog from .dewar import DewarDialog from .screen_defaults import ScreenDefaultsDialog +from .set_energy import SetEnergyDialog diff --git a/gui/dialog/set_energy.py b/gui/dialog/set_energy.py new file mode 100644 index 00000000..66a5f3c1 --- /dev/null +++ b/gui/dialog/set_energy.py @@ -0,0 +1,138 @@ +import logging +import typing + +from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor +from qtpy import QtCore, QtGui, QtWidgets +from qtpy.QtCore import Qt + +import daq_utils +import db_lib + +if typing.TYPE_CHECKING: + from lsdcGui import ControlMain + +logger = logging.getLogger() + + +class DCM(Device): + b = Cpt(EpicsMotor, "-Ax:B}Mtr", labels=["fmx"]) + g = Cpt(EpicsMotor, "-Ax:G}Mtr", labels=["fmx"]) + p = Cpt(EpicsMotor, "-Ax:P}Mtr", labels=["fmx"]) + r = Cpt(EpicsMotor, "-Ax:R}Mtr", labels=["fmx"]) + e = Cpt(EpicsMotor, "-Ax:E}Mtr", labels=["fmx"]) + + +class SetEnergyDialog(QtWidgets.QDialog): + energy_changed_signal = QtCore.Signal(object) + + def __init__(self, parent: "ControlMain"): + self.hdcm = DCM("XF:17IDA-OP:FMX{Mono:DCM", name="hdcm") + super().__init__(parent) + self._parent = parent + self.initUI() + + def initUI(self): + layout = QtWidgets.QGridLayout() + self.current_energy_label = QtWidgets.QLabel("Current Energy: ") + self.current_energy_value_label = QtWidgets.QLabel( + f"{self.hdcm.e.user_readback.get():.2f} eV" + ) + layout.addWidget(self.current_energy_label, 0, 0) + layout.addWidget(self.current_energy_value_label, 0, 1) + + self.setpoint_label = QtWidgets.QLabel("Energy setpoint: ") + validator = QtGui.QDoubleValidator() + validator.setBottom(5000) + validator.setTop(15000) + self.setpoint_edit = QtWidgets.QLineEdit() + self.setpoint_edit.setValidator(validator) + self.setpoint_edit.returnPressed.connect(self.check_value) + layout.addWidget(self.setpoint_label, 1, 0) + layout.addWidget(self.setpoint_edit, 1, 1) + + self.message = QtWidgets.QLabel("") + layout.addWidget(self.message, 2, 0, 1, 2) + + self.monochromator_button = QtWidgets.QPushButton("Monochromator") + self.monochromator_button.setAutoDefault(False) + self.monochromator_button.clicked.connect(self.set_monochromator_energy) + + self.full_alignment_button = QtWidgets.QPushButton("Full Alignment") + self.full_alignment_button.setAutoDefault(False) + self.full_alignment_button.clicked.connect(self.set_full_alignment_energy) + + self.close_button = QtWidgets.QPushButton("Close") + self.close_button.clicked.connect(self.close) + self.close_button.setAutoDefault(False) + + layout.addWidget(self.monochromator_button, 3, 0) + layout.addWidget(self.full_alignment_button, 3, 1) + layout.addWidget(self.close_button, 4, 0, 1, 2) + + self.hdcm.e.user_readback.subscribe(self.update_energy, run=True) + + self.energy_changed_signal.connect( + lambda value: self.current_energy_value_label.setText(f"{value:.2f}") + ) + + self.setLayout(layout) + self.setModal(True) + self.show() + + def update_energy(self, value, old_value, **kwargs): + self.energy_changed_signal.emit(value) + + def check_value(self): + if abs(float(self.setpoint_edit.text()) - self.hdcm.e.user_readback.get()) > 10: + self.message.setText( + "Energy change is greater than 10 eV.\nMonochromator cannot be used for alignment" + ) + self.monochromator_button.setDisabled(True) + else: + self.message.setText("Energy change less than 10 eV") + self.monochromator_button.setDisabled(False) + + if float(self.setpoint_edit.text()) <= 10000: + self.message.setText( + f"{self.message.text()} \n Beam shape not automatically optimized \nfor energies between 5 keV and 10 keV" + ) + + def unmount_cold_dialog(self): + msg_box = QtWidgets.QMessageBox() + msg_box.setText("A sample is mounted. Unmount cold?") + msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel) # type: ignore + msg_box.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok) + return msg_box + + def set_full_alignment_energy(self): + if self._parent.mountedPin_pv.get(): + # If sample is mounted, ask user to unmount cold + response = self.unmount_cold_dialog().exec_() + + if response == QtWidgets.QMessageBox.Ok: + self._parent.send_to_server("unmountCold") + else: + return + + if self._parent.governorMessage.getEntry().text() not in [ + "state SE", + "state BA", + "state BL", + "state XF", + "state SA", + ]: + self.message.setText("Governor not in a valid state, call staff!") + return + else: + self._parent.send_to_server("setGovState", ['SA']) + + self._parent.send_to_server(f"set_energy", [float(self.setpoint_edit.text())]) + + def set_monochromator_energy(self): + if abs(float(self.setpoint_edit.text()) - self.hdcm.e.user_readback.get()) > 10: + self.message.setText( + "Energy change is greater than 10 eV.\nMonochromator cannot be used for alignment" + ) + else: + self._parent.send_to_server("mvaDescriptor", ["energy", float(self.setpoint_edit.text())]) diff --git a/gui/dialog/staff_screen.py b/gui/dialog/staff_screen.py index 8362f8e0..b0f673ab 100644 --- a/gui/dialog/staff_screen.py +++ b/gui/dialog/staff_screen.py @@ -4,9 +4,14 @@ from qtpy import QtCore, QtWidgets from qtpy.QtWidgets import QCheckBox -from config_params import BEAM_CHECK, TOP_VIEW_CHECK, UNMOUNT_COLD_CHECK +from config_params import ( + BEAM_CHECK, + SET_ENERGY_CHECK, + TOP_VIEW_CHECK, + UNMOUNT_COLD_CHECK, +) from daq_utils import getBlConfig, setBlConfig -import db_lib +import daq_utils if typing.TYPE_CHECKING: from lsdcGui import ControlMain @@ -65,6 +70,17 @@ def __init__(self, parent: "ControlMain", **kwargs): self.gripperUnmountColdCheckBox.setEnabled(False) self.gripperUnmountColdCheckBox.setChecked(False) + # Set energy checkbox + if daq_utils.beamline == "fmx": + self.set_energy_checkbox = QCheckBox("Set Energy") + hBoxColParams1.addWidget(self.set_energy_checkbox) + if getBlConfig(SET_ENERGY_CHECK) == 1: + self.set_energy_checkbox.setChecked(True) + else: + self.set_energy_checkbox.setChecked(False) + self.set_energy_checkbox.stateChanged.connect(self.set_energy_check_cb) + + self.queueCollectOnCheckBox = QCheckBox("Queue Collect") hBoxColParams1.addWidget(self.queueCollectOnCheckBox) self.checkQueueCollect() @@ -297,6 +313,18 @@ def beamCheckOnCheckCB(self, state): setBlConfig(BEAM_CHECK, 0) logger.debug(f"{BEAM_CHECK} off") + def set_energy_check_cb(self, state): + if state == QtCore.Qt.Checked: + setBlConfig(SET_ENERGY_CHECK, 1) + logger.debug(f"{SET_ENERGY_CHECK} on") + else: + setBlConfig(SET_ENERGY_CHECK, 0) + logger.debug(f"{SET_ENERGY_CHECK} off") + msg_box = QtWidgets.QMessageBox() + msg_box.setText("Set Energy state changed, please restart the GUI to access feature") + msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) # type: ignore + msg_box.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok) + def unmountColdCheckCB(self, state): if state == QtCore.Qt.Checked: logger.info("unmountColdCheckCB On") diff --git a/setenergy_lsdc.py b/setenergy_lsdc.py index 21f68bd6..09f5fc13 100644 --- a/setenergy_lsdc.py +++ b/setenergy_lsdc.py @@ -16,6 +16,7 @@ import socket import time import gov_lib +import daq_lib from start_bs import db, gov_robot, govs import logging @@ -86,16 +87,21 @@ class DCM(Device): # w = Cpt(EpicsMotor, '-Ax:W}Mtr', labels=['fmx']) class XYPitchMotor(XYMotor): - pitch = Cpt(EpicsMotor, '-Ax:P}Mtr') + pitch = Cpt(EpicsMotor, '-Ax:P}Mtr') + config = Cpt(EpicsSignal, '-PS}:FORMAT') + status_monitor = Cpt(EpicsSignal, "-PS}:UNIT_STATUS_MON.A") + class KBMirror(Device): - hp = Cpt(EpicsMotor, ':KBH-Ax:P}Mtr') - hr = Cpt(EpicsMotor, ':KBH-Ax:R}Mtr') - hx = Cpt(EpicsMotor, ':KBH-Ax:X}Mtr') - hy = Cpt(EpicsMotor, ':KBH-Ax:Y}Mtr') - vp = Cpt(EpicsMotor, ':KBV-Ax:P}Mtr') - vx = Cpt(EpicsMotor, ':KBV-Ax:X}Mtr') - vy = Cpt(EpicsMotor, ':KBV-Ax:Y}Mtr') + hp = Cpt(EpicsMotor, ':KBH-Ax:P}Mtr') + hr = Cpt(EpicsMotor, ':KBH-Ax:R}Mtr') + hx = Cpt(EpicsMotor, ':KBH-Ax:X}Mtr') + hy = Cpt(EpicsMotor, ':KBH-Ax:Y}Mtr') + vp = Cpt(EpicsMotor, ':KBV-Ax:P}Mtr') + vx = Cpt(EpicsMotor, ':KBV-Ax:X}Mtr') + vy = Cpt(EpicsMotor, ':KBV-Ax:Y}Mtr') + config = Cpt(EpicsSignal, ':KB-PS}:FORMAT') + status_monitor = Cpt(EpicsSignal, ":KB-PS}:UNIT_STATUS_MON.A") class Cover(Device): @@ -116,6 +122,32 @@ class GoniometerStack(Device): py = Cpt(EpicsMotor, '-Ax:PY}Mtr', labels=['fmx']) pz = Cpt(EpicsMotor, '-Ax:PZ}Mtr', labels=['fmx']) +class PitchHold(Device): + pitch_control = Cpt(EpicsSignal, "}pitch_control") + mono_scan_freq = Cpt(EpicsSignal, ":mono}pitch-SCAN") + mono_max_tries = Cpt(EpicsSignal, ":mono}pitch-NUM") + mono_deadband = Cpt(EpicsSignal, ":mono}pitch-DB") + mono_target = Cpt(EpicsSignal, ":mono}pitch-SP") # Should be set to hdcm.p + bragg_control = Cpt(EpicsSignal, "}bragg_control") + bpm1_mon = Cpt(EpicsSignal, "}bpm1mon") + set_kb_config = Cpt(EpicsSignal, "}kb_bimorph") + set_hfm_config = Cpt(EpicsSignal, "}hfm_bimorph") + + def save_settings(self): + self.settings = {} + self.settings['pitch_control'] = self.pitch_control.get() + self.settings['bragg_control'] = self.bragg_control.get() + self.settings['bpm1_mon'] = self.bpm1_mon.get() + + def restore_settings(self): + self.pitch_control.put(self.settings['pitch_control']) + self.bragg_control.put(self.settings['bragg_control']) + self.bpm1_mon.put(self.settings['bpm1_mon']) + + +step_volts = EpicsSignal("XF:17IDA-BI:FMX{Best:1}:TwkCh1.INPA") +pitch_hold = PitchHold("XF:17ID:FMX{Karen", name="pitch_hold") + ## Horizontal Double Crystal Monochromator (FMX) hdcm = DCM('XF:17IDA-OP:FMX{Mono:DCM', name='hdcm') @@ -411,9 +443,31 @@ def setE_motors_FMX(energy): LGP = {m: epics.caget(LGP_fmt.format(m.name)) for m in (kbm.hp, kbm.hx, kbm.vp, kbm.vy)} + prev_hfm_config = hfm.config.get() + prev_kb_config = kbm.config.get() + + label = "highE" # Remove CRLs if going to energy < 9 keV (FMX specific) - if energy < 9001: - set_beamsize('V0','H0') + if energy < 10000: + # set_beamsize('V0','H0') + label = "lowE" + + if prev_hfm_config != f"HFM_{label}": + yield from bps.abs_set(hfm.config, f"HFM_{label}") + yield from bps.abs_set(pitch_hold.set_hfm_config, 1, wait=True) + + if prev_kb_config != f"KB_{label}": + yield from bps.abs_set(kbm.config, f"KB_{label}") + yield from bps.abs_set(pitch_hold.set_kb_config, 1, wait=True) + + logger.info("Waiting for 60 seconds for ramping") + time.sleep(60) + + #while int(kbm.status_monitor.get()) & 2 ** 30 != 1 and int(hfm.status_monitor.get()) & 2 ** 30 != 1: + # logger.info("Waiting for ramping to complete... ") + # time.sleep(1) + + # Lookup Table def lut(motor): @@ -670,6 +724,8 @@ def setELsdc(energy, slits1XGapOrg = slits1.x_gap.user_readback.get() slits1YGapOrg = slits1.y_gap.user_readback.get() + yield from deactivate_pitch_hold() + print('Setting FMX motor positions') try: yield from setE_motors_FMX(energy) @@ -703,15 +759,17 @@ def setELsdc(energy, print('ivu_gap_scan() successful') time.sleep(1) - # Activate sector 17 photon local feedback - photon_local_feedback_c17.x_enable.put(1) - photon_local_feedback_c17.y_enable.put(1) + + # Hold pitch + yield from activate_pitch_hold() # Align LSDC microscope center to beam center if beamCenterAlign: # Check for pre-conditions for beam_center_align() if shutter_hutch_c.status.get(): - print('Experiment hutch shutter closed. Has to be open for this to work. Exiting') + message = 'Experiment hutch shutter closed. Has to be open for this to work. Stopping' + logging.error(message) + daq_lib.gui_message(message) return -1 print('Aligning beam center') @@ -723,6 +781,7 @@ def setELsdc(energy, yield from bps.mv(slits1.y_gap, slits1YGapOrg) # Move Slit 1 Y to original position yield from fmx_reference(transSet=transSet) + daq_lib.gui_message(f"Set energy to {energy} complete") # Alignment =========================================================================================== @@ -1071,7 +1130,7 @@ def slit1_flux_reference(flux_df,slit1Gap): def fmx_flux_reference(slit1GapList = [2000, 1000, 600, 400], slit1GapDefault = 1000, transSet='All'): """ Sets Slit 1 X gap and Slit 1 Y gap to a list of settings, - and stores flux reference values in a global pandas DataFrame. + and returns flux reference values in a pandas DataFrame. Parameters ---------- @@ -1166,6 +1225,22 @@ def fmx_flux_reference(slit1GapList = [2000, 1000, 600, 400], slit1GapDefault = yield from trans_set(transOrgBCU, trans=trans_bcu) +def deactivate_pitch_hold(): + pitch_hold.save_settings() + yield from bps.abs_set(pitch_hold.bragg_control, 0, wait=True) + yield from bps.abs_set(pitch_hold.bpm1_mon, 0, wait=True) + yield from bps.abs_set(pitch_hold.pitch_control, 0, wait=True) + +def activate_pitch_hold(): + yield from bps.abs_set(step_volts, "0.01", wait=True) + yield from bps.abs_set(pitch_hold.mono_scan_freq, 1, wait=True) + yield from bps.abs_set(pitch_hold.mono_max_tries, 100, wait=True) + yield from bps.abs_set(pitch_hold.mono_deadband, 0.0003, wait=True) + yield from bps.abs_set(pitch_hold.mono_target, hdcm.p.user_readback.get(), wait=True) + + # Reactivate pitch control + pitch_hold.restore_settings() + def fmx_reference(slit1GapDefault = 1000, transSet='All'): """ Calls fmx_flux_reference, then fmx_beamline_reference.