diff --git a/csaxs_bec/device_configs/bl_optics_hutch.yaml b/csaxs_bec/device_configs/bl_optics_hutch.yaml index 5241a95..67a9ea3 100644 --- a/csaxs_bec/device_configs/bl_optics_hutch.yaml +++ b/csaxs_bec/device_configs/bl_optics_hutch.yaml @@ -203,46 +203,52 @@ ccm_energy: ######################## SMARACT STAGES ################################## ########################################################################## -xbpm2x: - description: X-ray beam position monitor 1 in OPbox - deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor - deviceConfig: - axis_Id: A - host: x12sa-eb-smaract-mcs-03.psi.ch - limits: - - -200 - - 200 - port: 5000 - sign: 1 - enabled: true - onFailure: retry - readOnly: false - readoutPriority: baseline - connectionTimeout: 20 - userParameter: - # bl_smar_stage to use csaxs reference method. assign number according to axis channel - init_position: 22.5 - bl_smar_stage: 0 +# xbpm2x: +# description: X-ray beam position monitor 1 in OPbox +# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor +# deviceConfig: +# axis_Id: A +# host: x12sa-eb-smaract-mcs-03.psi.ch +# limits: +# - -200 +# - 200 +# port: 5000 +# sign: 1 +# enabled: true +# onFailure: retry +# readOnly: false +# readoutPriority: baseline +# connectionTimeout: 20 +# userParameter: +# init_position: 22.5 +# in_position: -1.5 +# # bl_smar_stage to use csaxs reference method. assign number according to axis channel +# bl_smar_stage: 0 + +# xbpm2y: +# description: X-ray beam position monitor 1 in OPbox +# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor +# deviceConfig: +# axis_Id: B +# host: x12sa-eb-smaract-mcs-03.psi.ch +# limits: +# - -200 +# - 200 +# port: 5000 +# sign: 1 +# enabled: true +# onFailure: retry +# readOnly: false +# readoutPriority: baseline +# connectionTimeout: 20 +# userParameter: +# in_position: -1.0 +# # bl_smar_stage to use csaxs reference method. assign number according to axis channel +# bl_smar_stage: 1 + + + -xbpm2y: - description: X-ray beam position monitor 1 in OPbox - deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor - deviceConfig: - axis_Id: B - host: x12sa-eb-smaract-mcs-03.psi.ch - limits: - - -200 - - 200 - port: 5000 - sign: 1 - enabled: true - onFailure: retry - readOnly: false - readoutPriority: baseline - connectionTimeout: 20 - userParameter: - # bl_smar_stage to use csaxs reference method. assign number according to axis channel - bl_smar_stage: 1 scinx: description: scintillator in OPbox @@ -265,26 +271,47 @@ scinx: init_position: -23 bl_smar_stage: 2 -# poly: -# description: polarizer holder in OPbox -# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor -# deviceConfig: -# axis_Id: D -# host: x12sa-eb-smaract-mcs-03.psi.ch -# limits: -# - -200 -# - 200 -# port: 5000 -# sign: 1 -# enabled: true -# onFailure: retry -# readOnly: false -# readoutPriority: baseline -# connectionTimeout: 20 -# userParameter: -# # bl_smar_stage to use csaxs reference method. assign number according to axis channel -# init_position: -23 -# bl_smar_stage: 3 +poly: + description: polarizer holder in OPbox + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor + deviceConfig: + axis_Id: B + host: x12sa-eb-smaract-mcs-03.psi.ch + limits: + - -200 + - 200 + port: 5000 + sign: 1 + enabled: true + onFailure: retry + readOnly: false + readoutPriority: baseline + connectionTimeout: 20 + userParameter: + # bl_smar_stage to use csaxs reference method. assign number according to axis channel + init_position: -23 + bl_smar_stage: 1 + +polrot: + description: rotation of crytal of the polarizer + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor + deviceConfig: + axis_Id: A + host: x12sa-eb-smaract-mcs-03.psi.ch + limits: + - -200 + - 200 + port: 5000 + sign: 1 + enabled: true + onFailure: retry + readOnly: false + readoutPriority: baseline + connectionTimeout: 20 + userParameter: + in_position: -1.0 + # bl_smar_stage to use csaxs reference method. assign number according to axis channel + bl_smar_stage: 0 # dmm1_trx_readback_example: # This is the same template as for i.e. bpm4i # description: 'This is an example of a read-only Epics signal' diff --git a/csaxs_bec/device_configs/main.yaml b/csaxs_bec/device_configs/main.yaml index bf175f4..ee85882 100644 --- a/csaxs_bec/device_configs/main.yaml +++ b/csaxs_bec/device_configs/main.yaml @@ -25,136 +25,7 @@ detectors: #lamni: # - !include ./ptycho_lamni.yaml -#user setup: -# - !include ./user_setup.yaml +user setup: + - !include ./user_setup.yaml + -eyex: - description: Owis motor stage samx - deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME - deviceConfig: - prefix: X12SA-ES2-ES01 - 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 - onFailure: buffer - enabled: true - readoutPriority: baseline - softwareTrigger: false -eyey: - description: Owis motor stage samx - deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME - deviceConfig: - prefix: X12SA-ES2-ES02 - 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 - 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 -# deviceConfig: -# camera_id: 1 -# bits_per_pixel: 8 -# num_rotation_90: 1 -# transpose: false -# force_monochrome: false -# m_n_colormode: 1 -# enabled: true -# onFailure: buffer -# readOnly: false -# readoutPriority: async -smarx: - description: sample position x with smaract - deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor - deviceConfig: - axis_Id: G - host: x12sa-eb-smaract-mcs-05.psi.ch - limits: - - -200 - - 200 - port: 5000 - sign: 1 - # precision: 3 - # tolerance: 0.005 - enabled: true - onFailure: retry - readOnly: false - readoutPriority: baseline - connectionTimeout: 20 - userParameter: - #init_position: 0 - # bl_smar_stage to use csaxs reference method. assign number according to axis channel - bl_smar_stage: 6 -smary: - description: sample position y with smaract - deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor - deviceConfig: - axis_Id: H - host: x12sa-eb-smaract-mcs-05.psi.ch - limits: - - -200 - - 200 - port: 5000 - sign: 1 - # precision: 3 - # tolerance: 0.005 - enabled: true - onFailure: retry - readOnly: false - readoutPriority: baseline - connectionTimeout: 20 - userParameter: - #init_position: 0 - # bl_smar_stage to use csaxs reference method. assign number according to axis channel - bl_smar_stage: 7 diff --git a/csaxs_bec/device_configs/user_setup.yaml b/csaxs_bec/device_configs/user_setup.yaml index 1e6681d..bff8be9 100644 --- a/csaxs_bec/device_configs/user_setup.yaml +++ b/csaxs_bec/device_configs/user_setup.yaml @@ -2,14 +2,133 @@ -############################################################ -##################### EPS ################################## -############################################################ -x12saEPS: - description: X12SA EPS info and control - deviceClass: csaxs_bec.devices.epics.eps.EPS - deviceConfig: {} - enabled: true +eyex: + description: Owis motor stage samx + deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME + deviceConfig: + prefix: X12SA-ES2-ES01 + 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 onFailure: buffer + enabled: true + readoutPriority: baseline + softwareTrigger: false +eyey: + description: Owis motor stage samx + deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME + deviceConfig: + prefix: X12SA-ES2-ES02 + 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 + 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 +# deviceConfig: +# camera_id: 1 +# bits_per_pixel: 8 +# num_rotation_90: 1 +# transpose: false +# force_monochrome: false +# m_n_colormode: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: async +smarx: + description: sample position x with smaract + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor + deviceConfig: + axis_Id: G + host: x12sa-eb-smaract-mcs-05.psi.ch + limits: + - -200 + - 200 + port: 5000 + sign: 1 + # precision: 3 + # tolerance: 0.005 + enabled: true + onFailure: retry readOnly: false - readoutPriority: baseline \ No newline at end of file + readoutPriority: baseline + connectionTimeout: 20 + userParameter: + #init_position: 0 + # bl_smar_stage to use csaxs reference method. assign number according to axis channel + bl_smar_stage: 6 +smary: + description: sample position y with smaract + deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor + deviceConfig: + axis_Id: H + host: x12sa-eb-smaract-mcs-05.psi.ch + limits: + - -200 + - 200 + port: 5000 + sign: 1 + # precision: 3 + # tolerance: 0.005 + enabled: true + onFailure: retry + readOnly: false + readoutPriority: baseline + connectionTimeout: 20 + userParameter: + #init_position: 0 + # bl_smar_stage to use csaxs reference method. assign number according to axis channel + bl_smar_stage: 7 \ No newline at end of file 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 aebec23..597b032 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -37,6 +37,7 @@ import traceback from typing import TYPE_CHECKING from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import EpicsSignalRO, Kind from ophyd_devices import CompareStatus, DeviceStatus, StatusBase, TransitionStatus @@ -55,6 +56,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import StatusBitsCompareStatus, ) from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING +from csaxs_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import DeviceManagerBase, ScanInfo @@ -133,7 +135,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None. """ - USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"] + USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger", "get_shutter_to_open_delay"] # TODO Consider using the 'fsh' device instead. fast_shutter_readback = Cpt( @@ -172,6 +174,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): self._poll_thread_poll_loop_done = threading.Event() self._poll_thread_kill_event = threading.Event() self._poll_thread.start() + self.scan_parameters: ScanServerScanInfo | None = None # pylint: disable=attribute-defined-outside-init def on_connected(self) -> None: @@ -231,6 +234,10 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): self.burst_mode.put(0) time.sleep(0.02) + def get_shutter_to_open_delay(self) -> float: + """Get the current delay that is set to open the shutter before the exposure time.""" + return self._shutter_to_open_delay + def keep_shutter_open_during_scan(self, open: True) -> None: """ Method to configure the delay generator for keeping the shutter open during a scans. @@ -266,6 +273,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. """ + self.scan_parameters = fetch_scan_info(self.scan_info) start_time = time.time() ######################################## @@ -300,8 +308,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): ### Setup timing for burst and delays ### ######################################### - frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"] - exp_time = self.scan_info.msg.scan_parameters["exp_time"] + frames_per_trigger = self.scan_parameters.frames_per_trigger + exp_time = self.scan_parameters.exp_time # Burst Period DDG1 # Set burst_period to shutter width @@ -340,7 +348,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # Trigger extra pulse for MCS OR gate # f = e + 1us # e has refernce to d, f has reference to e - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": self.set_delay_pairs(channel="ef", delay=0, width=0) else: self.set_delay_pairs(channel="ef", delay=1e-6, width=1e-6) @@ -371,7 +379,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # 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 = TransitionStatus(mcs.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING]) + status_acquiring = CompareStatus(mcs.acquiring, ACQUIRING.ACQUIRING) self.cancel_on_stop(status_acquiring) mcs.erase_start.put(1) @@ -494,9 +502,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): """ if timeout is None: # Default timeout of 5 seconds + exposure time * frames_per_trigger - timeout = 5 + self.scan_info.msg.scan_parameters.get( - "exp_time", 0.1 - ) * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) + timeout = 5 + self.scan_parameters.exp_time * self.scan_parameters.frames_per_trigger # Callback to cancel the status if the device is stopped def cancel_cb(status: CompareStatus) -> None: @@ -562,7 +568,11 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # 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) - if mcs is None or mcs.enabled is False or self.scan_info.msg.scan_type == "fly": + if ( + mcs is None + or mcs.enabled is False + or self.scan_parameters.scan_type == "hardware_triggered" + ): self._poll_thread_poll_loop_done.wait(timeout=1) logger.warning("Did not find mcs card with name 'mcs' in current session") time.sleep(0.02) @@ -577,7 +587,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # 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. - status_mcs.wait(timeout=3) + try: + 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}") 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 5790d9c..c42d610 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py @@ -25,6 +25,7 @@ Burst mode is enabled: import time from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd_devices import DeviceStatus, StatusBase from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase @@ -39,6 +40,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import DelayGeneratorCSAXS, LiteralChannels, ) +from csaxs_bec.devices.utils.utils import fetch_scan_info logger = bec_logger.logger @@ -111,6 +113,10 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): distributes the trigger to the detectors. The DDG2 is triggered by the DDG1 through the EXT/EN channel. """ + def on_init(self) -> None: + """Initialize the device""" + self.scan_parameters: ScanServerScanInfo | None = None + # pylint: disable=attribute-defined-outside-init def on_connected(self) -> None: """ @@ -168,6 +174,7 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): through the EXT/EN channel. """ start_time = time.time() + self.scan_parameters = fetch_scan_info(self.scan_info) ######################################## ### Burst mode settings ################ ######################################## @@ -180,8 +187,8 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): if self.burst_delay.get() != 0: self.burst_delay.put(0) - exp_time = self.scan_info.msg.scan_parameters["exp_time"] - frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"] + exp_time = self.scan_parameters.exp_time + frames_per_trigger = self.scan_parameters.frames_per_trigger # NOTE Check if the exposure time is longer than all readout times. # Raise a ValueError if requested exposure time is too short. 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 c416d06..afc4aa1 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -20,9 +20,10 @@ 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 bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import EpicsSignalRO, Kind +from ophyd.utils.errors import WaitTimeoutError from ophyd_devices import ( AsyncMultiSignal, CompareStatus, @@ -43,6 +44,7 @@ from csaxs_bec.devices.epics.mcs_card.mcs_card import ( READMODE, MCSCard, ) +from csaxs_bec.devices.utils.utils import fetch_scan_info @contextmanager @@ -168,6 +170,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._mca_counter_index: int = 0 self._current_data: dict[str, dict[Literal["value", "timestamp"], list[int] | float]] = {} self._omit_mca_callbacks: threading.Event = threading.Event() + self.scan_parameters: ScanServerScanInfo | None = None def on_connected(self): """ @@ -315,6 +318,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): value: New value of the signal. """ try: + if ( + self._num_lines > 1 and old_value > value + ): # This indicates that we have moved to the next line in a cont scan + self._current_line += 1 + value = value + (self._current_line - 1) * self.scan_parameters.frames_per_trigger scan_done = bool(value == self._num_total_triggers) self.progress.put(value=value, max_value=self._num_total_triggers, done=scan_done) except Exception: @@ -335,6 +343,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): '_start_monitor_async_data_emission', '_scan_done_callbacks', and '_current_data'. """ start_time = time.time() + self.scan_parameters = fetch_scan_info(self.scan_info) # NOTE: If for some reason, the card is still acquiring, we need to stop it first # This should never happen as the card is properly stopped during unstage @@ -364,14 +373,16 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): ##################################### ### Setup Acquisition Parameters ### ##################################### - triggers = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) - num_points = self.scan_info.msg.num_points + triggers = self.scan_parameters.frames_per_trigger + num_points = self.scan_parameters.num_points self._num_total_triggers = triggers * num_points + 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) - if self.scan_info.msg.scan_type == "step": + if self.scan_parameters.scan_type == "software_triggered": self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) - elif self.scan_info.msg.scan_type == "fly": + elif self.scan_parameters.scan_type == "hardware_triggered": self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout) # Clear any previous data, just to be sure @@ -395,7 +406,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._omit_mca_callbacks.clear() # For a fly scan we need to start the mcs card ourselves - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": self.erase_start.put(1) logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.") @@ -406,7 +417,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): that the card is properly started for fly scans. For step scans, this will be handled by the DDG, so no action is required here. """ - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": status_acquiring = CompareStatus(self.acquiring, ACQUIRING.ACQUIRING) self.cancel_on_stop(status_acquiring) return status_acquiring @@ -444,11 +455,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): while self._start_monitor_async_data_emission.wait(): try: if ( - hasattr(self.scan_info.msg, "num_points") - and self.scan_info.msg.num_points is not None + hasattr(self.scan_parameters, "num_points") + and self.scan_parameters.num_points is not None ): - if self.scan_info.msg.scan_type == "step": - if self._current_data_index == self.scan_info.msg.num_points: + if self.scan_parameters.scan_type == "software_triggered": + if self._current_data_index == self.scan_parameters.num_points: for callback in self._scan_done_callbacks: callback(exception=None) else: @@ -513,13 +524,14 @@ 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. - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": 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=">=") + self.scan_parameters.num_points * self.scan_parameters.frames_per_trigger + ) + + status = CompareStatus( + self.current_channel, expected_points - 1, operation_success=">=" + ) try: status.wait(timeout=5) except WaitTimeoutError: diff --git a/csaxs_bec/devices/jungfraujoch/eiger.py b/csaxs_bec/devices/jungfraujoch/eiger.py index ed1a90c..e8d94d2 100644 --- a/csaxs_bec/devices/jungfraujoch/eiger.py +++ b/csaxs_bec/devices/jungfraujoch/eiger.py @@ -39,6 +39,7 @@ from typing import TYPE_CHECKING, Literal import yaml from bec_lib.file_utils import get_full_path from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from jfjoch_client.models.dataset_settings import DatasetSettings from jfjoch_client.models.detector_settings import DetectorSettings from jfjoch_client.models.detector_state import DetectorState @@ -52,6 +53,7 @@ from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from csaxs_bec.devices.jungfraujoch.jungfrau_joch_client import JungfrauJochClient from csaxs_bec.devices.jungfraujoch.jungfraujoch_preview import JungfrauJochPreview +from csaxs_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma no cover from bec_lib.devicemanager import ScanInfo @@ -117,6 +119,7 @@ class Eiger(PSIDeviceBase): self._wait_for_on_complete = 20 # seconds if self.device_manager is not None: self.device_manager: DeviceManagerDS + self.scan_parameters: ScanServerScanInfo | None = None def _preview_callback(self, message: dict) -> None: """ @@ -263,26 +266,25 @@ class Eiger(PSIDeviceBase): def on_stage(self) -> DeviceStatus | None: """ Hook called when staging the device. Information about the upcoming scan can be accessed from the scan_info object. - scan_msg = self.scan_info.msg """ start_time = time.time() - scan_msg = self.scan_info.msg + self.scan_parameters = fetch_scan_info(self.scan_info) # TODO: Check mono energy from device in BEC # Setting incident energy in keV incident_energy = 12.0 # Setting up exp_time and num_triggers acquisition parameter - exp_time = scan_msg.scan_parameters.get("exp_time", 0) + exp_time = self.scan_parameters.exp_time if exp_time <= self._readout_time: # Exp_time must be at least the readout time raise ValueError( f"Value error on device {self.name}: Exposure time {exp_time}s is less than readout time {self._readout_time}s." ) self._num_triggers = int( - scan_msg.num_points * scan_msg.scan_parameters["frames_per_trigger"] + self.scan_parameters.num_points * self.scan_parameters.frames_per_trigger ) # Setting up the full path for file writing - self._full_path = get_full_path(scan_msg, name=f"{self.name}_master") + self._full_path = get_full_path(self.scan_info.msg, name=f"{self.name}_master") self._full_path = os.path.abspath(os.path.expanduser(self._full_path)) # Inform BEC about upcoming file event diff --git a/csaxs_bec/devices/jungfraujoch/eiger_9m.py b/csaxs_bec/devices/jungfraujoch/eiger_9m.py index f206103..e8016a1 100644 --- a/csaxs_bec/devices/jungfraujoch/eiger_9m.py +++ b/csaxs_bec/devices/jungfraujoch/eiger_9m.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: # pragma no cover from bec_server.device_server.device_server import DeviceManagerDS EIGER9M_READOUT_TIME_US = 500e-6 # 500 microseconds in s -DETECTOR_NAME = "EIGER 9M" # "EIGER 9M"" +DETECTOR_NAME = "EIGER 8.5M (tmp)" # "EIGER 9M" # "EIGER 9M"" # pylint:disable=invalid-name diff --git a/csaxs_bec/devices/panda_box/panda_box.py b/csaxs_bec/devices/panda_box/panda_box.py index bd3e44d..7884532 100644 --- a/csaxs_bec/devices/panda_box/panda_box.py +++ b/csaxs_bec/devices/panda_box/panda_box.py @@ -3,9 +3,12 @@ import time from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd_devices import StatusBase from ophyd_devices.devices.panda_box.panda_box import PandaBox, PandaState +from csaxs_bec.devices.utils.utils import fetch_scan_info + logger = bec_logger.logger @@ -20,16 +23,19 @@ class PandaBoxCSAXS(PandaBox): super().on_init() self._acquisition_group = "burst" self._timeout_on_completed = 10 + self.scan_parameters: ScanServerScanInfo | None = None def on_stage(self): + self.scan_parameters = fetch_scan_info(self.scan_info) start_time = time.time() super().on_stage() + # TODO, adjust as seen fit. # Adjust the acquisition group based on scan parameters if needed - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": self._acquisition_group = "fly" - elif self.scan_info.msg.scan_type == "step": - if self.scan_info.msg.scan_parameters["frames_per_trigger"] == 1: + elif self.scan_parameters.scan_type == "software_triggered": + if self.scan_parameters.frames_per_trigger == 1: self._acquisition_group = "monitored" else: self._acquisition_group = "burst" @@ -44,8 +50,7 @@ class PandaBoxCSAXS(PandaBox): start_time = time.monotonic() try: expected_points = int( - self.scan_info.msg.num_points - * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) + self.scan_parameters.num_points * self.scan_parameters.frames_per_trigger ) while captured < expected_points: ret = self.send_raw("*PCAP.CAPTURED?") diff --git a/csaxs_bec/devices/panda_box/panda_box_omny.py b/csaxs_bec/devices/panda_box/panda_box_omny.py index fdf543f..b844834 100644 --- a/csaxs_bec/devices/panda_box/panda_box_omny.py +++ b/csaxs_bec/devices/panda_box/panda_box_omny.py @@ -3,9 +3,12 @@ import time from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd_devices import StatusBase from ophyd_devices.devices.panda_box.panda_box import PandaBox, PandaState +from csaxs_bec.devices.utils.utils import fetch_scan_info + logger = bec_logger.logger @@ -16,16 +19,18 @@ class PandaBoxOMNY(PandaBox): super().on_init() self._acquisition_group = "burst" self._timeout_on_completed = 10 + self.scan_parameters: ScanServerScanInfo | None = None def on_stage(self): start_time = time.time() super().on_stage() + self.scan_parameters = fetch_scan_info(self.scan_info) # TODO, adjust as seen fit. # Adjust the acquisition group based on scan parameters if needed - if self.scan_info.msg.scan_type == "fly": + if self.scan_parameters.scan_type == "hardware_triggered": self._acquisition_group = "fly" - elif self.scan_info.msg.scan_type == "step": - if self.scan_info.msg.scan_parameters["frames_per_trigger"] == 1: + elif self.scan_parameters.scan_type == "software_triggered": + if self.scan_parameters.frames_per_trigger == 1: self._acquisition_group = "monitored" else: self._acquisition_group = "burst" @@ -40,8 +45,7 @@ class PandaBoxOMNY(PandaBox): start_time = time.monotonic() try: expected_points = int( - self.scan_info.msg.num_points - * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) + self.scan_parameters.num_points * self.scan_parameters.frames_per_trigger ) while captured < expected_points: ret = self.send_raw("*PCAP.CAPTURED?") diff --git a/csaxs_bec/devices/utils/__init__.py b/csaxs_bec/devices/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/devices/utils/utils.py b/csaxs_bec/devices/utils/utils.py new file mode 100644 index 0000000..3e8794c --- /dev/null +++ b/csaxs_bec/devices/utils/utils.py @@ -0,0 +1,27 @@ +"""Utility functions for the devices.""" + +from copy import deepcopy + +import numpy as np +from bec_lib.devicemanager import ScanInfo +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo +from pydantic import ValidationError + + +def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: + """Fetch the scan parameters from the scan_info object and return them as a ScanServerScanInfo object.""" + info = scan_info.msg.info + if isinstance(info["positions"], list): + info["positions"] = np.array(info["positions"]) + info["num_monitored_readouts"] = scan_info.msg.num_monitored_readouts + try: + msg = ScanServerScanInfo.model_validate(info) + except ValidationError: # This means we have an old scan_info object. + info = deepcopy(info) + # We need to convert a few parameters manually. + info["scan_type"] = ( + "hardware_triggered" if info["scan_type"] == "fly" else "software_triggered" + ) + msg = ScanServerScanInfo.model_validate(info) + + return msg diff --git a/csaxs_bec/scans/__init__.py b/csaxs_bec/scans/__init__.py index 0497a31..ba6d225 100644 --- a/csaxs_bec/scans/__init__.py +++ b/csaxs_bec/scans/__init__.py @@ -3,4 +3,5 @@ from .jungfrau_joch_scan import JungfrauJochTestScan from .LamNIFermatScan import LamNIFermatScan, LamNIMoveToScanCenter from .omny_fermat_scan import OMNYFermatScan from .owis_grid import OwisGrid +from .scans_v4.cont_grid import ContGrid from .sgalil_grid import SgalilGrid diff --git a/csaxs_bec/scans/scans_v4/__init__.py b/csaxs_bec/scans/scans_v4/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/scans/scans_v4/cont_grid.py b/csaxs_bec/scans/scans_v4/cont_grid.py new file mode 100644 index 0000000..d85c0a1 --- /dev/null +++ b/csaxs_bec/scans/scans_v4/cont_grid.py @@ -0,0 +1,415 @@ +""" +Continuous grid scan with 2-axis. The scan requires the fast axis to properly implement base velocity as well as high velocity and high acceleration time + +Scan procedure: + - prepare_scan + - open_scan + - stage + - pre_scan + - scan_core + - at_each_point (optionally called by scan_core) + - post_scan + - unstage + - close_scan + - on_exception (called if any exception is raised during the scan) +""" + +from __future__ import annotations + +import time +from typing import Annotated, TypedDict + +import numpy as np +from bec_lib.device import DeviceBase +from bec_lib.logger import bec_logger +from bec_lib.scan_args import DefaultArgType, ScanArgument +from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.scans import position_generators +from bec_server.scan_server.scans.scan_base import ScanBase, ScanType +from bec_server.scan_server.scans.scan_modifier import scan_hook + +from csaxs_bec.scans.scan_customization.scan_components import CsaxsBecScanComponents + +logger = bec_logger.logger + + +class ContinuousMotorParameter(TypedDict): + """TypedDict for the parameters related to the continuous motor, which are needed to properly restore the motor state after the scan.""" + + original_velocity: float | None + original_acceleration: float | None + target_velocity: float | None + base_velocity: float | None + acc_time: float | None + premove_distance: float | None + shutter_open_delay: float | None + num_lines: int | None + + +class ContGrid(ScanBase): + # Scan Type: Hardware triggered or software triggered? + # If the main trigger and readout logic is done within the at_each_point method in scan_core, choose SOFTWARE_TRIGGERED. + # If the main trigger and readout logic is implemented on a device that is simply kicked off in this scan, choose HARDWARE_TRIGGERED. + # This primarily serves as information for devices: The device may need to react differently if a software trigger is expected + # for every point. + scan_type = ScanType.SOFTWARE_TRIGGERED + + # Scan name: This is the name of the scan, e.g. "line_scan". This is used for display purposes and to identify the scan type in user interfaces. + # Choose a descriptive name that does not conflict with existing scan names. + # It must be a valid Python identifier, that is, it can only contain letters, numbers, and underscores, and must not start with a number. + scan_name = "cont_grid" + + gui_config = { + "Scan Parameters": [ + "fast_axis", + "fast_start", + "fast_end", + "fast_step_size", + "stepper_axis", + "stepper_start", + "stepper_stop", + "stepper_step_size", + ], + "Acquisition Parameters": ["exp_time", "relative", "fast_axis_always_in_pos_dir"], + } + + def __init__( + self, + fast_axis: Annotated[ + DeviceBase, + ScanArgument(display_name="Fast Axis", description="Axis with continuous motion."), + ], + fast_start: Annotated[ + float, + ScanArgument( + display_name="Fast Start", + description="Start position for measurement points of the fast axis.", + ), + ], + fast_end: Annotated[ + float, + ScanArgument( + display_name="Fast End", + description="End position for measurement points of the fast axis.", + ), + ], + fast_step_size: Annotated[ + float, + ScanArgument( + display_name="Fast Step Size", + description="Step size for points of the continuous motion axis.", + ), + ], + stepper_axis: Annotated[ + DeviceBase, + ScanArgument( + display_name="Step Axis", + description="Step axis of the grid scan, stepping through the lines.", + ), + ], + stepper_start: Annotated[ + float, + ScanArgument(display_name="Step Start", description="Start position of the step axis."), + ], + stepper_stop: Annotated[ + float, + ScanArgument(display_name="Step Stop", description="End position of the step axis."), + ], + stepper_step_size: Annotated[ + float, + ScanArgument( + display_name="Step Step Size", + description="Step size of the step axis in units of the motor.", + ), + ], + exp_time: DefaultArgType.ExposureTime, + relative: DefaultArgType.Relative = False, + fast_axis_always_in_pos_dir: bool = True, + **kwargs, + ): + """ + Continuous grid scan with 2-axis. The scan requires the fast axis to properly implement base velocity as well as high velocity and high acceleration time + + Args: + fast_axis (DeviceBase): Axis with continuous motion. + fast_start (float): Start position for measurement points of the fast axis. + fast_end (float): End position for measurement points of the fast axis. + fast_step_size (float): Step size for points of the continuous motion axis. + stepper_axis (DeviceBase): Step axis of the grid scan, stepping through the lines. + stepper_start (float): Start position of the step axis. + stepper_stop (float): End position of the step axis. + stepper_step_size (float): Step size of the step axis in units of the motor. + relative (bool): Whether the positions are relative to the current position. Default is False, i.e. absolute positions. + exp_time (float): Exposure time in seconds + fast_axis_always_in_pos_dir (bool): Whether to always scan in the positive direction, default is True. + + Returns: + ScanReport + """ + super().__init__(**kwargs) + self.components = CsaxsBecScanComponents(self) + self._baseline_readout_status = None + self.fast_axis = fast_axis + self.fast_axis_always_in_pos_dir = fast_axis_always_in_pos_dir + if self.fast_axis_always_in_pos_dir and (fast_end < fast_start): # Switch if needed + fast_start, fast_end = fast_end, fast_start + self.fast_start = fast_start + self.fast_end = fast_end + self.fast_step_size = fast_step_size + self.stepper_axis = stepper_axis + self.stepper_start = stepper_start + self.stepper_stop = stepper_stop + self.stepper_step_size = stepper_step_size + self.relative = relative + self.exp_time = exp_time + self.ddg1 = self.device_manager.devices["ddg1"] + self.mcs = self.device_manager.devices["mcs"] + + self.motors = [fast_axis, stepper_axis] + self._cont_motor_params: ContinuousMotorParameter = ContinuousMotorParameter() + self.update_scan_info(relative=relative, exp_time=exp_time) + + @scan_hook + def prepare_scan(self): + """ + Prepare the scan. This can include any steps that need to be executed + before the scan is opened, such as preparing the positions (if not done already) + or setting up the devices. + """ + self._check_motor_inputs() + frames_per_trigger = int( + np.ceil(np.abs(self.fast_end - self.fast_start) / self.fast_step_size) + ) + self._cont_motor_params["num_lines"] = int( + np.ceil(np.abs(self.stepper_stop - self.stepper_start) / self.stepper_step_size) + ) + 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"]), + ], + 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, :] + # 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 + 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] + + # Adjust premove + self.fast_start -= self._cont_motor_params["premove_distance"] + self.fast_end += self._cont_motor_params["premove_distance"] + + self.actions.set_device_readout_priority(self.motors, priority="monitored") + self.update_scan_info( + positions=self.positions, + num_points=len(self.positions), + frames_per_trigger=frames_per_trigger, + computed_positions=positions, + num_lines=self._cont_motor_params["num_lines"], + ) + + self.actions.add_scan_report_instruction_device_progress(self.mcs) + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) + self._premove_motor_status = self.actions.set( + self.motors, [self.fast_start, self.stepper_start], wait=False + ) + + @scan_hook + def open_scan(self): + """ + Open the scan. + This step must call self.actions.open_scan() to ensure that a new scan is + opened. Make sure to prepare the scan metadata before, either in + prepare_scan() or in open_scan() itself and call self.update_scan_info(...) + to update the scan metadata if needed. + """ + self.actions.open_scan() + + @scan_hook + def stage(self): + """ + Stage the devices for the upcoming scan. The stage logic is typically + implemented on the device itself (i.e. by the device's stage method). + However, if there are any additional steps that need to be executed before + staging the devices, they can be implemented here. + """ + self.actions.stage_all_devices() + + @scan_hook + def pre_scan(self): + """ + Pre-scan steps to be executed before the main scan logic. + This is typically the last chance to prepare the devices before the core scan + logic is executed. For example, this is a good place to initialize time-criticial + devices, e.g. devices that have a short timeout. + The pre-scan logic is typically implemented on the device itself. + """ + self._premove_motor_status.wait() + self.actions.pre_scan_all_devices() + + @scan_hook + def scan_core(self): + """ + Core scan logic to be executed during the scan. + This is where the main scan logic should be implemented. + """ + # 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 + ) + self.at_each_point(motors=[self.fast_axis], positions=np.array([self.fast_end])) + self._restore_motor_properties() + + @scan_hook + def at_each_point( + self, + motors: list[str | DeviceBase], + positions: np.ndarray, + last_positions: np.ndarray | None = None, + ): + """ + Logic to be executed at each point of the scan. + This is where the main scan logic should be implemented, e.g. triggering the readout devices. + The at_each_point logic is typically implemented on the device itself. + """ + self.fast_axis.velocity.set(self._cont_motor_params["target_velocity"]).wait(timeout=5) + 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() + while not move_status.done: + self.actions.read_monitored_devices(wait=True) + try: + move_status.wait(timeout=0.5) + except TimeoutError: + continue + + try: + trigger_status.wait(timeout=2) + except TimeoutError as exc: + raise ScanAbortion( + f"Status for delay generator trigger {self.ddg1.name} did not resolve after 2 seconds. " + ) from exc + + @scan_hook + def post_scan(self): + """ + Post-scan steps to be executed after the main scan logic. + """ + self._restore_motor_properties() + status = self.actions.complete_all_devices(wait=False) + if self.relative: + self.components.move_and_wait(self.motors, self.start_positions) + status.wait() + + @scan_hook + def unstage(self): + """Unstage the scan by executing post-scan steps.""" + self.actions.unstage_all_devices() + + @scan_hook + def close_scan(self): + """Close the scan.""" + if self._baseline_readout_status is not None: + self._baseline_readout_status.wait() + self.actions.close_scan() + self.actions.check_for_unchecked_statuses() + + @scan_hook + def on_exception(self, exception: Exception): + """ + Handle exceptions that occur during the scan. + This is a good place to implement any cleanup logic that needs to be executed in case of an exception, + such as returning the devices to a safe state or moving the motors back to their starting position. + """ + self._restore_motor_properties() + if self.relative: + self.components.move_and_wait(self.motors, self.start_positions) + + ####################################################### + ######### Helper methods for the scan logic ########### + ####################################################### + + # Implement scan-specific helper methods below. + + def _check_motor_inputs(self): + """ + Check the motor inputs for validity, e.g. whether the start and stop positions are not too close for the given step size. + """ + if np.isclose(self.fast_start, self.fast_end, atol=self.fast_step_size): + raise ScanAbortion( + f"Fast stop {self.fast_end} and fast start {self.fast_start} positions are too close for the given step size {self.fast_step_size}." + ) + if np.isclose(self.stepper_start, self.stepper_stop, atol=self.stepper_step_size): + raise ScanAbortion( + f"Stepper stop {self.stepper_stop} and stepper start {self.stepper_start} positions are too close for the given step size {self.stepper_step_size}." + ) + + def _restore_motor_properties(self): + vel = self._cont_motor_params["original_velocity"] + acc = self._cont_motor_params["original_acceleration"] + if vel is not None: + self.fast_axis.velocity.put(vel) + if acc is not None: + self.fast_axis.acceleration.put(acc) + + def _fetch_device_params(self): + self._cont_motor_params["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + self._cont_motor_params["original_acceleration"] = self.fast_axis.acceleration.get() + self._cont_motor_params["original_velocity"] = self.fast_axis.velocity.get() + self._cont_motor_params["base_velocity"] = self.fast_axis.base_velocity.get() + + target_vel = self.fast_step_size / self.exp_time + if target_vel > self._cont_motor_params["original_velocity"]: + raise ScanAbortion( + f"Requested velocity of {target_vel} exceeds maximum velocity {self._cont_motor_params['original_velocity']} of motor {self.fast_axis.name}." + ) + if target_vel < self._cont_motor_params["base_velocity"]: + raise ScanAbortion( + f"Requested velocity of {target_vel} is below base velocity {self._cont_motor_params['base_velocity']}." + ) + accceleration_time = ( + (target_vel - self._cont_motor_params["base_velocity"]) + / ( + self._cont_motor_params["original_velocity"] + - self._cont_motor_params["base_velocity"] + ) + * self._cont_motor_params["original_acceleration"] + ) + self._cont_motor_params["target_velocity"] = target_vel + self._cont_motor_params["acc_time"] = accceleration_time + + if self._cont_motor_params["acc_time"] < self._cont_motor_params["shutter_open_delay"]: + extra_distance = ( + self._cont_motor_params["target_velocity"] + * self._cont_motor_params["shutter_open_delay"] + ) + # Adjust acc time to account for shutter delay + self._cont_motor_params["acc_time"] += self._cont_motor_params["shutter_open_delay"] + else: + extra_distance = 0 + self._cont_motor_params["premove_distance"] = self._compute_premove_distance( + additional_distance=extra_distance + ) + + def _compute_premove_distance(self, additional_distance: float) -> float: + return ( + 0.5 + * ( + self._cont_motor_params["target_velocity"] + + self._cont_motor_params["base_velocity"] + ) + * self._cont_motor_params["acc_time"] + + additional_distance * self._cont_motor_params["target_velocity"] + ) diff --git a/tests/tests_devices/test_delay_generator_csaxs.py b/tests/tests_devices/test_delay_generator_csaxs.py index 230b86e..6410cfa 100644 --- a/tests/tests_devices/test_delay_generator_csaxs.py +++ b/tests/tests_devices/test_delay_generator_csaxs.py @@ -43,6 +43,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import DelayGeneratorCSAXS, ) from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS +from csaxs_bec.devices.utils.utils import fetch_scan_info ############################ ### Test Delay Generator ### @@ -280,8 +281,8 @@ def test_ddg1_stage(mock_ddg1: DDG1): mock_ddg1.burst_delay.put(5) # Non-default, should be reset on stage mock_ddg1.burst_count.put(10) # Non-default, should be reset on stage - mock_ddg1.scan_info.msg.scan_parameters["exp_time"] = exp_time - mock_ddg1.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger + mock_ddg1.scan_info.msg.info["exp_time"] = exp_time + mock_ddg1.scan_info.msg.info["frames_per_trigger"] = frames_per_trigger mock_ddg1.fast_shutter_control._read_pv.mock_data = 0 # Simulate shutter control mock_ddg1.stage() @@ -315,7 +316,7 @@ def test_ddg1_stage(mock_ddg1: DDG1): shutter_width, exp_time * frames_per_trigger ) # Shutter to open delay is not added as shutter is kept open # Simulate fly scan, so no extra trigger for MCS card. - mock_ddg1.scan_info.msg.scan_type = "fly" + mock_ddg1.scan_info.msg.info["scan_type"] = "hardware_triggered" mock_ddg1.stage() # Shutter channel cd assert np.isclose(mock_ddg1.cd.delay.get(), 0) @@ -339,6 +340,11 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1): # Make sure DDG is setup in default state through on_connected ddg.on_connected() + ddg.scan_info.msg.info["scan_type"] = ( + "software_triggered" # Simulate fly scan, so no extra trigger for MCS card. + ) + ddg.scan_parameters = fetch_scan_info(ddg.scan_info) + # Check that poll thread is running and run event is not set assert ddg._poll_thread.is_alive() assert not ddg._poll_thread_run_event.is_set() @@ -514,8 +520,8 @@ def test_ddg2_on_stage(mock_ddg2: DDG2): exp_time = 0.1 frames_per_trigger = 10 ddg.on_connected() - ddg.scan_info.msg.scan_parameters["exp_time"] = exp_time - ddg.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger + ddg.scan_info.msg.info["exp_time"] = exp_time + ddg.scan_info.msg.info["frames_per_trigger"] = frames_per_trigger # Set non-default burst mode settings ddg.burst_mode.put(0) @@ -538,7 +544,7 @@ def test_ddg2_on_stage(mock_ddg2: DDG2): ddg.unstage() # Reset staged state for next test exp_time_short = 2e-4 # too short exposure time with pytest.raises(ValueError): - ddg.scan_info.msg.scan_parameters["exp_time"] = exp_time_short + ddg.scan_info.msg.info["exp_time"] = exp_time_short ddg.stage() diff --git a/tests/tests_devices/test_eiger.py b/tests/tests_devices/test_eiger.py index acb20d5..73ca78e 100644 --- a/tests/tests_devices/test_eiger.py +++ b/tests/tests_devices/test_eiger.py @@ -8,6 +8,7 @@ from unittest import mock import numpy as np import pytest from bec_lib.messages import FileMessage, ScanStatusMessage +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from jfjoch_client.models.broker_status import BrokerStatus from jfjoch_client.models.dataset_settings import DatasetSettings from jfjoch_client.models.detector_list import DetectorList @@ -49,10 +50,19 @@ def mock_scan_info(request, tmpdir): "frames_per_trigger": frames_per_trigger, "system_config": {}, }, - info={"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")}, + info=ScanServerScanInfo( + scan_name=scan_name, + scan_id="test_id", + positions=np.array([[0, 0], [1, 1], [2, 2]]), + exp_time=exp_time, + frames_per_trigger=frames_per_trigger, + system_config={}, + num_points=num_points, + ).model_dump(), num_points=num_points, scan_name=scan_name, ) + scan_info.info.update({"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")}) yield scan_info @@ -171,6 +181,9 @@ def test_eiger_1_5m_on_connected(eiger_1_5m, detector_list, detector_state): assert mock_jfj_preview_client.start.call_count == 1 +@pytest.mark.skip( + reason="This test is currently disabled because the Eiger 9M config is replaced by the 8_5M config." +) @pytest.mark.parametrize("detector_state", ["Idle", "Inactive"]) def test_eiger_9m_on_connected(eiger_9m, detector_list, detector_state): """Test the on_connected logic of the Eiger detector.""" diff --git a/tests/tests_devices/test_mcs_card.py b/tests/tests_devices/test_mcs_card.py index 35351bf..2f8f6b0 100644 --- a/tests/tests_devices/test_mcs_card.py +++ b/tests/tests_devices/test_mcs_card.py @@ -25,6 +25,7 @@ from csaxs_bec.devices.epics.mcs_card.mcs_card import ( MCSCard, ) from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS +from csaxs_bec.devices.utils.utils import fetch_scan_info @pytest.fixture(scope="function") @@ -121,8 +122,8 @@ def test_mcs_card_csaxs_stage(mock_mcs_csaxs: MCSCardCSAXS): mcs = mock_mcs_csaxs triggers = 5 num_points = 10 - mcs.scan_info.msg.scan_parameters["frames_per_trigger"] = triggers - mcs.scan_info.msg.num_points = num_points + mcs.scan_info.msg.info["frames_per_trigger"] = triggers + mcs.scan_info.msg.info["num_points"] = num_points # Simulate that the MCS card is still acquiring, and that current channel is !=0 mcs.current_channel._read_pv.mock_data = 2 # Simulate that current channel is not zero @@ -171,6 +172,7 @@ def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs: MCSCardCSAXS): II. Acquisition completes normally """ mcs = mock_mcs_csaxs + mcs.scan_parameters = fetch_scan_info(mcs.scan_info) mcs.acquiring._read_pv.mock_data = ACQUIRING.ACQUIRING # Make sure that device on_connected has been called which starts the monitoring thread mcs.on_connected() @@ -197,7 +199,8 @@ def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs: MCSCardCSAXS): ####################### mcs._current_data_index = 0 - mcs.scan_info.msg.num_points = 10 + mcs.scan_info.msg.info["num_points"] = 10 + mcs.scan_parameters = fetch_scan_info(mcs.scan_info) mcs.acquiring._read_pv.mock_data = ACQUIRING.ACQUIRING st = mcs.complete() diff --git a/tests/tests_devices/test_panda.py b/tests/tests_devices/test_panda.py index 02bca15..57afc8d 100644 --- a/tests/tests_devices/test_panda.py +++ b/tests/tests_devices/test_panda.py @@ -10,6 +10,7 @@ from ophyd import Staged from csaxs_bec.devices.panda_box.panda_box import PandaBoxCSAXS from csaxs_bec.devices.panda_box.panda_box_omny import PandaBoxOMNY +from csaxs_bec.devices.utils.utils import fetch_scan_info @pytest.fixture @@ -65,18 +66,18 @@ def test_panda_omny(panda_omny): @pytest.mark.parametrize( "scan_type, frames_per_trigger, expected_acquisition_group", [ - ("fly", 1, "fly"), - ("fly", 5, "fly"), - ("step", 10, "burst"), - ("step", 1, "monitored"), # Default case + ("hardware_triggered", 1, "fly"), + ("hardware_triggered", 5, "fly"), + ("software_triggered", 10, "burst"), + ("software_triggered", 1, "monitored"), # Default case ], ) def test_panda_omny_stage(panda_omny, scan_type, frames_per_trigger, expected_acquisition_group): # Check that the stage signal is present and has the correct PV assert len(panda_omny._status_callbacks) == 0 - panda_omny.scan_info.msg.scan_type = scan_type - panda_omny.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger + panda_omny.scan_info.msg.info["scan_type"] = scan_type + panda_omny.scan_info.msg.info["frames_per_trigger"] = frames_per_trigger panda_omny.stage() assert panda_omny._acquisition_group == expected_acquisition_group @@ -85,8 +86,9 @@ def test_panda_omny_stage(panda_omny, scan_type, frames_per_trigger, expected_ac def test_panda_omny_complete(panda_omny): """Test the on_complete method of the PandaBoxCSAXS device.""" - panda_omny.scan_info.msg.num_points = 1 - panda_omny.scan_info.msg.scan_parameters["frames_per_trigger"] = 1 + panda_omny.scan_info.msg.info["num_points"] = 1 + panda_omny.scan_info.msg.info["frames_per_trigger"] = 1 + panda_omny.scan_parameters = fetch_scan_info(panda_omny.scan_info) panda_omny._timeout_on_completed = 0.5 # Set a short timeout for testing @@ -133,18 +135,18 @@ def test_panda_csaxs(panda_csaxs): @pytest.mark.parametrize( "scan_type, frames_per_trigger, expected_acquisition_group", [ - ("fly", 1, "fly"), - ("fly", 5, "fly"), - ("step", 10, "burst"), - ("step", 1, "monitored"), # Default case + ("hardware_triggered", 1, "fly"), + ("hardware_triggered", 5, "fly"), + ("software_triggered", 10, "burst"), + ("software_triggered", 1, "monitored"), # Default case ], ) def test_panda_csaxs_stage(panda_csaxs, scan_type, frames_per_trigger, expected_acquisition_group): """Test the on_stage method of the PandaBoxCSAXS device for different scan types and frames per trigger.""" assert len(panda_csaxs._status_callbacks) == 0 - panda_csaxs.scan_info.msg.scan_type = scan_type - panda_csaxs.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger + panda_csaxs.scan_info.msg.info["scan_type"] = scan_type + panda_csaxs.scan_info.msg.info["frames_per_trigger"] = frames_per_trigger panda_csaxs.stage() assert panda_csaxs._acquisition_group == expected_acquisition_group @@ -153,8 +155,9 @@ def test_panda_csaxs_stage(panda_csaxs, scan_type, frames_per_trigger, expected_ def test_panda_csaxs_complete(panda_csaxs): """Test the on_complete method of the PandaBoxCSAXS device.""" - panda_csaxs.scan_info.msg.num_points = 1 - panda_csaxs.scan_info.msg.scan_parameters["frames_per_trigger"] = 1 + panda_csaxs.scan_info.msg.info["num_points"] = 1 + panda_csaxs.scan_info.msg.info["frames_per_trigger"] = 1 + panda_csaxs.scan_parameters = fetch_scan_info(panda_csaxs.scan_info) panda_csaxs._timeout_on_completed = 0.5 # Set a short timeout for testing