diff --git a/csaxs_bec/device_configs/bl_detectors.yaml b/csaxs_bec/device_configs/bl_detectors.yaml index 80a159e..14243e7 100644 --- a/csaxs_bec/device_configs/bl_detectors.yaml +++ b/csaxs_bec/device_configs/bl_detectors.yaml @@ -13,8 +13,8 @@ eiger_9: description: Eiger 9M detector deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M deviceConfig: - detector_distance: 2200 - beam_center: [870, 1203] + detector_distance: 2150 + beam_center: [860, 1219] onFailure: raise enabled: True readoutPriority: async diff --git a/csaxs_bec/device_configs/user_setup.yaml b/csaxs_bec/device_configs/user_setup.yaml index bff8be9..6129d05 100644 --- a/csaxs_bec/device_configs/user_setup.yaml +++ b/csaxs_bec/device_configs/user_setup.yaml @@ -15,7 +15,7 @@ eyex: user_offset_dir: 0 deviceTags: - cSAXS - - owis_samx + - owis_eyex onFailure: buffer enabled: true readoutPriority: baseline @@ -33,7 +33,7 @@ eyey: user_offset_dir: 0 deviceTags: - cSAXS - - owis_samx + - owis_eyey onFailure: buffer enabled: true readoutPriority: baseline @@ -42,11 +42,11 @@ samx: description: Owis motor stage samx deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME deviceConfig: - prefix: X12SA-ES2-ES18 - motor_resolution: 0.000125 - base_velocity: 0.00625 - velocity: 1 - backlash_distance: 0.0125 + prefix: X12SA-ES2-ES03 + motor_resolution: 0.00125 + base_velocity: 0.0625 + velocity: 10 + backlash_distance: 0.125 acceleration: 0.2 user_offset_dir: 0 deviceTags: @@ -60,20 +60,56 @@ samy: description: Owis motor stage samx deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME deviceConfig: - prefix: X12SA-ES2-ES19 - motor_resolution: 0.000125 - base_velocity: 0.00625 - velocity: 1 - backlash_distance: 0.0125 + prefix: X12SA-ES2-ES04 + motor_resolution: 0.00125 + base_velocity: 0.0625 + velocity: 10 + backlash_distance: 0.125 acceleration: 0.2 user_offset_dir: 0 deviceTags: - cSAXS - - owis_samx + - owis_samy onFailure: buffer enabled: true readoutPriority: baseline softwareTrigger: false +# samx: +# description: Owis motor stage samx +# deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME +# deviceConfig: +# prefix: X12SA-ES2-ES18 +# motor_resolution: 0.000125 +# base_velocity: 0.00625 +# velocity: 1 +# backlash_distance: 0.0125 +# acceleration: 0.2 +# user_offset_dir: 0 +# deviceTags: +# - cSAXS +# - owis_samx +# onFailure: buffer +# enabled: true +# readoutPriority: baseline +# softwareTrigger: false +# samy: +# description: Owis motor stage samx +# deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME +# deviceConfig: +# prefix: X12SA-ES2-ES19 +# motor_resolution: 0.000125 +# base_velocity: 0.00625 +# velocity: 1 +# backlash_distance: 0.0125 +# acceleration: 0.2 +# user_offset_dir: 0 +# deviceTags: +# - cSAXS +# - owis_samx +# onFailure: buffer +# enabled: true +# readoutPriority: baseline +# softwareTrigger: false # eye_cam: # description: Camera Microscope # deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera 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 597b032..b4455a2 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -55,7 +55,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import LiteralChannels, StatusBitsCompareStatus, ) -from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING +from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING, suppress_mca_callbacks from csaxs_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma: no cover @@ -135,7 +135,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None. """ - USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger", "get_shutter_to_open_delay"] + USER_ACCESS = [ + "keep_shutter_open_during_scan", + "set_trigger", + "get_shutter_to_open_delay", + "prepare_mcs_on_trigger", + ] # TODO Consider using the 'fsh' device instead. fast_shutter_readback = Cpt( @@ -273,6 +278,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): - We set the delay pairs ef to be triggered after the shutter closes with a width of 1us to trigger the MCS card. - Finally, we add a short sleep to ensure that the IOC and DDG HW process the values properly. """ + logger.info(f"DDG {self.name} on_stage called.") self.scan_parameters = fetch_scan_info(self.scan_info) start_time = time.time() @@ -359,7 +365,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): time.sleep(0.2) logger.info(f"DDG {self.name} on_stage completed in {time.time() - start_time:.3f}s.") - def _prepare_mcs_on_trigger(self, mcs: MCSCardCSAXS) -> None: + def prepare_mcs_on_trigger(self) -> CompareStatus: """ This method is used by the DDG1 on_trigger method to prepare the MCS card for the next trigger. @@ -368,6 +374,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): It relies on the MCS card implementation and needs to be adapted if the MCS card logic changes. """ + mcs_name = "mcs" + if self.device_manager is None or not hasattr(self.device_manager, "devices"): + raise ValueError("Device manager is not properly initialized.") + mcs = self.device_manager.devices.get(mcs_name, None) + if mcs is None: + raise ValueError(f"MCS card {mcs_name} not found in device manager.") # NOTE First we wait that the MCS card is not acquiring. We add here a timeout of 5s to avoid # a deadlock in case the MCS card is stuck for some reason. This should not happen normally. @@ -378,10 +390,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # NOTE Clear the '_omit_mca_callbacks' flag. This makes sure that data received from the mca1...mca3 # counters are forwarded to BEC. Once the flag is set, we create a TransitionStatus DONE->ACQUIRING # and start the acquisition through erase_start.put(1). Finally, we wait for the card to go to ACQUIRING state. - mcs._omit_mca_callbacks.clear() # pylint: disable=protected-access status_acquiring = CompareStatus(mcs.acquiring, ACQUIRING.ACQUIRING) + with suppress_mca_callbacks(mcs): + mcs.erase_start.put(1) + mcs._omit_mca_callbacks.clear() # pylint: disable=protected-access + self.cancel_on_stop(status_acquiring) - mcs.erase_start.put(1) return status_acquiring @@ -564,7 +578,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # NOTE If the MCS card is present in the current session of BEC, # we prepare the card for the next trigger. The procedure is implemented - # in the '_prepare_mcs_on_trigger' method. We will also use the mcs card + # in the 'prepare_mcs_on_trigger' method. We will also use the mcs card # to indicate when the burst cycle is done. If no mcs card is available # the fallback is to use the polling of the DDG mcs = self.device_manager.devices.get("mcs", None) @@ -583,18 +597,36 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): else: start_time = time.time() logger.debug(f"Preparing mcs card ") - status_mcs = self._prepare_mcs_on_trigger(mcs) + status_mcs = self.prepare_mcs_on_trigger() # NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash # an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should # be investigated why the EPICS interface is slow to respond. try: + # current_time = time.time() + # while time.time() - current_time < 3 and mcs.acquiring.get() != ACQUIRING.ACQUIRING: + # time.sleep(0.1) + # logger.warning( + # f"MCS card is not in acquiring state, current state: {mcs.acquiring.get()}" + # ) + # if mcs.acquiring.get() != ACQUIRING.ACQUIRING: + # logger.error( + # f"MCS card is not finishing after 3s, current state: {mcs.acquiring.get()}, waiting another 3s for its return to ACQUIRING state. If this happens regularly, please check the EPICS interface and the MCS card logic." + # ) + if not status_mcs.done: + mcs.acquiring.get(use_monitor=False) status_mcs.wait(timeout=3) except Exception as exc: - logger.warning(f"MCS did not go to Acquiring within 3s. Retrying erase_start {exc}") - mcs.erase_start.put(1) - status_mcs.wait(timeout=3) - status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE]) - logger.debug(f"Finished preparing mcs card {time.time()-start_time}") + if ( + mcs.acquiring.get(use_monitor=False) != ACQUIRING.ACQUIRING + ): # Get the current state without monitoring to avoid any side effects + logger.warning( + f"MCS did not go to Acquiring within 3s. Retrying erase_start {exc}" + ) + raise exc + # mcs.erase_start.put(1) + # status_mcs.wait(timeout=3) + status = CompareStatus(mcs.acquiring, ACQUIRING.DONE) + logger.info(f"Finished preparing mcs card {time.time()-start_time}") # Send trigger self.trigger_shot.put(1, use_complete=True) 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 c42d610..0141d5d 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py @@ -173,6 +173,7 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): This logic is robust for step scans as well as fly scans, as the DDG2 is triggered by the DDG1 through the EXT/EN channel. """ + logger.info(f"DDG {self.name} on_stage called.") start_time = time.time() self.scan_parameters = fetch_scan_info(self.scan_info) ######################################## diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py b/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py index 4789f5c..a5cdeff 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py @@ -508,6 +508,7 @@ class DelayGeneratorCSAXS(Device): EpicsSignal, "TriggerDelayBO", name="trigger_shot", + put_complete=True, kind=Kind.omitted, doc="Software trigger, needs to be in correct mode to work", ) diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card.py b/csaxs_bec/devices/epics/mcs_card/mcs_card.py index ef0360e..6d18dfb 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card.py @@ -164,6 +164,8 @@ class MCSCard(Device): - EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html """ + WRITE_TIMEOUT = 4.0 # seconds + snl_connected = Cpt( EpicsSignalRO, "SNL_Connected", @@ -175,18 +177,21 @@ class MCSCard(Device): EpicsSignal, "EraseAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0. Please note that this operation sends the mca or waveform records to process after erasing, potentially also 0s.", ) erase_start = Cpt( EpicsSignal, "EraseStart", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records and starts acquisition.", ) start_all = Cpt( EpicsSignal, "StartAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Starts or resumes acquisition without erasing first.", ) acquiring = Cpt( @@ -196,11 +201,18 @@ class MCSCard(Device): auto_monitor=True, doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.", ) - stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.") + stop_all = Cpt( + EpicsSignal, + "StopAll", + kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, + doc="Stops acquisition.", + ) preset_real = Cpt( EpicsSignal, "PresetReal", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Preset real time. If non-zero then acquisition will stop when this time is reached.", ) elapsed_real = Cpt( @@ -213,72 +225,84 @@ class MCSCard(Device): EpicsSignal, "DoReadAll.VAL", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.", ) read_mode = Cpt( EpicsSignal, "ReadAll.SCAN", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.", ) num_use_all = Cpt( EpicsSignal, "NuseAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.", ) dwell = Cpt( EpicsSignal, "Dwell", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The dwell time per channel when using internal channel advance mode.", ) channel_advance = Cpt( EpicsSignal, "ChannelAdvance", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).", ) count_on_start = Cpt( EpicsSignal, "CountOnStart", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.", ) software_channel_advance = Cpt( EpicsSignal, "SoftwareChannelAdvance", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.", ) channel1_source = Cpt( EpicsSignal, "Channel1Source", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.", ) prescale = Cpt( EpicsSignal, "Prescale", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.", ) enable_client_wait = Cpt( EpicsSignal, "EnableClientWait", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.", ) client_wait = Cpt( EpicsSignal, "ClientWait", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.", ) acquire_mode = Cpt( EpicsSignal, "AcquireMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.", ) # NOTE: Setting mux_output programmatically results in occasional errors on the IOC; it is recommended to avoid using it. @@ -286,36 +310,42 @@ class MCSCard(Device): EpicsSignal, "MUXOutput", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3. NOTE: This settings seems to occasionally result in errors on the IOC; it is recommended to avoid using it.", ) user_led = Cpt( EpicsSignal, "UserLED", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Toggles the user LED and also output signal 8 on the SIS3820.", ) input_mode = Cpt( EpicsSignal, "InputMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.", ) input_polarity = Cpt( EpicsSignal, "InputPolarity", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.", ) output_mode = Cpt( EpicsSignal, "OutputMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.", ) output_polarity = Cpt( EpicsSignal, "OutputPolarity", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.", ) model = Cpt( 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 afc4aa1..9948784 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -68,13 +68,14 @@ def suppress_mca_callbacks(mcs_card: MCSCard, restore_after_timeout: None | floa clear the suppression after the specified time. If None, the original state is not restored. """ - mcs_card._omit_mca_callbacks.set() # pylint: disable=protected-access - try: - yield - finally: - if restore_after_timeout is not None: - time.sleep(restore_after_timeout) - mcs_card._omit_mca_callbacks.clear() # pylint: disable=protected-access + with mcs_card._rlock: + mcs_card._omit_mca_callbacks.set() # pylint: disable=protected-access + try: + yield + finally: + if restore_after_timeout is not None: + time.sleep(restore_after_timeout) + mcs_card._omit_mca_callbacks.clear() # pylint: disable=protected-access if TYPE_CHECKING: # pragma: no cover @@ -97,7 +98,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): prefix (str, optional): Prefix for the EPICS PVs. Defaults to "". """ - USER_ACCESS = ["mcs_recovery"] + USER_ACCESS = ["mcs_recovery", "get_transition_status", "get_compare_status"] # NOTE The number of MCA channels is fixed to 32 for the CSAXS MCS card. # On the IOC, we receive a 'warning' or 'error' once we set this channel for the @@ -297,7 +298,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): # Once we have received all channels, push data to BEC and reset for next accumulation if len(self._current_data) == self.NUM_MCA_CHANNELS: - logger.debug( + logger.info( f"Current data index {self._current_data_index} complete, pushing to BEC." ) self.mca.put(self._current_data, acquisition_group=self._acquisition_group) @@ -342,6 +343,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): - Clear any events and buffers related to async data emission. This includes '_omit_mca_callbacks', '_start_monitor_async_data_emission', '_scan_done_callbacks', and '_current_data'. """ + logger.info(f"MCS Card {self.name} on_stage called.") start_time = time.time() self.scan_parameters = fetch_scan_info(self.scan_info) @@ -379,11 +381,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._num_lines = self.scan_parameters.additional_scan_parameters.get("num_lines", 1) self._current_line = 1 self._acquisition_group = "monitored" if triggers == 1 else "burst_group" - self.preset_real.set(0).wait(timeout=self._pv_timeout) + self.preset_real.set(0).wait() # TODO consider using put... if self.scan_parameters.scan_type == "software_triggered": - self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) + self.num_use_all.set(triggers).wait() elif self.scan_parameters.scan_type == "hardware_triggered": - self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout) + self.num_use_all.set(self._num_total_triggers).wait() # Clear any previous data, just to be sure with self._rlock: @@ -459,6 +461,9 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): and self.scan_parameters.num_points is not None ): if self.scan_parameters.scan_type == "software_triggered": + logger.info( + f"Software triggered scan: {self._current_data_index}/{self.scan_parameters.num_points} points received." + ) if self._current_data_index == self.scan_parameters.num_points: for callback in self._scan_done_callbacks: callback(exception=None) @@ -482,13 +487,12 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._start_monitor_async_data_emission.clear() # Stop monitoring # NOTE Important check as set_finished or set_exception should not be called # if the status is already done (e.g. cancelled externally) - with self._rlock: - if status.done: - return # Already done and cancelled externally. - if exception is not None: - status.set_exception(exception) - else: - status.set_finished() + if status.done: + return # Already done and cancelled externally. + if exception is not None: + status.set_exception(exception) + else: + status.set_finished() def _status_failed_callback(self, status: StatusBase) -> None: """Callback for status failure, the monitoring thread should be stopped.""" @@ -543,7 +547,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self.software_channel_advance.put(1) # Prepare and register status callback for the async monitoring loop - status_async_data = StatusBase(obj=self) + status_async_data = StatusBase(obj=self, timeout=5) self._scan_done_callbacks.append(partial(self._status_callback, status_async_data)) # Set the event to start monitoring async data emission @@ -551,12 +555,14 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._start_monitor_async_data_emission.set() # Add CompareStatus for Acquiring DONE - status = CompareStatus(self.acquiring, ACQUIRING.DONE) + status = CompareStatus(self.acquiring, ACQUIRING.DONE, timeout=5) + # status.wait(timeout=3) # timeout is passed to individual status objects, so don't wait here. # Combine both statuses ret_status = status & status_async_data # NOTE: Handle external stop/cancel, and stop monitoring ret_status.add_callback(self._status_failed_callback) + # ret_status.wait(timeout=3) # timeout is passed to individual status objects, so don't wait here. self.cancel_on_stop(ret_status) return ret_status @@ -601,3 +607,32 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): # We restore the callback suppression after timeout to ensure proper operation afterwards. with suppress_mca_callbacks(self, restore_after_timeout=sleep_time): self.erase_all.put(1) + + def get_transition_status(self, signal_name: str, transition: list) -> TransitionStatus: + """ + Get the transition status for a signal of the device. + + Args: + signal_name: The name of the signal to check the transition status for. + transition (list): List of transitions to check. + """ + signal = getattr(self, signal_name, None) + if signal is None: + raise ValueError(f"Signal {signal_name} not found in device {self.name}.") + return TransitionStatus(signal, transition) + + def get_compare_status( + self, signal_name: str, expected_value: any, operation_success: str = "==" + ) -> CompareStatus: + """ + Get the compare status for a signal of the device. + + Args: + signal_name: The name of the signal to check the compare status for. + expected_value: The expected value to compare against. + operation_success: The comparison operation to use. Defaults to "==". + """ + signal = getattr(self, signal_name, None) + if signal is None: + raise ValueError(f"Signal {signal_name} not found in device {self.name}.") + return CompareStatus(signal, expected_value, operation_success=operation_success) diff --git a/csaxs_bec/devices/jungfraujoch/eiger.py b/csaxs_bec/devices/jungfraujoch/eiger.py index eccb857..07c27c2 100644 --- a/csaxs_bec/devices/jungfraujoch/eiger.py +++ b/csaxs_bec/devices/jungfraujoch/eiger.py @@ -270,6 +270,7 @@ class Eiger(PSIDeviceBase): """ Hook called when staging the device. Information about the upcoming scan can be accessed from the scan_info object. """ + logger.info(f"Device {self.name} on_stage called.") start_time = time.time() self.scan_parameters = fetch_scan_info(self.scan_info) @@ -332,7 +333,7 @@ class Eiger(PSIDeviceBase): logger.debug(f"Setting data_settings: {yaml.dump(data_settings.to_dict(), indent=4)}") prep_time = time.time() self.jfj_client.wait_for_idle(timeout=10) # Ensure we are in IDLE state - self.jfj_client.start(settings=data_settings) # Takes around ~0.6s + self.jfj_client.start(settings=data_settings) # Takes around ~0.6s -> 6s currently. # Time the stage process logger.info( diff --git a/csaxs_bec/scans/scans_v4/cont_grid.py b/csaxs_bec/scans/scans_v4/cont_grid.py index d85c0a1..2863fbb 100644 --- a/csaxs_bec/scans/scans_v4/cont_grid.py +++ b/csaxs_bec/scans/scans_v4/cont_grid.py @@ -17,6 +17,7 @@ Scan procedure: from __future__ import annotations import time +from copy import deepcopy from typing import Annotated, TypedDict import numpy as np @@ -185,26 +186,28 @@ class ContGrid(ScanBase): ) positions = position_generators.nd_grid_positions( [ - (self.fast_start, self.fast_end, frames_per_trigger), (self.stepper_start, self.stepper_stop, self._cont_motor_params["num_lines"]), + (self.fast_start, self.fast_end, frames_per_trigger), ], snaked=False, ) # Count only the end point of each line as a valid position, as the fast axis is continuously moving and only triggered at # the beginning of the line moving to the end point. - self.positions = positions[(frames_per_trigger - 1) :: frames_per_trigger, :] + positions = positions[:, ::-1] # Get device specific parameters self._fetch_device_params() # Adjust relative positions if needed if self.relative: self.start_positions = self.components.get_start_positions(self.motors) - self.positions += self.start_positions + positions += self.start_positions self.fast_start += self.start_positions[0] self.fast_end += self.start_positions[0] self.stepper_start += self.start_positions[1] self.stepper_stop += self.start_positions[1] + self.positions = deepcopy(positions[(frames_per_trigger - 1) :: frames_per_trigger, :]) + # Adjust premove self.fast_start -= self._cont_motor_params["premove_distance"] self.fast_end += self._cont_motor_params["premove_distance"] @@ -265,10 +268,27 @@ class ContGrid(ScanBase): """ # Only use every second position, at each point will use for line_index in range(self._cont_motor_params["num_lines"]): - self.actions.set( - self.motors, [self.fast_start, self.positions[line_index][1]], wait=True + line_start = time.time() + status_mcs = self.ddg1.prepare_mcs_on_trigger() + status_motor = self.actions.set( + self.motors, [self.fast_start, self.positions[line_index][1]] + ) + status_motor.wait() + logger.info( + f"Overhead from motor motion for line {line_index}: {(time.time() - line_start):.02f}s" + ) + status_mcs.wait() + mcs_aquiring_status = self.mcs.get_transition_status( + signal_name="acquiring", transition=[1, 0] + ) + logger.info( + f"Overhead before calling trigger for line {line_index}: {(time.time() - line_start):.02f}s" + ) + self.at_each_point( + motors=[self.fast_axis], + positions=np.array([self.fast_end]), + status_mcs=mcs_aquiring_status, ) - self.at_each_point(motors=[self.fast_axis], positions=np.array([self.fast_end])) self._restore_motor_properties() @scan_hook @@ -276,6 +296,7 @@ class ContGrid(ScanBase): self, motors: list[str | DeviceBase], positions: np.ndarray, + status_mcs, last_positions: np.ndarray | None = None, ): """ @@ -287,7 +308,7 @@ class ContGrid(ScanBase): self.fast_axis.acceleration.set(self._cont_motor_params["acc_time"]).wait(timeout=5) move_status = self.actions.set(motors, positions, wait=False) time.sleep(self._cont_motor_params["acc_time"]) - trigger_status = self.ddg1.trigger() + self.ddg1.trigger_shot.put(1) while not move_status.done: self.actions.read_monitored_devices(wait=True) try: @@ -296,10 +317,10 @@ class ContGrid(ScanBase): continue try: - trigger_status.wait(timeout=2) + status_mcs.wait(timeout=3) except TimeoutError as exc: raise ScanAbortion( - f"Status for delay generator trigger {self.ddg1.name} did not resolve after 2 seconds. " + f"MCS card did not go back to DONE after receiving all triggers and an extra 3 seconds. " ) from exc @scan_hook diff --git a/tests/tests_devices/test_delay_generator_csaxs.py b/tests/tests_devices/test_delay_generator_csaxs.py index 6410cfa..a484dae 100644 --- a/tests/tests_devices/test_delay_generator_csaxs.py +++ b/tests/tests_devices/test_delay_generator_csaxs.py @@ -260,7 +260,7 @@ def test_ddg1_prepare_mcs(mock_ddg1: DDG1, mock_mcs_csaxs: MCSCardCSAXS): mcs.erase_start.put(0) # reset erase start # Prepare MCS on trigger - st = ddg._prepare_mcs_on_trigger(mcs) + st = ddg.prepare_mcs_on_trigger() assert st.done is False assert st.success is False assert mcs.erase_start.get() == 1 # erase started @@ -356,16 +356,16 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1): ################################# # Scenario I - normal operation # ################################# - with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs: + with mock.patch.object(ddg, "prepare_mcs_on_trigger") as mock_prepare_mcs: + # Set acquioring PV to acquiring + mcs = ddg.device_manager.devices.get("mcs", None) + assert mcs is not None + mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started mock_prepare_mcs.return_value = ophyd.StatusBase(done=True, success=True) # MCS card is present and enabled, should call prepare_mcs_on_trigger # and the status should resolve once acuiring goes from 1 to 0. status = ddg.trigger() assert status.done is False - mcs = ddg.device_manager.devices.get("mcs", None) - assert mcs is not None - mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started - assert status.done is False mcs.acquiring._read_pv.mock_data = 0 # Simulate acquiring stopped status.wait(timeout=1) # Wait for the status to be done assert status.done is True @@ -407,7 +407,7 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1): ddg.state.event_status._read_pv.mock_data = STATUSBITS.ABORT_DELAY.value ddg._start_polling() assert ddg._poll_thread_run_event.is_set() - with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs: + with mock.patch.object(ddg, "prepare_mcs_on_trigger") as mock_prepare_mcs: status = ddg.trigger() mock_prepare_mcs.assert_not_called() # MCS is disabled, should not be called assert status.done is False