diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 5f9181b..aebec23 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -104,7 +104,7 @@ DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [ ("B", CHANNELREFERENCE.A), ("C", CHANNELREFERENCE.T0), # T0 ("D", CHANNELREFERENCE.C), - ("E", CHANNELREFERENCE.D), # D One extra pulse once shutter closes for MCS + ("E", CHANNELREFERENCE.B), # B One extra pulse once shutter closes for MCS ("F", CHANNELREFERENCE.E), # E + 1mu s ("G", CHANNELREFERENCE.T0), ("H", CHANNELREFERENCE.G), @@ -213,8 +213,23 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # NOTE Burst delay should be set to 0, don't remove as this will not be checked # Also set the burst count to 1 to only have a single pulse for DDG1. + # As the IOC may be out of sync with the HW, we make sure that we set the default parameters + # in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out + # of sync. + self.burst_delay.put(1) + time.sleep(0.02) # Give HW time to process self.burst_delay.put(0) + time.sleep(0.02) + + self.burst_count.put(2) + time.sleep(0.02) self.burst_count.put(1) + time.sleep(0.02) + + self.burst_mode.put(1) + time.sleep(0.02) + self.burst_mode.put(0) + time.sleep(0.02) def keep_shutter_open_during_scan(self, open: True) -> None: """ @@ -291,17 +306,24 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # Burst Period DDG1 # Set burst_period to shutter width # c/t0 + self._shutter_to_open_delay + exp_time * burst_count - shutter_width = ( - self._shutter_to_open_delay + exp_time * frames_per_trigger - ) # Shutter starts closing at end of exposure + # SHUTTER WIDTH timing consists of the delay for the shutter to open + # + the exposure time * frames per trigger + shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger + # TOTAL EXPOSURE accounts for the shutter to open AND close. In addition, we add + # a short additional delay of 3e-6 to allow for the extra trigger through 'ef' + # (delay of 1e-6, width of 1e-6) + total_exposure_time = 2 * self._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6 if self.burst_period.get() != shutter_width: - self.burst_period.put(shutter_width) + # The burst_period has to be slightly longer + self.burst_period.put(total_exposure_time) # Trigger DDG2 # a = t0 + 2ms, b = a + 1us # a has reference to t0, b has reference to a - # Add delay of self._shutter_to_open_delay to allow shutter to open - self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6) + # AB is delayed by the shutter opening time, and the falling edge indicates the shutter has + # fully closed, it has to be considered as the blocking signal for the next acquisition to start. + # PS: + 3e-6 + self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=shutter_width) # Trigger shutter # d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms @@ -321,7 +343,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): if self.scan_info.msg.scan_type == "fly": self.set_delay_pairs(channel="ef", delay=0, width=0) else: - self.set_delay_pairs(channel="ef", delay=0, width=1e-6) + self.set_delay_pairs(channel="ef", delay=1e-6, width=1e-6) # NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly # This value has been choosen empirically after testing with the HW. It's diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py index e9ee1da..5790d9c 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py @@ -29,6 +29,7 @@ from ophyd_devices import DeviceStatus, StatusBase from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import ( + BURSTCONFIG, CHANNELREFERENCE, OUTPUTPOLARITY, STATUSBITS, @@ -37,7 +38,6 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import ChannelConfig, DelayGeneratorCSAXS, LiteralChannels, - BURSTCONFIG, ) logger = bec_logger.logger @@ -138,6 +138,24 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): # Set burst config self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value) + # TODO As the IOC may be out of sync with the HW, we make sure that we set the default parameters + # in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out + # of sync. + self.burst_delay.put(1) + time.sleep(0.02) # Give HW time to process + self.burst_delay.put(0) + time.sleep(0.02) + + self.burst_count.put(2) + time.sleep(0.02) + self.burst_count.put(1) + time.sleep(0.02) + + self.burst_mode.put(1) + time.sleep(0.02) + self.burst_mode.put(0) + time.sleep(0.02) + def on_stage(self) -> DeviceStatus | StatusBase | None: """ diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py index bfe2c48..971e115 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Callable, Literal import numpy as np from bec_lib.logger import bec_logger +from ophyd.utils.errors import WaitTimeoutError from ophyd import Component as Cpt from ophyd import EpicsSignalRO, Kind from ophyd_devices import ( @@ -513,7 +514,22 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): # that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition # was already finished (i.e. normal step scan sends 1 extra pulse per burst cycle), this will # not have any effect as the card will already be in DONE state and signal. - self.software_channel_advance.put(1) + if self.scan_info.msg.scan_type == "fly": + expected_points = int( + self.scan_info.msg.num_points + * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) + ) + + status = CompareStatus(self.current_channel, expected_points-1, operation_success=">=") + try: + status.wait(timeout=5) + except WaitTimeoutError: + text = f"Device {self.name} received num points {self.current_channel.get()} / {expected_points}. Device timed out after 5s." + logger.error(text) + raise TimeoutError(text) + + # Manually set the last advance + self.software_channel_advance.put(1) # Prepare and register status callback for the async monitoring loop status_async_data = StatusBase(obj=self) diff --git a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py index e769e14..ad2c638 100644 --- a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py +++ b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py @@ -92,7 +92,8 @@ class RtFlomniController(Controller): parent._min_scan_buffer_reached = False start_time = time.time() for pos_index, pos in enumerate(positions): - parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}") + cmd = f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}" + parent.socket_put_and_receive(cmd) if pos_index > 100: parent._min_scan_buffer_reached = True parent._min_scan_buffer_reached = True @@ -174,11 +175,12 @@ class RtFlomniController(Controller): self.set_device_read_write("foptx", False) self.set_device_read_write("fopty", False) - def move_samx_to_scan_region(self, fovx: float, cenx: float): - #new routine not using fovx anymore - self.device_manager.devices.rtx.obj.move(cenx, wait=True) + def move_samx_to_scan_region(self, cenx: float, move_in_this_routine: bool = False): + # attention. a movement will clear all positions in the rt trajectory generator! + if move_in_this_routine == True: + self.device_manager.devices.rtx.obj.move(cenx, wait=True) time.sleep(0.05) - #at cenx we expect the PID to be close to zero for a good fsamx position + # at cenx we expect the PID to be close to zero for a good fsamx position if self.rt_pid_voltage is None: rtx = self.device_manager.devices.rtx self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage") @@ -188,29 +190,31 @@ class RtFlomniController(Controller): ) logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}") expected_voltage = self.rt_pid_voltage - #logger.info(f"Expected PID voltage: {expected_voltage}") + # logger.info(f"Expected PID voltage: {expected_voltage}") logger.info(f"Current PID voltage: {self.get_pid_x()}") wait_on_exit = False - #we allow 2V range from center, this corresponds to 30 microns + # we allow 2V range from center, this corresponds to 30 microns if np.abs(self.get_pid_x() - expected_voltage) < 2: logger.info("No correction of fsamx needed") else: fsamx = self.device_manager.devices.fsamx fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") while True: - #when we correct, then to 1 V, within 15 microns + # when we correct, then to 1 V, within 15 microns if np.abs(self.get_pid_x() - expected_voltage) < 1: logger.info("No further correction needed") break wait_on_exit = True - #disable FZP piezo feedback + # disable FZP piezo feedback self.socket_put("v0") fsamx.read_only = False logger.info(f"Current PID voltage: {self.get_pid_x()}") - #here we accumulate the correction + # here we accumulate the correction fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.006 fsamx_in = fsamx.user_parameter.get("in") - logger.info(f"Moving fsamx to {cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction}, PID portion of that {fsamx.obj.pid_x_correction}") + logger.info( + f"Moving fsamx to {cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction}, PID portion of that {fsamx.obj.pid_x_correction}" + ) fsamx.obj.move(fsamx_in + cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction, wait=True) fsamx.read_only = True time.sleep(0.1) @@ -219,7 +223,7 @@ class RtFlomniController(Controller): if wait_on_exit: time.sleep(1) - #enable fast FZP feedback again + # enable fast FZP feedback again self.socket_put("v1") @threadlocked @@ -510,7 +514,7 @@ class RtFlomniController(Controller): # while scan is running while mode > 0: - #TODO here?: scan abortion if no progress in scan *raise error + # TODO here?: scan abortion if no progress in scan *raise error # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() diff --git a/csaxs_bec/scans/flomni_fermat_scan.py b/csaxs_bec/scans/flomni_fermat_scan.py index 099464b..5e616b7 100644 --- a/csaxs_bec/scans/flomni_fermat_scan.py +++ b/csaxs_bec/scans/flomni_fermat_scan.py @@ -165,7 +165,8 @@ class FlomniFermatScan(SyncFlyScanBase): if self.flomni_rotation_status: self.flomni_rotation_status.wait() - rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False) + # rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False) + rtx_status = yield from self.stubs.set(device="rtx", value=self.cenx, wait=False) rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False) yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on") @@ -173,13 +174,15 @@ class FlomniFermatScan(SyncFlyScanBase): rtx_status.wait() rtz_status.wait() + # status = yield from self.stubs.send_rpc("rtx", "move", self.cenx) + # status.wait() yield from self._transfer_positions_to_flomni() - yield from self.stubs.send_rpc_and_wait( - "rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx - ) tracker_signal_status = yield from self.stubs.send_rpc_and_wait( "rtx", "controller.laser_tracker_check_signalstrength" ) + yield from self.stubs.send_rpc_and_wait( + "rtx", "controller.move_samx_to_scan_region", self.cenx + ) # self.device_manager.connector.send_client_info(tracker_signal_status) if tracker_signal_status == "low": error_info = messages.ErrorInfo( @@ -313,7 +316,11 @@ class FlomniFermatScan(SyncFlyScanBase): # in flomni, we need to move to the start position of the next scan, which is the end position of the current scan # this method is called in finalize and overwrites the default move_to_start() if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3: - yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1]) + # yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1]) + # in x we move to cenx, then we avoid jumps in centering routine + value = self.positions[-1] + value[0] = self.cenx + yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=value) return logger.warning("No positions found to return to start") diff --git a/tests/tests_devices/test_delay_generator_csaxs.py b/tests/tests_devices/test_delay_generator_csaxs.py index 6ba4ecb..230b86e 100644 --- a/tests/tests_devices/test_delay_generator_csaxs.py +++ b/tests/tests_devices/test_delay_generator_csaxs.py @@ -287,19 +287,20 @@ def test_ddg1_stage(mock_ddg1: DDG1): mock_ddg1.stage() shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger + total_exposure = 2 * mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6 assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled assert np.isclose(mock_ddg1.burst_delay.get(), 0) - assert np.isclose(mock_ddg1.burst_period.get(), shutter_width) + assert np.isclose(mock_ddg1.burst_period.get(), total_exposure) # Trigger DDG2 through EXT/EN assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3) - assert np.isclose(mock_ddg1.ab.width.get(), 1e-6) + assert np.isclose(mock_ddg1.ab.width.get(), shutter_width) # Shutter channel cd assert np.isclose(mock_ddg1.cd.delay.get(), 0) assert np.isclose(mock_ddg1.cd.width.get(), shutter_width) # MCS channel ef or gate - assert np.isclose(mock_ddg1.ef.delay.get(), 0) + assert np.isclose(mock_ddg1.ef.delay.get(), 1e-6) assert np.isclose(mock_ddg1.ef.width.get(), 1e-6) assert mock_ddg1.staged == ophyd.Staged.yes