From dd0fe31cb753f770899337928ddede470269c869 Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Wed, 3 Sep 2025 13:24:18 +0200 Subject: [PATCH 01/21] feat(pilatus): Initial commit of Pilatus integration --- debye_bec/device_configs/x01da_pilatus.yaml | 9 + debye_bec/devices/pilatus/__init__.py | 0 debye_bec/devices/pilatus/pilatus.py | 408 ++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 debye_bec/device_configs/x01da_pilatus.yaml create mode 100644 debye_bec/devices/pilatus/__init__.py create mode 100644 debye_bec/devices/pilatus/pilatus.py diff --git a/debye_bec/device_configs/x01da_pilatus.yaml b/debye_bec/device_configs/x01da_pilatus.yaml new file mode 100644 index 0000000..261cfa9 --- /dev/null +++ b/debye_bec/device_configs/x01da_pilatus.yaml @@ -0,0 +1,9 @@ + pilatus: + readoutPriority: async + description: Pilatus + deviceClass: debye_bec.devices.pilatus.PilatusDetector + deviceConfig: + prefix: "X01DA-ES2-PIL:" + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/devices/pilatus/__init__.py b/debye_bec/devices/pilatus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py new file mode 100644 index 0000000..8a570db --- /dev/null +++ b/debye_bec/devices/pilatus/pilatus.py @@ -0,0 +1,408 @@ +"""Pilatus AD integration at Debye beamline.""" + +from __future__ import annotations + +import enum +import threading +import time +import traceback +from typing import TYPE_CHECKING + +import numpy as np +from bec_lib.file_utils import get_full_path +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import EpicsSignal, Kind +from ophyd.areadetector.cam import ADBase, PilatusDetectorCam +from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin +from ophyd.status import WaitTimeoutError +from ophyd_devices import ( + AndStatusWithList, + CompareStatus, + DeviceStatus, + FileEventSignal, + PreviewSignal, +) +from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 as ImagePlugin +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from ophyd_devices.utils.psi_device_base_utils import TaskStatus + +if TYPE_CHECKING: # pragma: no cover + from bec_lib.devicemanager import ScanInfo + from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage + from bec_server.device_server.device_server import DeviceManagerDS + +PILATUS_READOUT_TIME = 0.1 # in s +PILATUS_ACQUIRE_TIME = ( + 999999 # This time is the timeout of the detector in operation mode, so it needs to be large. +) + +# pylint: disable=redefined-outer-name + +logger = bec_logger.logger + + +class DETECTORSTATE(int, enum.Enum): + """Pilatus Detector States from CamServer""" + + UNARMED = 0 + ARMED = 1 + + +class ACQUIREMODE(int, enum.Enum): + """Pilatus Acquisition Modes""" + + DONE = 0 + ACQUIRING = 1 + + +class FILEWRITEMODE(int, enum.Enum): + """HDF5 Plugin FileWrite Mode""" + + SINGLE = 0 + CAPTURE = 1 + STREAM = 2 + + +class COMPRESSIONALGORITHM(int, enum.Enum): + """HDF5 Plugin Compression Algorithm""" + + NONE = 0 + NBIT = 1 # Don't use that.. + SZIP = 2 + ZLIB = 3 + + +class TRIGGERMODE(int, enum.Enum): + """Pilatus Trigger Modes""" + + INTERNAL = 0 + EXT_ENABLE = 1 + EXT_TRIGGER = 2 + MULT_TRIGGER = 3 + ALIGNMENT = 4 + + def description(self) -> str: + """Return a description of the trigger mode.""" + descriptions = { + TRIGGERMODE.INTERNAL: "Internal trigger mode, images are acquired on internal trigger.", + TRIGGERMODE.EXT_ENABLE: "External Enable trigger mode; check manual as details are currently unknown", + TRIGGERMODE.EXT_TRIGGER: "External Trigger mode, images are acquired on external trigger signal. All images on single trigger.", + TRIGGERMODE.MULT_TRIGGER: "Multiple External Trigger mode, images are acquired on multiple external trigger signals. One image per trigger.", + TRIGGERMODE.ALIGNMENT: "Alignment mode, used for beam alignment.", + } + return descriptions.get(self, "Unknown") + + def __str__(self): + return self.description() + + +class Pilatus(PSIDeviceBase, ADBase): + """ + Pilatus Base integration for Debye. + Prefix of the detector is 'X01DA-ES2-PIL:' + + Args: + prefix (str) : Prefix for the IOC + name (str) : Name of the detector + scan_info (ScanInfo | None) : ScanInfo object passed through the device by the device_manager + device_manager (DeviceManager | None) : DeviceManager object passed through the device by the device_manager + """ + + cam = Cpt(PilatusDetectorCam, "cam1:") + hdf = Cpt(HDF5Plugin, "HDF1:") + image1 = Cpt(ImagePlugin, "image1:") + filter_number = Cpt( + EpicsSignal, "cam1:FileNumber", kind=Kind.omitted, doc="File number for ramdisk" + ) + trigger_shot = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_req", + write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_req", + add_prefix=("a",), + kind=Kind.omitted, + doc="Trigger PV from MO1 Bragg", + ) + trigger_source = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_src_ENUM_RBV", + write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_src_ENUM", + add_prefix=("a",), + kind=Kind.omitted, + doc="Trigger Source; PV, 0 : EPICS, 1 : INPOS", + ) + trigger_mode = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_mode_ENUM_RBV", + write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_mode_ENUM", + add_prefix=("a",), + kind=Kind.omitted, + doc="Trigger Mode; 0 : PULSE, 1 : CONDITION", + ) + trigger_pulse_length = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_len_RBV", + write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_len", + add_prefix=("a",), + kind=Kind.omitted, + doc="Trigger Pulse Length in seconds", + ) + preview = Cpt( + PreviewSignal, + name="preview", + ndim=2, + num_rotation_90=0, # Check this + doc="Preview signal for the Pilatus Detector", + ) + file_event = Cpt(FileEventSignal, name="file_event") + + def __init__( + self, + *, + name: str, + prefix: str = "", + scan_info: ScanInfo | None = None, + device_manager: DeviceManagerDS | None = None, + **kwargs, + ): + super().__init__( + name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs + ) + self.device_manager = device_manager + self._readout_time = PILATUS_READOUT_TIME + self._full_path = "" + self._poll_thread_stop_event = threading.Event() + self._task_status: TaskStatus | None = None + self._poll_rate = 1 # 1Hz + + ######################################## + # Custom Beamline Methods # + ######################################## + + def _poll_array_data(self): + while not self._poll_thread_stop_event.wait(1 / self._poll_rate): + logger.debug("Polling Pilatus array data for preview...") + try: + value = self.image1.array_data.get() + if value is None: + continue + width = self.image1.array_size.width.get() + height = self.image1.array_size.height.get() + # Geometry correction for the image + data = np.reshape(value, (height, width)) + last_image: DevicePreviewMessage = self.preview.get() + if last_image is None: + return + elif np.array_equal(data, last_image.data): + # No update if image is the same, ~2.5ms on 2400x2400 image (6M) + logger.debug( + f"Pilatus preview image for {self.name} is the same as last one, not updating." + ) + return + self.preview.put(data) + except Exception: # pylint: disable=broad-except + content = traceback.format_exc() + logger.error( + f"Error while polling array data for preview of {self.name}: {content}" + ) + + ######################################## + # Beamline Specific Implementations # + ######################################## + + def on_init(self) -> None: + """ + Called when the device is initialized. + + No signals are connected at this point. If you like to + set default values on signals, please use on_connected instead. + """ + + def on_connected(self) -> None: + """ + Called after the device is connected and its signals are connected. + Default values for signals should be set here. + """ + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + try: + status_cam.wait(timeout=5) + status_hdf.wait(timeout=5) + except WaitTimeoutError: + logger.warning( + f"Camera device {self.name} was running an acquisition. Stopping acquisition." + ) + self.cam.acquire.put(0) + self.hdf.capture.put(0) + + self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5) + self.cam.image_file_tmot.set(60).wait(5) + self.hdf.file_write_mode.set(FILEWRITEMODE.STREAM.value).wait(5) + self.hdf.file_template.set("%s%s").wait(5) + self.hdf.auto_save.set(1).wait(5) + self.hdf.lazy_open.set(1).wait(5) + self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use + # Start polling thread... + self._task_status = self.task_handler.submit_task(task=self._poll_array_data, run=True) + + def on_stage(self) -> DeviceStatus | None: + """ + Called while staging the device. + + Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. + """ + scan_msg: ScanStatusMessage = self.scan_info.msg + if scan_msg.scan_name.startswith("xas"): + return None + # TODO implement logic for 'xas' scans + else: + exp_time = scan_msg.scan_parameters.get("exposure_time", 0.0) + if exp_time - self._readout_time <= 0: + raise ValueError( + f"Exposure time {exp_time} is too short for Pilatus with readout_time {self._readout_time}." + ) + detector_exp_time = exp_time - self._readout_time + n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) + self._full_path = get_full_path(scan_msg, name="pilatus") + file_path = "/".join(self._full_path.split("/")[:-1]) + file_name = self._full_path.split("/")[-1] + + # Prepare detector and backend + self.cam.array_callbacks.set(1).wait(5) # Enable array callbacks + self.hdf.enable.set(1).wait(5) # Enable HDF5 plugin + # Camera settings + self.cam.num_exposures.set(1).wait(5) + self.cam.num_images.set(n_images).wait(5) + self.cam.acquire_time.set(exp_time).wait(5) # let's try this + self.cam.acquire_period.set(PILATUS_ACQUIRE_TIME).wait(5) + self.filter_number.set(0).wait(5) + # HDF5 settings + logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}") + self.hdf.file_path.set(file_path).wait(5) + self.hdf.file_name.set(file_name).wait(5) + self.hdf.num_capture.set(n_images).wait(5) + self.cam.array_counter.set(0).wait(5) # Reset array counter + self.file_event.put( + file_path=self._full_path, done=False, successful=False + ) # TODO add h5_entry dict + return None + + def on_unstage(self) -> None: + """Called while unstaging the device.""" + + def on_pre_scan(self) -> DeviceStatus | None: + """Called right before the scan starts on all devices automatically.""" + if self.scan_info.msg.scan_name.startswith("xas"): + # TODO implement logic for 'xas' scans + return None + else: + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_cam_server] + ) + self.cam.acquire.put(1) + self.hdf.capture.put(1) + self.cancel_on_stop(status) + return status + + def on_trigger(self) -> DeviceStatus | None: + """Called when the device is triggered.""" + if self.scan_info.msg.scan_name.startswith("xas"): + return None + # TODO implement logic for 'xas' scans + else: + start_time = time.time() + logger.warning(f"Triggering image with num_captured {self.hdf.num_captured.get()}") + img_counter = self.hdf.num_captured.get() + status = CompareStatus(self.hdf.num_captured, img_counter + 1) + logger.warning(f"Triggering took image {time.time() - start_time:.3f} seconds") + self.trigger_shot.put(1) + self.cancel_on_stop(status) + return status + + def _complete_callback(self, status: DeviceStatus): + """Callback for when the device completes a scan.""" + if status.success: + status.device.file_event.put( + file_path=status.device._full_path, done=True, successful=True + ) + else: + status.device.file_event.put( + file_path=status.device._full_path, done=True, successful=False + ) + + def on_complete(self) -> DeviceStatus | None: + """Called to inquire if a device has completed a scans.""" + if self.scan_info.msg.scan_name.startswith("xas"): + # TODO implement logic for 'xas' scans + return None + else: + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) + num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( + "frames_per_trigger", 1 + ) + status_img_written = CompareStatus(self.hdf.num_captured, num_images) + status = AndStatusWithList( + device=self, + status_list=[status_hdf, status_cam, status_img_written, status_cam_server], + ) + status.add_callback(self._complete_callback) # Callback that writing was successful + self.cancel_on_stop(status) + return status + + def on_kickoff(self) -> None: + """Called to kickoff a device for a fly scan. Has to be called explicitly.""" + + def on_stop(self) -> None: + """Called when the device is stopped.""" + self.cam.acquire.put(0) + self.hdf.capture.put(0) + + def on_destroy(self) -> None: + """Called when the device is destroyed. Cleanup resources here.""" + self.on_stop() + self._poll_thread_stop_event.set() + + +if __name__ == "__main__": + try: + pilatus = Pilatus(name="pilatus", prefix="X01DA-ES2-PIL:") + logger.info(f"Calling wait for connection") + # pilatus.wait_for_connection(all_signals=True, timeout=20) + logger.info(f"Connecting to pilatus...") + pilatus.on_connected() + for exp_time, scan_number, n_pnts in zip([0.5, 1.0, 2.0], [1, 2, 3], [30, 20, 10]): + logger.info(f"Sleeping for 5s") + time.sleep(5) + pilatus.scan_info.msg.num_points = n_pnts + pilatus.scan_info.msg.scan_parameters["exposure_time"] = exp_time + pilatus.scan_info.msg.scan_parameters["frames_per_trigger"] = 1 + pilatus.scan_info.msg.info["file_components"] = ( + f"/sls/x01da/data/p22481/raw/data/S00000-00999/S{scan_number:05d}/S{scan_number:05d}", + "h5", + ) + pilatus.on_stage() + logger.info(f"Stage done") + pilatus.on_pre_scan().wait(timeout=5) + logger.info(f"Pre-scan done") + for ii in range(pilatus.scan_info.msg.num_points): + # if ii == 0: + # time.sleep(1) + logger.info(f"Triggering image {ii+1}/{pilatus.scan_info.msg.num_points}") + pilatus.on_trigger().wait() + p = pilatus.preview.get() + if p is not None: + p: DevicePreviewMessage + logger.warning( + f"Preview shape: {p.data.shape}, max: {np.max(p.data)}, min: {np.min(p.data)}, mean: {np.mean(p.data)}" + ) + pilatus.on_complete().wait(timeout=5) + logger.info(f"Complete done") + pilatus.on_unstage() + logger.info(f"Unstage done") + finally: + pilatus.on_destroy() -- 2.49.1 From 8e5bdd230d04ca3361513939b315a2d0a9a34d19 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 4 Sep 2025 17:54:34 +0200 Subject: [PATCH 02/21] test(pilatus): add tests for the pilatus. on_stage & on_connected tests fail due to AD baseclass callbacks --- debye_bec/devices/pilatus/pilatus.py | 14 +- tests/tests_devices/test_pilatus.py | 271 +++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 tests/tests_devices/test_pilatus.py diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 8a570db..c2fbad9 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -33,9 +33,9 @@ if TYPE_CHECKING: # pragma: no cover from bec_server.device_server.device_server import DeviceManagerDS PILATUS_READOUT_TIME = 0.1 # in s -PILATUS_ACQUIRE_TIME = ( - 999999 # This time is the timeout of the detector in operation mode, so it needs to be large. -) +# PILATUS_ACQUIRE_TIME = ( +# 999999 # This time is the timeout of the detector in operation mode, so it needs to be large. +# ) # pylint: disable=redefined-outer-name @@ -256,7 +256,7 @@ class Pilatus(PSIDeviceBase, ADBase): return None # TODO implement logic for 'xas' scans else: - exp_time = scan_msg.scan_parameters.get("exposure_time", 0.0) + exp_time = scan_msg.scan_parameters.get("exp_time", 0.0) if exp_time - self._readout_time <= 0: raise ValueError( f"Exposure time {exp_time} is too short for Pilatus with readout_time {self._readout_time}." @@ -273,8 +273,8 @@ class Pilatus(PSIDeviceBase, ADBase): # Camera settings self.cam.num_exposures.set(1).wait(5) self.cam.num_images.set(n_images).wait(5) - self.cam.acquire_time.set(exp_time).wait(5) # let's try this - self.cam.acquire_period.set(PILATUS_ACQUIRE_TIME).wait(5) + self.cam.acquire_time.set(detector_exp_time).wait(5) # let's try this + self.cam.acquire_period.set(exp_time).wait(5) self.filter_number.set(0).wait(5) # HDF5 settings logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}") @@ -379,7 +379,7 @@ if __name__ == "__main__": logger.info(f"Sleeping for 5s") time.sleep(5) pilatus.scan_info.msg.num_points = n_pnts - pilatus.scan_info.msg.scan_parameters["exposure_time"] = exp_time + pilatus.scan_info.msg.scan_parameters["exp_time"] = exp_time pilatus.scan_info.msg.scan_parameters["frames_per_trigger"] = 1 pilatus.scan_info.msg.info["file_components"] = ( f"/sls/x01da/data/p22481/raw/data/S00000-00999/S{scan_number:05d}/S{scan_number:05d}", diff --git a/tests/tests_devices/test_pilatus.py b/tests/tests_devices/test_pilatus.py new file mode 100644 index 0000000..75c7c69 --- /dev/null +++ b/tests/tests_devices/test_pilatus.py @@ -0,0 +1,271 @@ +# pylint: skip-file +import os +import threading +from typing import TYPE_CHECKING, Generator +from unittest import mock + +import numpy as np +import ophyd +import pytest +from bec_lib.messages import ScanStatusMessage +from bec_server.scan_server.scan_worker import ScanWorker +from ophyd_devices import CompareStatus, DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError +from ophyd_devices.tests.utils import MockPV, patch_dual_pvs +from ophyd_devices.utils.psi_device_base_utils import TaskStatus + +from debye_bec.devices.pilatus.pilatus import ( + ACQUIREMODE, + COMPRESSIONALGORITHM, + DETECTORSTATE, + FILEWRITEMODE, + TRIGGERMODE, + Pilatus, +) + +if TYPE_CHECKING: # pragma no cover + from bec_lib.messages import FileMessage + +# @pytest.fixture(scope="function") +# def scan_worker_mock(scan_server_mock): +# scan_server_mock.device_manager.connector = mock.MagicMock() +# scan_worker = ScanWorker(parent=scan_server_mock) +# yield scan_worker + + +@pytest.fixture( + scope="function", + params=[(0.1, 1, 1, "line_scan"), (0.2, 2, 2, "time_scan"), (0.5, 5, 5, "xas_advanced_scan")], +) +def mock_scan_info(request, tmpdir): + exp_time, frames_per_trigger, num_points, scan_name = request.param + scan_info = ScanStatusMessage( + scan_id="test_id", + status="open", + scan_number=1, + scan_parameters={ + "exp_time": exp_time, + "frames_per_trigger": frames_per_trigger, + "system_config": {}, + }, + info={"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")}, + num_points=num_points, + scan_name=scan_name, + ) + yield scan_info + + +@pytest.fixture(scope="function") +def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]: + name = "pilatus" + prefix = "X01DA-OP-MO1:PILATUS:" + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + mock_cl.thread_class = threading.Thread + dev = Pilatus(name=name, prefix=prefix) + patch_dual_pvs(dev) + # dev.image1 = mock.MagicMock() + # with mock.patch.object(dev, "image1"): + with mock.patch.object(dev, "task_handler"): + dev.scan_info.msg = mock_scan_info + try: + yield dev + finally: + dev.destroy() + + +# TODO figure out how to test as set calls on the PV below seem to break it.. +# def test_pilatus_on_connected(pilatus): +# """Test the on_connected logic of the Pilatus detector.""" +# pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.DONE.value +# pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.DONE.value +# pilatus.on_connected() +# assert pilatus.cam.trigger_mode.get() == TRIGGERMODE.MULT_TRIGGER +# assert pilatus.hdf.file_write_mode.get() == FILEWRITEMODE.STREAM +# assert pilatus.hdf.file_template.get() == "%s%s" +# assert pilatus.hdf.auto_save.get() == 1 +# assert pilatus.hdf.lazy_open.get() == 1 +# assert pilatus.hdf.compression.get() == COMPRESSIONALGORITHM.NONE + + +def test_pilatus_on_stop(pilatus): + """Test the on_stop logic of the Pilatus detector.""" + pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value + pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value + pilatus.on_stop() + assert pilatus.cam.acquire.get() == ACQUIREMODE.DONE + assert pilatus.hdf.capture.get() == ACQUIREMODE.DONE + + +def test_pilatus_on_destroy(pilatus): + """Test the on_destroy logic of the Pilatus detector.""" + with mock.patch.object(pilatus, "on_stop") as mock_on_stop: + pilatus.destroy() + assert mock_on_stop.call_count == 1 + assert pilatus._poll_thread_stop_event.is_set() + + +def test_pilatus_on_failure_callback(pilatus): + """Test the on_failure_callback logic of the Pilatus detector.""" + + with mock.patch.object(pilatus, "on_stop") as mock_on_stop: + status = DeviceStatus(pilatus) + status.set_finished() # Does not trigger 'stop' + assert mock_on_stop.call_count == 0 + status = DeviceStatus(pilatus) + status.set_exception(RuntimeError("Test error")) # triggers 'stop' + assert mock_on_stop.call_count == 1 + + +def test_pilatus_on_pre_scan(pilatus): + """Test the on_pre_scan logic of the Pilatus detector.""" + if pilatus.scan_info.msg.scan_name.startswith("xas"): + assert pilatus.on_pre_scan() is None + return + pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.DONE.value + pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.DONE.value + pilatus.cam.armed._read_pv.mock_data = DETECTORSTATE.UNARMED.value + status = pilatus.on_pre_scan() + assert status.done is False + pilatus.cam.armed.put(DETECTORSTATE.ARMED.value) + status.wait(timeout=5) + assert status.done is True + assert status.success is True + + +def test_pilatus_on_trigger(pilatus): + """test on trigger logic of the Pilatus detector.""" + if pilatus.scan_info.msg.scan_name.startswith("xas"): + status = pilatus.trigger() + assert status.done is True + assert status.success is True + return None + pilatus.hdf.num_captured._read_pv.mock_data = 0 + pilatus.trigger_shot.put(0) + status = pilatus.trigger() + assert status.done is False + assert pilatus.trigger_shot.get() == 1 + pilatus.hdf.num_captured._read_pv.mock_data = 1 + status.wait(timeout=5) + assert status.done is True + assert status.success is True + + +def test_pilatus_on_trigger_cancel_on_stop(pilatus): + """Test that the status of the trigger is cancelled if stop is called""" + if pilatus.scan_info.msg.scan_name.startswith("xas"): + status = pilatus.trigger() + assert status.done is True + assert status.success is True + return + pilatus.hdf.num_captured._read_pv.mock_data = 0 + pilatus.trigger_shot.put(0) + status = pilatus.trigger() + assert status.done is False + with pytest.raises(DeviceStoppedError): + pilatus.stop() + status.wait(timeout=5) + + +def test_pilatus_on_complete(pilatus): + """Test the on_complete logic of the Pilatus detector.""" + if pilatus.scan_info.msg.scan_name.startswith("xas"): + status = pilatus.complete() + assert status.done is True + assert status.success is True + return + # Check in addition that the file event is set properly, once with if it works, and once if not (i.e. when cancelled) + for success in [True, False]: + if success is True: + pilatus.file_event.put(file_path="", done=False, successful=False) + pilatus._full_path = "file-path-for-success" + else: + pilatus.file_event.put(file_path="", done=False, successful=True) + pilatus._full_path = "file-path-for-failure" + # Set values for relevant PVs + pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value + pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value + pilatus.cam.armed._read_pv.mock_data = DETECTORSTATE.ARMED.value + num_images = pilatus.scan_info.msg.num_points * pilatus.scan_info.msg.scan_parameters.get( + "frames_per_trigger", 1 + ) + pilatus.hdf.num_captured._read_pv.mock_data = num_images - 1 + # Call on complete + status = pilatus.complete() + # Should not be finished + assert status.done is False + pilatus.cam.acquire.put(ACQUIREMODE.DONE.value) + pilatus.hdf.capture.put(ACQUIREMODE.DONE.value) + pilatus.cam.armed.put(DETECTORSTATE.UNARMED.value) + assert status.done is False + if success is True: + pilatus.hdf.num_captured._read_pv.mock_data = num_images + # Now it should resolve + status.wait(timeout=5) + assert status.done is True + assert status.success is True + file_msg: FileMessage = pilatus.file_event.get() + assert file_msg.file_path == "file-path-for-success" + assert file_msg.done is True + assert file_msg.successful is True + else: + with pytest.raises(DeviceStoppedError): + pilatus.stop() + status.wait(timeout=5) + assert status.done is True + assert status.success is False + file_msg: FileMessage = pilatus.file_event.get() + assert file_msg.file_path == "file-path-for-failure" + assert file_msg.done is True + assert file_msg.successful is False + + +# TODO, figure out how to properly test this.. +# def test_pilatus_on_stage(pilatus): +# """Test the on_stage logic of the Pilatus detector.""" +# # Make sure that no additional logic from stage signals of underlying components is triggered +# pilatus.stage_sigs = {} +# pilatus.cam.stage_sigs = {} +# pilatus.hdf.stage_sigs = {} +# if pilatus.scan_info.msg.scan_name.startswith("xas"): +# pilatus.on_stage() +# return +# exp_time = pilatus.scan_info.msg.scan_parameters.get("exp_time", 0.1) +# n_images = pilatus.scan_info.msg.num_points * pilatus.scan_info.msg.scan_parameters.get( +# "frames_per_trigger", 1 +# ) +# if exp_time <= 0.1: +# with pytest.raises(ValueError): +# pilatus.on_stage() +# return +# pilatus.filter_number.put(10) +# pilatus.cam.array_counter.put(1) +# file_components = pilatus.scan_info.msg.info.get("file_components", ("", ""))[0] +# base_path = file_components[0].rsplit("/", 1)[0] +# file_name = file_components[0].rsplit("/", 1)[1] + "_pilatus.h5" +# file_path = os.path.join(base_path, file_name) +# pilatus.on_stage() +# assert pilatus.cam.array_callbacks.get() == 0 +# assert pilatus.hdf.enable.get() == 1 +# assert pilatus.cam.num_exposures.get() == 1 +# assert pilatus.cam.num_images.get() == n_images +# assert pilatus.cam.acquire_time.get() == exp_time - pilatus._readout_time +# assert pilatus.cam.acquire_period.get() == exp_time +# assert pilatus.filter_number.get() == 0 +# assert pilatus.hdf.file_path.get() == base_path +# assert pilatus.hdf.file_name.get() == file_name +# assert pilatus.hdf.num_capture.get() == n_images +# assert pilatus.cam.array_counter.get() == 0 +# file_msg: FileMessage = pilatus.file_event.get() +# assert file_msg.file_path == file_path +# assert file_msg.done is False +# assert file_msg.successful is False + + +def test_pilatus_on_stage_raises_low_exp_time(pilatus): + """Test that on_stage raises a ValueError if the exposure time is too low.""" + pilatus.scan_info.msg.scan_parameters["exp_time"] = 0.09 + if pilatus.scan_info.msg.scan_name.startswith("xas"): + return + with pytest.raises(ValueError): + pilatus.on_stage() -- 2.49.1 From 09c3e395de258f393e4e228a774c35c45946c5a9 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 4 Sep 2025 17:57:48 +0200 Subject: [PATCH 03/21] refactor(pilatus): update config, add live mode --- debye_bec/device_configs/x01da_pilatus.yaml | 31 +++++++- debye_bec/devices/pilatus/pilatus.py | 83 +++++++++++---------- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/debye_bec/device_configs/x01da_pilatus.yaml b/debye_bec/device_configs/x01da_pilatus.yaml index 261cfa9..33986f3 100644 --- a/debye_bec/device_configs/x01da_pilatus.yaml +++ b/debye_bec/device_configs/x01da_pilatus.yaml @@ -1,9 +1,34 @@ - pilatus: +pilatus: readoutPriority: async description: Pilatus - deviceClass: debye_bec.devices.pilatus.PilatusDetector + deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus + deviceTags: + - detector deviceConfig: prefix: "X01DA-ES2-PIL:" onFailure: retry enabled: true - softwareTrigger: false \ No newline at end of file + softwareTrigger: true +samx: + readoutPriority: baseline + deviceClass: ophyd_devices.SimPositioner + deviceConfig: + delay: 1 + limits: + - -50 + - 50 + tolerance: 0.01 + update_frequency: 400 + deviceTags: + - user motors + enabled: true + readOnly: false +bpm4i: + readoutPriority: monitored + deviceClass: ophyd_devices.SimMonitor + deviceConfig: + deviceTags: + - beamline + enabled: true + readOnly: false + diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index c2fbad9..e287da7 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -15,6 +15,7 @@ from ophyd import Component as Cpt from ophyd import EpicsSignal, Kind from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin +from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin from ophyd.status import WaitTimeoutError from ophyd_devices import ( AndStatusWithList, @@ -23,9 +24,7 @@ from ophyd_devices import ( FileEventSignal, PreviewSignal, ) -from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 as ImagePlugin from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase -from ophyd_devices.utils.psi_device_base_utils import TaskStatus if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -171,17 +170,19 @@ class Pilatus(PSIDeviceBase, ADBase): self.device_manager = device_manager self._readout_time = PILATUS_READOUT_TIME self._full_path = "" + self._poll_thread = threading.Thread( + target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread" + ) self._poll_thread_stop_event = threading.Event() - self._task_status: TaskStatus | None = None - self._poll_rate = 1 # 1Hz + self._poll_rate = 1 # Poll rate in Hz ######################################## # Custom Beamline Methods # ######################################## def _poll_array_data(self): + """Poll the array data for preview updates.""" while not self._poll_thread_stop_event.wait(1 / self._poll_rate): - logger.debug("Polling Pilatus array data for preview...") try: value = self.image1.array_data.get() if value is None: @@ -191,14 +192,16 @@ class Pilatus(PSIDeviceBase, ADBase): # Geometry correction for the image data = np.reshape(value, (height, width)) last_image: DevicePreviewMessage = self.preview.get() - if last_image is None: - return - elif np.array_equal(data, last_image.data): - # No update if image is the same, ~2.5ms on 2400x2400 image (6M) - logger.debug( - f"Pilatus preview image for {self.name} is the same as last one, not updating." - ) - return + + if last_image is not None: + if np.array_equal(data, last_image.data): + # No update if image is the same, ~2.5ms on 2400x2400 image (6M) + logger.debug( + f"Pilatus preview image for {self.name} is the same as last one, not updating." + ) + continue + + logger.debug(f"Setting preview data for {self.name}") self.preview.put(data) except Exception: # pylint: disable=broad-except content = traceback.format_exc() @@ -243,13 +246,14 @@ class Pilatus(PSIDeviceBase, ADBase): self.hdf.lazy_open.set(1).wait(5) self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use # Start polling thread... - self._task_status = self.task_handler.submit_task(task=self._poll_array_data, run=True) + self._poll_thread.start() def on_stage(self) -> DeviceStatus | None: """ Called while staging the device. - Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. + Information about the upcoming scan can be accessed from the scan_info + (self.scan_info.msg) object. """ scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name.startswith("xas"): @@ -327,32 +331,30 @@ class Pilatus(PSIDeviceBase, ADBase): if status.success: status.device.file_event.put( file_path=status.device._full_path, done=True, successful=True - ) + ) # pylint: disable:protected-access else: status.device.file_event.put( file_path=status.device._full_path, done=True, successful=False - ) + ) # pylint: disable:protected-access def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" if self.scan_info.msg.scan_name.startswith("xas"): # TODO implement logic for 'xas' scans return None - else: - status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) - status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) - status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) - num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( - "frames_per_trigger", 1 - ) - status_img_written = CompareStatus(self.hdf.num_captured, num_images) - status = AndStatusWithList( - device=self, - status_list=[status_hdf, status_cam, status_img_written, status_cam_server], - ) - status.add_callback(self._complete_callback) # Callback that writing was successful - self.cancel_on_stop(status) - return status + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) + num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( + "frames_per_trigger", 1 + ) + status_img_written = CompareStatus(self.hdf.num_captured, num_images) + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] + ) + status.add_callback(self._complete_callback) # Callback that writing was successful + self.cancel_on_stop(status) + return status def on_kickoff(self) -> None: """Called to kickoff a device for a fly scan. Has to be called explicitly.""" @@ -364,19 +366,20 @@ class Pilatus(PSIDeviceBase, ADBase): def on_destroy(self) -> None: """Called when the device is destroyed. Cleanup resources here.""" - self.on_stop() self._poll_thread_stop_event.set() + # TODO do we need to clean the poll thread ourselves? + self.on_stop() if __name__ == "__main__": try: pilatus = Pilatus(name="pilatus", prefix="X01DA-ES2-PIL:") - logger.info(f"Calling wait for connection") + logger.info("Calling wait for connection") # pilatus.wait_for_connection(all_signals=True, timeout=20) - logger.info(f"Connecting to pilatus...") + logger.info("Connecting to pilatus...") pilatus.on_connected() for exp_time, scan_number, n_pnts in zip([0.5, 1.0, 2.0], [1, 2, 3], [30, 20, 10]): - logger.info(f"Sleeping for 5s") + logger.info("Sleeping for 5s") time.sleep(5) pilatus.scan_info.msg.num_points = n_pnts pilatus.scan_info.msg.scan_parameters["exp_time"] = exp_time @@ -386,9 +389,9 @@ if __name__ == "__main__": "h5", ) pilatus.on_stage() - logger.info(f"Stage done") + logger.info("Stage done") pilatus.on_pre_scan().wait(timeout=5) - logger.info(f"Pre-scan done") + logger.info("Pre-scan done") for ii in range(pilatus.scan_info.msg.num_points): # if ii == 0: # time.sleep(1) @@ -401,8 +404,8 @@ if __name__ == "__main__": f"Preview shape: {p.data.shape}, max: {np.max(p.data)}, min: {np.min(p.data)}, mean: {np.mean(p.data)}" ) pilatus.on_complete().wait(timeout=5) - logger.info(f"Complete done") + logger.info("Complete done") pilatus.on_unstage() - logger.info(f"Unstage done") + logger.info("Unstage done") finally: pilatus.on_destroy() -- 2.49.1 From 2633c8be0a5521c223aea595f68985a2298ab333 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 5 Sep 2025 10:37:05 +0200 Subject: [PATCH 04/21] refactor(pilatus): add live mode to pilatus --- debye_bec/devices/pilatus/pilatus.py | 129 ++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index e287da7..ba307cc 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -37,6 +37,7 @@ PILATUS_READOUT_TIME = 0.1 # in s # ) # pylint: disable=redefined-outer-name +# pylint: disable=raise-missing-from logger = bec_logger.logger @@ -108,6 +109,8 @@ class Pilatus(PSIDeviceBase, ADBase): device_manager (DeviceManager | None) : DeviceManager object passed through the device by the device_manager """ + # USER_ACCESS = ["start_live_mode", "stop_live_mode"] + cam = Cpt(PilatusDetectorCam, "cam1:") hdf = Cpt(HDF5Plugin, "HDF1:") image1 = Cpt(ImagePlugin, "image1:") @@ -173,8 +176,15 @@ class Pilatus(PSIDeviceBase, ADBase): self._poll_thread = threading.Thread( target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread" ) - self._poll_thread_stop_event = threading.Event() + self._poll_thread_kill_event = threading.Event() self._poll_rate = 1 # Poll rate in Hz + # self._live_mode_thread = threading.Thread( + # target=self._live_mode_loop, daemon=True, name=f"{self.name}_live_mode_thread" + # ) + # self._live_mode_kill_event = threading.Event() + # self._live_mode_run_event = threading.Event() + # self._live_mode_stopped_event = threading.Event() + # self._live_mode_stopped_event.set() # Initial state is stopped ######################################## # Custom Beamline Methods # @@ -182,8 +192,9 @@ class Pilatus(PSIDeviceBase, ADBase): def _poll_array_data(self): """Poll the array data for preview updates.""" - while not self._poll_thread_stop_event.wait(1 / self._poll_rate): + while not self._poll_thread_kill_event.wait(1 / self._poll_rate): try: + logger.info(f"Running poll loop for {self.name}..") value = self.image1.array_data.get() if value is None: continue @@ -192,7 +203,7 @@ class Pilatus(PSIDeviceBase, ADBase): # Geometry correction for the image data = np.reshape(value, (height, width)) last_image: DevicePreviewMessage = self.preview.get() - + logger.info(f"Preview image for {self.name} has shape {data.shape}") if last_image is not None: if np.array_equal(data, last_image.data): # No update if image is the same, ~2.5ms on 2400x2400 image (6M) @@ -209,6 +220,90 @@ class Pilatus(PSIDeviceBase, ADBase): f"Error while polling array data for preview of {self.name}: {content}" ) + # def start_live_mode(self, exp_time: float, n_images_max: int = 50000): + # """ + # Start live mode with given exposure time. + + # Args: + # exp_time (float) : Exposure time in seconds + # n_images_max (int): Maximum number of images to capture during live mode. + # Default is 5000. Only reset if needed. + # """ + # if ( + # self.cam.acquire.get() != ACQUIREMODE.DONE.value + # or self.hdf.capture.get() != ACQUIREMODE.DONE.value + # ): + # logger.warning(f"Can't start live mode, acquisition running on detector {self.name}.") + # return + # if self._live_mode_run_event.is_set(): + # logger.warning(f"Live mode is already running on detector {self.name}.") + # return + + # # Set relevant PVs + # self.cam.array_counter.set(0).wait(5) # Reset array counter + # self.cam.num_images.set(n_images_max).wait(5) + # logger.info( + # f"Setting exposure time to {exp_time} s for live mode on {self.name} with {n_images_max} images." + # ) + # self.cam.acquire_time.set(exp_time - self._readout_time).wait(5) + # self.cam.acquire_period.set(exp_time).wait(5) + + # status = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + # # It should suffice to make sure that self.hdf.capture is not set.. + # self.cam.acquire.put(1) # Start measurement + # try: + # status.wait(10) + # except WaitTimeoutError: + # content = traceback.format_exc() + # raise RuntimeError( + # f"Live Mode on detector {self.name} did not stop: {content} after 10s." + # ) + # self._live_mode_run_event.set() + + # def _live_mode_loop(self, exp_time: float): + # while not self._live_mode_kill_event.is_set(): + # self._live_mode_run_event.wait() + # self._live_mode_stopped_event.clear() # Clear stopped event + # time.sleep(self._readout_time) # make sure to wait for the readout_time + # n_images = self.cam.array_counter.get() + # status = CompareStatus(self.cam.array_counter, n_images + 1) + # self.trigger_shot.put(1) + # try: + # status.wait(60) + # except WaitTimeoutError: + # logger.warning( + # f"Live mode timeout exceeded for {self.name}. Continuing in live_mode_loop" + # ) + # if self._live_mode_run_event.is_set(): + # self._live_mode_stopped_event.set() # Set stopped event to indicate that live mode loop is stopped + + # def stop_live_mode(self): + # """Stop live mode.""" + # if self._live_mode_stopped_event.is_set(): + # return + # status = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + # self.cam.acquire.put(0) + # self._live_mode_run_event.clear() + # if not self._live_mode_stopped_event.wait(10): # Wait until live mode loop is stopped + # logger.warning(f"Live mode did not stop in time for {self.name}.") + # try: + # status.wait(10) + # except WaitTimeoutError: + # content = traceback.format_exc() + # raise RuntimeError( + # f"Live Mode on detector {self.name} did not stop: {content} after 10s." + # ) + + def check_detector_stop_running_acquisition(self) -> AndStatusWithList: + """Check if the detector is still running an acquisition.""" + status_acquire = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_writing = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) + status = AndStatusWithList( + device=self, status_list=[status_acquire, status_writing, status_cam_server] + ) + return status + ######################################## # Beamline Specific Implementations # ######################################## @@ -226,6 +321,7 @@ class Pilatus(PSIDeviceBase, ADBase): Called after the device is connected and its signals are connected. Default values for signals should be set here. """ + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) try: @@ -247,6 +343,8 @@ class Pilatus(PSIDeviceBase, ADBase): self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use # Start polling thread... self._poll_thread.start() + # Start live mode thread... + # self._live_mode_thread.start() def on_stage(self) -> DeviceStatus | None: """ @@ -255,6 +353,7 @@ class Pilatus(PSIDeviceBase, ADBase): Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. """ + # self.stop_live_mode() # Make sure that live mode is stopped if scan runs scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name.startswith("xas"): return None @@ -287,9 +386,11 @@ class Pilatus(PSIDeviceBase, ADBase): self.hdf.num_capture.set(n_images).wait(5) self.cam.array_counter.set(0).wait(5) # Reset array counter self.file_event.put( - file_path=self._full_path, done=False, successful=False - ) # TODO add h5_entry dict - return None + file_path=self._full_path, + done=False, + successful=False, + hinted_h5_entries={"data": "/entry/data/data"}, + ) def on_unstage(self) -> None: """Called while unstaging the device.""" @@ -330,12 +431,18 @@ class Pilatus(PSIDeviceBase, ADBase): """Callback for when the device completes a scan.""" if status.success: status.device.file_event.put( - file_path=status.device._full_path, done=True, successful=True - ) # pylint: disable:protected-access + file_path=status.device._full_path, # pylint: disable:protected-access + done=True, + successful=True, + hinted_h5_entries={"data": "/entry/data/data"}, + ) else: status.device.file_event.put( - file_path=status.device._full_path, done=True, successful=False - ) # pylint: disable:protected-access + file_path=status.device._full_path, # pylint: disable:protected-access + done=True, + successful=False, + hinted_h5_entries={"data": "/entry/data/data"}, + ) def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" @@ -366,7 +473,7 @@ class Pilatus(PSIDeviceBase, ADBase): def on_destroy(self) -> None: """Called when the device is destroyed. Cleanup resources here.""" - self._poll_thread_stop_event.set() + self._poll_thread_kill_event.set() # TODO do we need to clean the poll thread ourselves? self.on_stop() -- 2.49.1 From 02e6462ea1344478caffbc77dc261efe4bcdd742 Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Wed, 10 Sep 2025 16:54:56 +0200 Subject: [PATCH 05/21] updated trigger signals of mono --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 71 ++++++++++------ .../devices/mo1_bragg/mo1_bragg_devices.py | 8 ++ debye_bec/scans/mono_bragg_scans.py | 82 ++++++++++--------- 3 files changed, 95 insertions(+), 66 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 1daec65..ea163ba 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -56,16 +56,18 @@ class ScanParameter(BaseModel): scan_time: float | None = Field(None, description="Scan time for a half oscillation") scan_duration: float | None = Field(None, description="Duration of the scan") - xrd_enable_low: bool | None = Field( - None, description="XRD enabled for low, should be PV trig_ena_lo_enum" + break_enable_low: bool | None = Field( + None, description="Break enabled for low, should be PV trig_ena_lo_enum" ) # trig_enable_low: bool = None - xrd_enable_high: bool | None = Field( - None, description="XRD enabled for high, should be PV trig_ena_hi_enum" + break_enable_high: bool | None = Field( + None, description="Break enabled for high, should be PV trig_ena_hi_enum" ) # trig_enable_high: bool = None - exp_time_low: float | None = Field(None, description="Exposure time low energy/angle") - exp_time_high: float | None = Field(None, description="Exposure time high energy/angle") + break_time_low: float | None = Field(None, description="Break time low energy/angle") + break_time_high: float | None = Field(None, description="Break time high energy/angle") cycle_low: int | None = Field(None, description="Cycle for low energy/angle") cycle_high: int | None = Field(None, description="Cycle for high energy/angle") + exp_time: float | None = Field(None, description="XRD trigger period") + n_of_trigger: int | None = Field(None, description="Amount of XRD triggers") start: float | None = Field(None, description="Start value for energy/angle") stop: float | None = Field(None, description="Stop value for energy/angle") p_kink: float | None = Field(None, description="P Kink") @@ -140,10 +142,12 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self.set_trig_settings( enable_low=False, enable_high=False, - exp_time_low=0, - exp_time_high=0, + break_time_low=0, + break_time_high=0, cycle_low=0, cycle_high=0, + exp_time=0, + n_of_trigger=0, ) self.set_scan_control_settings( mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration @@ -155,12 +159,14 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): scan_time=self.scan_parameter.scan_time, ) self.set_trig_settings( - enable_low=self.scan_parameter.xrd_enable_low, # enable_low=self.scan_parameter.trig_enable_low, - enable_high=self.scan_parameter.xrd_enable_high, # enable_high=self.scan_parameter.trig_enable_high, - exp_time_low=self.scan_parameter.exp_time_low, - exp_time_high=self.scan_parameter.exp_time_high, + enable_low=self.scan_parameter.break_enable_low, + enable_high=self.scan_parameter.break_enable_high, + break_time_low=self.scan_parameter.break_time_low, + break_time_high=self.scan_parameter.break_time_high, cycle_low=self.scan_parameter.cycle_low, cycle_high=self.scan_parameter.cycle_high, + exp_time=self.scan_parameter.exp_time, + n_of_trigger=self.scan_parameter.n_of_trigger, ) self.set_scan_control_settings( mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration @@ -176,10 +182,12 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self.set_trig_settings( enable_low=False, enable_high=False, - exp_time_low=0, - exp_time_high=0, + break_time_low=0, + break_time_high=0, cycle_low=0, cycle_high=0, + exp_time=0, + n_of_trigger=0, ) self.set_scan_control_settings( mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration @@ -193,12 +201,14 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): e_kink=self.scan_parameter.e_kink, ) self.set_trig_settings( - enable_low=self.scan_parameter.xrd_enable_low, # enable_low=self.scan_parameter.trig_enable_low, - enable_high=self.scan_parameter.xrd_enable_high, # enable_high=self.scan_parameter.trig_enable_high, - exp_time_low=self.scan_parameter.exp_time_low, - exp_time_high=self.scan_parameter.exp_time_high, + enable_low=self.scan_parameter.break_enable_low, + enable_high=self.scan_parameter.break_enable_high, + break_time_low=self.scan_parameter.break_time_low, + break_time_high=self.scan_parameter.break_time_high, cycle_low=self.scan_parameter.cycle_low, cycle_high=self.scan_parameter.cycle_high, + exp_time=self.scan_parameter.exp_time, + n_of_trigger=self.scan_parameter.n_of_trigger, ) self.set_scan_control_settings( mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration @@ -391,22 +401,25 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self, enable_low: bool, enable_high: bool, - exp_time_low: int, - exp_time_high: int, + break_time_low: float, + break_time_high: float, cycle_low: int, cycle_high: int, + exp_time: float, + n_of_trigger: int, + ) -> None: """Set TRIG settings for the upcoming scan. Args: enable_low (bool): Enable TRIG for low energy/angle enable_high (bool): Enable TRIG for high energy/angle - num_trigger_low (int): Number of triggers for low energy/angle - num_trigger_high (int): Number of triggers for high energy/angle - exp_time_low (int): Exposure time for low energy/angle - exp_time_high (int): Exposure time for high energy/angle + break_time_low (float): Exposure time for low energy/angle + break_time_high (float): Exposure time for high energy/angle cycle_low (int): Cycle for low energy/angle cycle_high (int): Cycle for high energy/angle + exp_time (float): Length of 1 trigger period in seconds + n_of_trigger (int): Amount of triggers to be fired during brake """ status_list = [] @@ -417,10 +430,10 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status_list.append(self.scan_settings.trig_ena_lo_enum.set(int(enable_low))) self.cancel_on_stop(status_list[-1]) - status_list.append(self.scan_settings.trig_time_hi.set(exp_time_high)) + status_list.append(self.scan_settings.trig_time_hi.set(break_time_high)) self.cancel_on_stop(status_list[-1]) - status_list.append(self.scan_settings.trig_time_lo.set(exp_time_low)) + status_list.append(self.scan_settings.trig_time_lo.set(break_time_low)) self.cancel_on_stop(status_list[-1]) status_list.append(self.scan_settings.trig_every_n_hi.set(cycle_high)) @@ -429,6 +442,12 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status_list.append(self.scan_settings.trig_every_n_lo.set(cycle_low)) self.cancel_on_stop(status_list[-1]) + status_list.append(self.trigger_settings.xrd_trig_period.set(exp_time)) + self.cancel_on_stop(status_list[-1]) + + status_list.append(self.trigger_settings.xrd_n_of_trig.set(n_of_trigger)) + self.cancel_on_stop(status_list[-1]) + for s in status_list: s.wait(timeout=self.timeout_for_pvwait) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 0977c17..6a4fe1a 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -139,21 +139,29 @@ class Mo1TriggerSettings(Device): xrd_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_src_ENUM", kind="config") xrd_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_mode_ENUM", kind="config") xrd_trig_len = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_len", kind="config") + xrd_trig_period = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_period", kind="config") + xrd_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="xrd_n_of_trig", kind="config") xrd_trig_req = Cpt(EpicsSignal, suffix="xrd_trig_req", kind="config") falcon_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_src_ENUM", kind="config") falcon_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_mode_ENUM", kind="config") falcon_trig_len = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_len", kind="config") + falcon_trig_period = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_period", kind="config") + falcon_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="falcon_n_of_trig", kind="config") falcon_trig_req = Cpt(EpicsSignal, suffix="falcon_trig_req", kind="config") univ1_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_src_ENUM", kind="config") univ1_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_mode_ENUM", kind="config") univ1_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_len", kind="config") + univ1_trig_period = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_period", kind="config") + univ1_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="univ1_n_of_trig", kind="config") univ1_trig_req = Cpt(EpicsSignal, suffix="univ1_trig_req", kind="config") univ2_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_src_ENUM", kind="config") univ2_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_mode_ENUM", kind="config") univ2_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_len", kind="config") + univ2_trig_period = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_period", kind="config") + univ2_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="univ2_n_of_trig", kind="config") univ2_trig_req = Cpt(EpicsSignal, suffix="univ2_trig_req", kind="config") diff --git a/debye_bec/scans/mono_bragg_scans.py b/debye_bec/scans/mono_bragg_scans.py index 1dc131b..03c234b 100644 --- a/debye_bec/scans/mono_bragg_scans.py +++ b/debye_bec/scans/mono_bragg_scans.py @@ -108,8 +108,9 @@ class XASSimpleScanWithXRD(XASSimpleScan): gui_config = { "Movement Parameters": ["start", "stop"], "Scan Parameters": ["scan_time", "scan_duration"], - "Low Energy Range": ["xrd_enable_low", "num_trigger_low", "exp_time_low", "cycle_low"], - "High Energy Range": ["xrd_enable_high", "num_trigger_high", "exp_time_high", "cycle_high"], + "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], + "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], + "XRD Triggers": ["exp_time", "n_of_trigger"], } def __init__( @@ -118,14 +119,14 @@ class XASSimpleScanWithXRD(XASSimpleScan): stop: float, scan_time: float, scan_duration: float, - xrd_enable_low: bool, - num_trigger_low: int, - exp_time_low: float, + break_enable_low: bool, + break_time_low: float, cycle_low: int, - xrd_enable_high: bool, - num_trigger_high: int, - exp_time_high: float, + break_enable_high: bool, + break_time_high: float, cycle_high: float, + exp_time: float, + n_of_trigger: int, motor: DeviceBase = "mo1_bragg", **kwargs, ): @@ -138,16 +139,16 @@ class XASSimpleScanWithXRD(XASSimpleScan): stop (float): Stop energy for the scan. scan_time (float): Time for one oscillation . scan_duration (float): Total duration of the scan. - xrd_enable_low (bool): Enable XRD triggering for the low energy range. - num_trigger_low (int): Number of triggers for the low energy range. - exp_time_low (float): Exposure time for the low energy range. + break_enable_low (bool): Enable breaks for the low energy range. + break_time_low (float): Break time for the low energy range. cycle_low (int): Specify how often the triggers should be considered, every nth cycle for low - xrd_enable_high (bool): Enable XRD triggering for the high energy range. - num_trigger_high (int): Number of triggers for the high energy range. - exp_time_high (float): Exposure time for the high energy range. + break_enable_high (bool): Enable breaks for the high energy range. + break_time_high (float): Break time for the high energy range. cycle_high (int): Specify how often the triggers should be considered, every nth cycle for high + exp_time (float): Length of 1 trigger period in seconds + n_of_trigger (int): Amount of triggers to be fired during break motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg". @@ -162,14 +163,14 @@ class XASSimpleScanWithXRD(XASSimpleScan): motor=motor, **kwargs, ) - self.xrd_enable_low = xrd_enable_low - self.num_trigger_low = num_trigger_low - self.exp_time_low = exp_time_low + self.break_enable_low = break_enable_low + self.break_time_low = break_time_low self.cycle_low = cycle_low - self.xrd_enable_high = xrd_enable_high - self.num_trigger_high = num_trigger_high - self.exp_time_high = exp_time_high + self.break_enable_high = break_enable_high + self.break_time_high = break_time_high self.cycle_high = cycle_high + self.exp_time = exp_time + self.n_of_trigger = n_of_trigger class XASAdvancedScan(XASSimpleScan): @@ -233,8 +234,9 @@ class XASAdvancedScanWithXRD(XASAdvancedScan): "Movement Parameters": ["start", "stop"], "Scan Parameters": ["scan_time", "scan_duration"], "Spline Parameters": ["p_kink", "e_kink"], - "Low Energy Range": ["xrd_enable_low", "num_trigger_low", "exp_time_low", "cycle_low"], - "High Energy Range": ["xrd_enable_high", "num_trigger_high", "exp_time_high", "cycle_high"], + "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], + "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], + "XRD Triggers": ["exp_time", "n_of_trigger"], } def __init__( @@ -245,14 +247,14 @@ class XASAdvancedScanWithXRD(XASAdvancedScan): scan_duration: float, p_kink: float, e_kink: float, - xrd_enable_low: bool, - num_trigger_low: int, - exp_time_low: float, + break_enable_low: bool, + break_time_low: float, cycle_low: int, - xrd_enable_high: bool, - num_trigger_high: int, - exp_time_high: float, + break_enable_high: bool, + break_time_high: float, cycle_high: float, + exp_time: float, + n_of_trigger: int, motor: DeviceBase = "mo1_bragg", **kwargs, ): @@ -270,16 +272,16 @@ class XASAdvancedScanWithXRD(XASAdvancedScan): scan_duration (float): Total duration of the scan. p_kink (float): Position of kink. e_kink (float): Energy of the kink. - xrd_enable_low (bool): Enable XRD triggering for the low energy range. - num_trigger_low (int): Number of triggers for the low energy range. - exp_time_low (float): Exposure time for the low energy range. + break_enable_low (bool): Enable breaks for the low energy range. + break_time_low (float): Break time for the low energy range. cycle_low (int): Specify how often the triggers should be considered, every nth cycle for low - xrd_enable_high (bool): Enable XRD triggering for the high energy range. - num_trigger_high (int): Number of triggers for the high energy range. - exp_time_high (float): Exposure time for the high energy range. + break_enable_high (bool): Enable breaks for the high energy range. + break_time_high (float): Break time for the high energy range. cycle_high (int): Specify how often the triggers should be considered, every nth cycle for high + exp_time (float): Length of 1 trigger period in seconds + n_of_trigger (int): Amount of triggers to be fired during break motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg". @@ -298,11 +300,11 @@ class XASAdvancedScanWithXRD(XASAdvancedScan): ) self.p_kink = p_kink self.e_kink = e_kink - self.xrd_enable_low = xrd_enable_low - self.num_trigger_low = num_trigger_low - self.exp_time_low = exp_time_low + self.break_enable_low = break_enable_low + self.break_time_low = break_time_low self.cycle_low = cycle_low - self.xrd_enable_high = xrd_enable_high - self.num_trigger_high = num_trigger_high - self.exp_time_high = exp_time_high + self.break_enable_high = break_enable_high + self.break_time_high = break_time_high self.cycle_high = cycle_high + self.exp_time = exp_time + self.n_of_trigger = n_of_trigger -- 2.49.1 From ee748d56c4dbe637e37298fac7d287a06e5176b5 Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Wed, 10 Sep 2025 16:55:20 +0200 Subject: [PATCH 06/21] added timestamp signals for nidaq --- debye_bec/devices/nidaq/nidaq.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 2942559..39db508 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -302,6 +302,14 @@ class NidaqControl(Device): SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD" ) + xas_timestamp = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp" + ) + + xrd_timestamp = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp" + ) + di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX") -- 2.49.1 From 1f7fdb89d72ca8b8530c73ed3715744a80e456a1 Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Wed, 10 Sep 2025 16:55:52 +0200 Subject: [PATCH 07/21] add on_stage for xas_xrd scans --- debye_bec/devices/pilatus/pilatus.py | 175 +++++++++++++++++++++------ 1 file changed, 137 insertions(+), 38 deletions(-) diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index ba307cc..be471c4 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -25,6 +25,7 @@ from ophyd_devices import ( PreviewSignal, ) from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from pydantic import BaseModel, Field if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -95,6 +96,32 @@ class TRIGGERMODE(int, enum.Enum): def __str__(self): return self.description() + + + +class ScanParameter(BaseModel): + """Dataclass to store the scan parameters for the Pilatus. + This needs to be in sync with the kwargs of the XRD related scans from Debye, to + ensure that the scan parameters are correctly set. Any changes in the scan kwargs, + i.e. renaming or adding new parameters, need to be represented here as well.""" + + scan_time: float | None = Field(None, description="Scan time for a half oscillation") + scan_duration: float | None = Field(None, description="Duration of the scan") + break_enable_low: bool | None = Field( + None, description="Break enabled for low, should be PV trig_ena_lo_enum" + ) # trig_enable_low: bool = None + break_enable_high: bool | None = Field( + None, description="Break enabled for high, should be PV trig_ena_hi_enum" + ) # trig_enable_high: bool = None + break_time_low: float | None = Field(None, description="Break time low energy/angle") + break_time_high: float | None = Field(None, description="Break time high energy/angle") + cycle_low: int | None = Field(None, description="Cycle for low energy/angle") + cycle_high: int | None = Field(None, description="Cycle for high energy/angle") + exp_time: float | None = Field(None, description="XRD trigger period") + n_of_trigger: int | None = Field(None, description="Amount of XRD triggers") + start: float | None = Field(None, description="Start value for energy/angle") + stop: float | None = Field(None, description="Stop value for energy/angle") + model_config: dict = {"validate_assignment": True} class Pilatus(PSIDeviceBase, ADBase): @@ -170,6 +197,7 @@ class Pilatus(PSIDeviceBase, ADBase): super().__init__( name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs ) + self.scan_parameter = ScanParameter() self.device_manager = device_manager self._readout_time = PILATUS_READOUT_TIME self._full_path = "" @@ -178,6 +206,10 @@ class Pilatus(PSIDeviceBase, ADBase): ) self._poll_thread_kill_event = threading.Event() self._poll_rate = 1 # Poll rate in Hz + self.xas_xrd_scan_names = [ + "xas_simple_scan_with_xrd", + "xas_advanced_scan_with_xrd", + ] # self._live_mode_thread = threading.Thread( # target=self._live_mode_loop, daemon=True, name=f"{self.name}_live_mode_thread" # ) @@ -354,50 +386,108 @@ class Pilatus(PSIDeviceBase, ADBase): (self.scan_info.msg) object. """ # self.stop_live_mode() # Make sure that live mode is stopped if scan runs + self._update_scan_parameter() scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name.startswith("xas"): - return None - # TODO implement logic for 'xas' scans - else: - exp_time = scan_msg.scan_parameters.get("exp_time", 0.0) - if exp_time - self._readout_time <= 0: - raise ValueError( - f"Exposure time {exp_time} is too short for Pilatus with readout_time {self._readout_time}." - ) - detector_exp_time = exp_time - self._readout_time - n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) - self._full_path = get_full_path(scan_msg, name="pilatus") - file_path = "/".join(self._full_path.split("/")[:-1]) - file_name = self._full_path.split("/")[-1] + if scan_msg.scan_name in self.xas_xrd_scan_names: + total_osc = 0 + total_trig_lo = 0 + total_trig_hi = 0 + calc_duration = 0 + n_trig_lo = 1 + n_trig_hi = 1 + init_lo = 1 + init_hi = 1 + lo_done = 0 + hi_done = 0 + if not self.scan_parameter.break_enable_low: + lo_done = 1 + if not self.scan_parameter.break_enable_high: + hi_done = 1 + while True: + total_osc = total_osc + 2 + calc_duration = calc_duration + 2 * self.scan_parameter.scan_time + + if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low: + n_trig_lo = 1 + calc_duration = calc_duration + self.scan_parameter.break_time_low + if init_lo: + lo_done = 1 + init_lo = 0 + else: + n_trig_lo += 1 - # Prepare detector and backend - self.cam.array_callbacks.set(1).wait(5) # Enable array callbacks - self.hdf.enable.set(1).wait(5) # Enable HDF5 plugin - # Camera settings - self.cam.num_exposures.set(1).wait(5) - self.cam.num_images.set(n_images).wait(5) - self.cam.acquire_time.set(detector_exp_time).wait(5) # let's try this - self.cam.acquire_period.set(exp_time).wait(5) - self.filter_number.set(0).wait(5) - # HDF5 settings - logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}") - self.hdf.file_path.set(file_path).wait(5) - self.hdf.file_name.set(file_name).wait(5) - self.hdf.num_capture.set(n_images).wait(5) - self.cam.array_counter.set(0).wait(5) # Reset array counter - self.file_event.put( - file_path=self._full_path, - done=False, - successful=False, - hinted_h5_entries={"data": "/entry/data/data"}, - ) + if self.scan_parameter.break_enable_high and n_trig_hi >= self.scan_parameter.cycle_high: + n_trig_hi = 1 + calc_duration = calc_duration + self.scan_parameter.break_time_high + if init_hi: + hi_done = 1 + init_hi = 0 + else: + n_trig_hi += 1 + + if lo_done and hi_done: + n = np.floor(self.scan_parameter.scan_duration / calc_duration) + total_osc = total_osc * n + if self.scan_parameter.break_enable_low: + total_trig_lo = n + 1 + if self.scan_parameter.break_enable_high: + total_trig_hi = n + 1 + calc_duration = calc_duration * n + lo_done = 0 + hi_done = 0 + + if calc_duration >= self.scan_parameter.scan_duration: + break + + # logger.info(f'total_osc: {total_osc}') + # logger.info(f'total trig low: {total_trig_lo}') + # logger.info(f'total trig high: {total_trig_hi}') + + n_images = total_trig_lo + total_trig_hi + exp_time = self.scan_parameter.exp_time + + elif scan_msg.scan_type == 'step': + n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) + exp_time = scan_msg.scan_parameters.get("exp_time") + else: + return None + # Common settings + if exp_time - self._readout_time <= 0: + raise ValueError((f"Exposure time {exp_time} is too short ", + f"for Pilatus with readout_time {self._readout_time}." + )) + detector_exp_time = exp_time - self._readout_time + self._full_path = get_full_path(scan_msg, name="pilatus") + file_path = "/".join(self._full_path.split("/")[:-1]) + file_name = self._full_path.split("/")[-1] + # Prepare detector and backend + self.cam.array_callbacks.set(1).wait(5) # Enable array callbacks + self.hdf.enable.set(1).wait(5) # Enable HDF5 plugin + # Camera settings + self.cam.num_exposures.set(1).wait(5) + self.cam.num_images.set(n_images).wait(5) + self.cam.acquire_time.set(detector_exp_time).wait(5) # let's try this + self.cam.acquire_period.set(exp_time).wait(5) + self.filter_number.set(0).wait(5) + # HDF5 settings + logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}") + self.hdf.file_path.set(file_path).wait(5) + self.hdf.file_name.set(file_name).wait(5) + self.hdf.num_capture.set(n_images).wait(5) + self.cam.array_counter.set(0).wait(5) # Reset array counter + self.file_event.put( + file_path=self._full_path, + done=False, + successful=False, + hinted_h5_entries={"data": "/entry/data/data"}, + ) def on_unstage(self) -> None: """Called while unstaging the device.""" def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" - if self.scan_info.msg.scan_name.startswith("xas"): + if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: # TODO implement logic for 'xas' scans return None else: @@ -414,7 +504,7 @@ class Pilatus(PSIDeviceBase, ADBase): def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" - if self.scan_info.msg.scan_name.startswith("xas"): + if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: return None # TODO implement logic for 'xas' scans else: @@ -446,7 +536,7 @@ class Pilatus(PSIDeviceBase, ADBase): def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" - if self.scan_info.msg.scan_name.startswith("xas"): + if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: # TODO implement logic for 'xas' scans return None status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) @@ -477,6 +567,15 @@ class Pilatus(PSIDeviceBase, ADBase): # TODO do we need to clean the poll thread ourselves? self.on_stop() + def _update_scan_parameter(self): + """Get the scan_info parameters for the scan.""" + for key, value in self.scan_info.msg.request_inputs["inputs"].items(): + if hasattr(self.scan_parameter, key): + setattr(self.scan_parameter, key, value) + for key, value in self.scan_info.msg.request_inputs["kwargs"].items(): + if hasattr(self.scan_parameter, key): + setattr(self.scan_parameter, key, value) + if __name__ == "__main__": try: -- 2.49.1 From 626b0dc8a0ea01ba1bbb04ec0cfe88043cdb6f2b Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Thu, 11 Sep 2025 09:14:00 +0200 Subject: [PATCH 08/21] added xrd energy signal --- debye_bec/devices/nidaq/nidaq.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 39db508..4b2a792 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -310,6 +310,10 @@ class NidaqControl(Device): SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp" ) + xrd_energy = Cpt( + SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy" + ) + di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX") -- 2.49.1 From 4424f83b8b004a1e55b0f4a420e2b835126d0f5e Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Thu, 11 Sep 2025 09:19:51 +0200 Subject: [PATCH 09/21] working example of combined xas_xrd scan --- debye_bec/devices/pilatus/pilatus.py | 63 +++++++++++++++------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index be471c4..c30b44d 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -83,6 +83,13 @@ class TRIGGERMODE(int, enum.Enum): MULT_TRIGGER = 3 ALIGNMENT = 4 +class MONO_TRIGGER_SOURCE(int, enum.Enum): + """"Mono XRD trigger source""" + + EPICS = 0 + INPOS = 1 + + def description(self) -> str: """Return a description of the trigger mode.""" descriptions = { @@ -210,6 +217,7 @@ class Pilatus(PSIDeviceBase, ADBase): "xas_simple_scan_with_xrd", "xas_advanced_scan_with_xrd", ] + self.n_images = None # self._live_mode_thread = threading.Thread( # target=self._live_mode_loop, daemon=True, name=f"{self.name}_live_mode_thread" # ) @@ -226,7 +234,7 @@ class Pilatus(PSIDeviceBase, ADBase): """Poll the array data for preview updates.""" while not self._poll_thread_kill_event.wait(1 / self._poll_rate): try: - logger.info(f"Running poll loop for {self.name}..") + # logger.info(f"Running poll loop for {self.name}..") value = self.image1.array_data.get() if value is None: continue @@ -235,7 +243,7 @@ class Pilatus(PSIDeviceBase, ADBase): # Geometry correction for the image data = np.reshape(value, (height, width)) last_image: DevicePreviewMessage = self.preview.get() - logger.info(f"Preview image for {self.name} has shape {data.shape}") + # logger.info(f"Preview image for {self.name} has shape {data.shape}") if last_image is not None: if np.array_equal(data, last_image.data): # No update if image is the same, ~2.5ms on 2400x2400 image (6M) @@ -443,12 +451,14 @@ class Pilatus(PSIDeviceBase, ADBase): # logger.info(f'total trig low: {total_trig_lo}') # logger.info(f'total trig high: {total_trig_hi}') - n_images = total_trig_lo + total_trig_hi + self.n_images = (total_trig_lo + total_trig_hi) * self.scan_parameter.n_of_trigger exp_time = self.scan_parameter.exp_time + self.trigger_source.set(MONO_TRIGGER_SOURCE.INPOS).wait(5) elif scan_msg.scan_type == 'step': - n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) + self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) exp_time = scan_msg.scan_parameters.get("exp_time") + self.trigger_source.set(MONO_TRIGGER_SOURCE.EPICS).wait(5) else: return None # Common settings @@ -465,15 +475,15 @@ class Pilatus(PSIDeviceBase, ADBase): self.hdf.enable.set(1).wait(5) # Enable HDF5 plugin # Camera settings self.cam.num_exposures.set(1).wait(5) - self.cam.num_images.set(n_images).wait(5) + self.cam.num_images.set(self.n_images).wait(5) self.cam.acquire_time.set(detector_exp_time).wait(5) # let's try this self.cam.acquire_period.set(exp_time).wait(5) self.filter_number.set(0).wait(5) # HDF5 settings - logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}") + logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}. full_path is {self._full_path}") self.hdf.file_path.set(file_path).wait(5) self.hdf.file_name.set(file_name).wait(5) - self.hdf.num_capture.set(n_images).wait(5) + self.hdf.num_capture.set(self.n_images).wait(5) self.cam.array_counter.set(0).wait(5) # Reset array counter self.file_event.put( file_path=self._full_path, @@ -487,26 +497,21 @@ class Pilatus(PSIDeviceBase, ADBase): def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" - if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: - # TODO implement logic for 'xas' scans - return None - else: - status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) - status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) - status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) - status = AndStatusWithList( - device=self, status_list=[status_hdf, status_cam, status_cam_server] - ) - self.cam.acquire.put(1) - self.hdf.capture.put(1) - self.cancel_on_stop(status) - return status + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_cam_server] + ) + self.cam.acquire.put(1) + self.hdf.capture.put(1) + self.cancel_on_stop(status) + return status def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: return None - # TODO implement logic for 'xas' scans else: start_time = time.time() logger.warning(f"Triggering image with num_captured {self.hdf.num_captured.get()}") @@ -536,16 +541,16 @@ class Pilatus(PSIDeviceBase, ADBase): def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" - if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: - # TODO implement logic for 'xas' scans - return None status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) - num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( - "frames_per_trigger", 1 - ) - status_img_written = CompareStatus(self.hdf.num_captured, num_images) + if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: + # For long scans, it can be that the mono will execute one cycle more, + # meaning a few more XRD triggers will be sent + status_img_written = CompareStatus(self.hdf.num_captured, self.n_images, operation='>=') + else: + status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) + status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) status = AndStatusWithList( device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] ) -- 2.49.1 From 217a14d03d9644b7bab53edc78740234ad698feb Mon Sep 17 00:00:00 2001 From: gac-x01da Date: Thu, 11 Sep 2025 11:19:29 +0200 Subject: [PATCH 10/21] configure mono trigger signal --- debye_bec/devices/pilatus/pilatus.py | 34 +++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index c30b44d..5326417 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -83,12 +83,18 @@ class TRIGGERMODE(int, enum.Enum): MULT_TRIGGER = 3 ALIGNMENT = 4 -class MONO_TRIGGER_SOURCE(int, enum.Enum): +class MONOTRIGGERSOURCE(int, enum.Enum): """"Mono XRD trigger source""" EPICS = 0 INPOS = 1 +class MONOTRIGGERMODE(int, enum.Enum): + """"Mono XRD trigger mode""" + + PULSE = 0 + CONDITION = 1 + def description(self) -> str: """Return a description of the trigger mode.""" @@ -181,8 +187,24 @@ class Pilatus(PSIDeviceBase, ADBase): write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_len", add_prefix=("a",), kind=Kind.omitted, + doc="Trigger Period in seconds", + ) + trigger_period = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_period_RBV", + write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_period", + add_prefix=("a",), + kind=Kind.omitted, doc="Trigger Pulse Length in seconds", ) + trigger_n_of = Cpt( + EpicsSignal, + read_pv="X01DA-OP-MO1:BRAGG:xrd_n_of_trig_RBV", + write_pv="X01DA-OP-MO1:BRAGG:xrd_n_of_trig", + add_prefix=("a",), + kind=Kind.omitted, + doc="Number of trigger to generate for each request", + ) preview = Cpt( PreviewSignal, name="preview", @@ -453,15 +475,21 @@ class Pilatus(PSIDeviceBase, ADBase): self.n_images = (total_trig_lo + total_trig_hi) * self.scan_parameter.n_of_trigger exp_time = self.scan_parameter.exp_time - self.trigger_source.set(MONO_TRIGGER_SOURCE.INPOS).wait(5) + self.trigger_source.set(MONOTRIGGERSOURCE.INPOS).wait(5) + self.trigger_n_of.set(self.scan_parameter.n_of_trigger).wait(5) elif scan_msg.scan_type == 'step': self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) exp_time = scan_msg.scan_parameters.get("exp_time") - self.trigger_source.set(MONO_TRIGGER_SOURCE.EPICS).wait(5) + self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5) + self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition else: return None # Common settings + self.trigger_mode.set(MONOTRIGGERMODE.PULSE).wait(5) + self.trigger_period.set(exp_time).wait(5) + self.trigger_pulse_length.set(0.005).wait(5) # Pulse length of 5 ms enough for Pilatus and NIDAQ + if exp_time - self._readout_time <= 0: raise ValueError((f"Exposure time {exp_time} is too short ", f"for Pilatus with readout_time {self._readout_time}." -- 2.49.1 From a6f0d01558fcfea77f0e15ab9fff4b2614d75e0d Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 08:38:11 +0200 Subject: [PATCH 11/21] updated/added configs --- .../device_configs/x01da_beam_monitors.yaml | 34 + debye_bec/device_configs/x01da_database.yaml | 875 ------------------ .../x01da_experimental_hutch.yaml | 389 ++++++++ debye_bec/device_configs/x01da_frontend.yaml | 218 +++++ debye_bec/device_configs/x01da_machine.yaml | 5 + ...1da_optic_slits.yaml => x01da_optics.yaml} | 180 +++- debye_bec/device_configs/x01da_pilatus.yaml | 34 - .../device_configs/x01da_standard_config.yaml | 75 ++ .../device_configs/x01da_test_config.yaml | 636 ------------- debye_bec/device_configs/x01da_xas.yaml | 73 ++ debye_bec/device_configs/x01da_xrd.yaml | 108 +++ 11 files changed, 1076 insertions(+), 1551 deletions(-) create mode 100644 debye_bec/device_configs/x01da_beam_monitors.yaml delete mode 100644 debye_bec/device_configs/x01da_database.yaml create mode 100644 debye_bec/device_configs/x01da_experimental_hutch.yaml create mode 100644 debye_bec/device_configs/x01da_frontend.yaml rename debye_bec/device_configs/{x01da_optic_slits.yaml => x01da_optics.yaml} (55%) delete mode 100644 debye_bec/device_configs/x01da_pilatus.yaml create mode 100644 debye_bec/device_configs/x01da_standard_config.yaml delete mode 100644 debye_bec/device_configs/x01da_test_config.yaml create mode 100644 debye_bec/device_configs/x01da_xas.yaml create mode 100644 debye_bec/device_configs/x01da_xrd.yaml diff --git a/debye_bec/device_configs/x01da_beam_monitors.yaml b/debye_bec/device_configs/x01da_beam_monitors.yaml new file mode 100644 index 0000000..d2e3a02 --- /dev/null +++ b/debye_bec/device_configs/x01da_beam_monitors.yaml @@ -0,0 +1,34 @@ + +################################### +## Beam Monitors ## +################################### + +beam_monitor_1: + readoutPriority: async + description: Beam monitor 1 + deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam + deviceConfig: + prefix: "X01DA-OP-GIGE01:" + onFailure: retry + enabled: true + softwareTrigger: false + +beam_monitor_2: + readoutPriority: async + description: Beam monitor 2 + deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam + deviceConfig: + prefix: "X01DA-OP-GIGE02:" + onFailure: retry + enabled: true + softwareTrigger: false + +xray_eye: + readoutPriority: async + description: X-ray eye + deviceClass: debye_bec.devices.cameras.basler_cam.BaslerCam + deviceConfig: + prefix: "X01DA-ES-XRAYEYE:" + onFailure: retry + enabled: true + softwareTrigger: false diff --git a/debye_bec/device_configs/x01da_database.yaml b/debye_bec/device_configs/x01da_database.yaml deleted file mode 100644 index 7ef4b68..0000000 --- a/debye_bec/device_configs/x01da_database.yaml +++ /dev/null @@ -1,875 +0,0 @@ -################### -#### FRONT END #### -################### - -## Slit Diaphragm -- Physical positioners -sldi_trxr: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Ring-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRXR - onFailure: retry - enabled: true - softwareTrigger: false -sldi_trxw: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Wall-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRXW - onFailure: retry - enabled: true - softwareTrigger: false -sldi_tryb: - readoutPriority: baseline - description: Front-end slit diaphragm Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRYB - onFailure: retry - enabled: true - softwareTrigger: false -sldi_tryt: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Top-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRYT - onFailure: retry - enabled: true - softwareTrigger: false - -## Slit Diaphragm -- Virtual positioners - -sldi_centerx: - readoutPriority: baseline - description: Front-end slit diaphragm X-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:CENTERX - onFailure: retry - enabled: true - softwareTrigger: false -sldi_gapx: - readoutPriority: baseline - description: Front-end slit diaphragm X-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:GAPX - onFailure: retry - enabled: true - softwareTrigger: false -sldi_centery: - readoutPriority: baseline - description: Front-end slit diaphragm Y-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:CENTERY - onFailure: retry - enabled: true - softwareTrigger: false -sldi_gapy: - readoutPriority: baseline - description: Front-end slit diaphragm Y-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:GAPY - onFailure: retry - enabled: true - softwareTrigger: false - -## Collimating Mirror -- Physical Positioners - -cm_trxu: - readoutPriority: baseline - description: Collimating Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRXU - onFailure: retry - enabled: true - softwareTrigger: false -cm_trxd: - readoutPriority: baseline - description: Collimating Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRXD - onFailure: retry - enabled: true - softwareTrigger: false -cm_tryu: - readoutPriority: baseline - description: Collimating Mirror Y-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYU - onFailure: retry - enabled: true - softwareTrigger: false -cm_trydr: - readoutPriority: baseline - description: Collimating Mirror Y-translation downstream ring - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYDR - onFailure: retry - enabled: true - softwareTrigger: false -cm_trydw: - readoutPriority: baseline - description: Collimating Mirror Y-translation downstream wall - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYDW - onFailure: retry - enabled: true - softwareTrigger: false -cm_bnd: - readoutPriority: baseline - description: Collimating Mirror bender - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:BND - onFailure: retry - enabled: true - softwareTrigger: false - -## Collimating Mirror -- Virtual Positioners - -cm_rotx: - readoutPriority: baseline - description: Collimating Morror Pitch - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTX - onFailure: retry - enabled: true - softwareTrigger: false -cm_roty: - readoutPriority: baseline - description: Collimating Morror Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTY - onFailure: retry - enabled: true - softwareTrigger: false -cm_rotz: - readoutPriority: baseline - description: Collimating Morror Roll - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTZ - onFailure: retry - enabled: true - softwareTrigger: false -cm_xctp: - readoutPriority: baseline - description: Collimating Morror Center Point X - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:XTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_ytcp: - readoutPriority: baseline - description: Collimating Morror Center Point Y - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:YTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_ztcp: - readoutPriority: baseline - description: Collimating Morror Center Point Z - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ZTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_xstripe: - readoutPriority: baseline - description: Collimating Morror X Stripe - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:XSTRIPE - onFailure: retry - enabled: true - softwareTrigger: false - -################### -###### OPTICS ##### -################### - -## Bragg Monochromator -mo1_bragg: - readoutPriority: baseline - description: Positioner for the Monochromator - deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg - deviceConfig: - prefix: "X01DA-OP-MO1:BRAGG:" - onFailure: retry - enabled: true - softwareTrigger: false - -## Monochromator -- Physical Positioners - -mo_try: - readoutPriority: baseline - description: Monochromator Y Translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:TRY - onFailure: retry - enabled: true - softwareTrigger: false -mo_trx: - readoutPriority: baseline - description: Monochromator X Translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:TRY - onFailure: retry - enabled: true - softwareTrigger: false -mo_roty: - readoutPriority: baseline - description: Monochromator Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:ROTY - onFailure: retry - enabled: true - softwareTrigger: false - -## Focusing Mirror -- Physical Positioners - -fm_trxu: - readoutPriority: baseline - description: Focusing Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRXU - onFailure: retry - enabled: true - softwareTrigger: false -fm_trxd: - readoutPriority: baseline - description: Focusing Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRXD - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryd: - readoutPriority: baseline - description: Focusing Mirror Y-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYD - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryur: - readoutPriority: baseline - description: Focusing Mirror Y-translation upstream ring - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYUR - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryuw: - readoutPriority: baseline - description: Focusing Mirror Y-translation upstream wall - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYUW - onFailure: retry - enabled: true - softwareTrigger: false -fm_bnd: - readoutPriority: baseline - description: Focusing Mirror bender - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:BND - onFailure: retry - enabled: true - softwareTrigger: false - -## Focusing Mirror -- Virtual Positioners - -fm_rotx: - readoutPriority: baseline - description: Focusing Morror Pitch - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTX - onFailure: retry - enabled: true - softwareTrigger: false -fm_roty: - readoutPriority: baseline - description: Focusing Morror Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTY - onFailure: retry - enabled: true - softwareTrigger: false -fm_rotz: - readoutPriority: baseline - description: Focusing Morror Roll - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTZ - onFailure: retry - enabled: true - softwareTrigger: false -fm_xctp: - readoutPriority: baseline - description: Focusing Morror Center Point X - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:XTCP - onFailure: retry - enabled: true - softwareTrigger: false -fm_ytcp: - readoutPriority: baseline - description: Focusing Morror Center Point Y - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:YTCP - onFailure: retry - enabled: true - softwareTrigger: false -fm_ztcp: - readoutPriority: baseline - description: Focusing Morror Center Point Z - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ZTCP - onFailure: retry - enabled: true - softwareTrigger: false -# fm_xstripe: -# readoutPriority: baseline -# description: Focusing Morror X Stripe -# deviceClass: ophyd.EpicsMotor -# deviceConfig: -# prefix: X01DA-OP-FM:XSTRIPE -# onFailure: retry -# enabled: true -# softwareTrigger: false - -## Optics Slits 1 -- Physical positioners - -sl1_trxr: - readoutPriority: baseline - description: Optics slits 1 X-translation Ring-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:TRXR - onFailure: retry - enabled: true - softwareTrigger: false -sl1_trxw: - readoutPriority: baseline - description: Optics slits 1 X-translation Wall-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:TRXW - onFailure: retry - enabled: true - softwareTrigger: false -sl1_tryb: - readoutPriority: baseline - description: Optics slits 1 Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:TRYB - onFailure: retry - enabled: true - softwareTrigger: false -sl1_tryt: - readoutPriority: baseline - description: Optics slits 1 X-translation Top-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:TRYT - onFailure: retry - enabled: true - softwareTrigger: false -bm1_try: - readoutPriority: baseline - description: Beam Monitor 1 Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-BM1:TRY - onFailure: retry - enabled: true - softwareTrigger: false - -## Optics Slits 1 -- Virtual positioners - -sl1_centerx: - readoutPriority: baseline - description: Optics slits 1 X-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:CENTERX - onFailure: retry - enabled: true - softwareTrigger: false -sl1_gapx: - readoutPriority: baseline - description: Optics slits 1 X-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:GAPX - onFailure: retry - enabled: true - softwareTrigger: false -sl1_centery: - readoutPriority: baseline - description: Optics slits 1 Y-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:CENTERY - onFailure: retry - enabled: true - softwareTrigger: false -sl1_gapy: - readoutPriority: baseline - description: Optics slits 1 Y-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL1:GAPY - onFailure: retry - enabled: true - softwareTrigger: false - -## Optics Slits 2 -- Physical positioners - -sl2_trxr: - readoutPriority: baseline - description: Optics slits 2 X-translation Ring-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:TRXR - onFailure: retry - enabled: true - softwareTrigger: false -sl2_trxw: - readoutPriority: baseline - description: Optics slits 2 X-translation Wall-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:TRXW - onFailure: retry - enabled: true - softwareTrigger: false -sl2_tryb: - readoutPriority: baseline - description: Optics slits 2 Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:TRYB - onFailure: retry - enabled: true - softwareTrigger: false -sl2_tryt: - readoutPriority: baseline - description: Optics slits 2 X-translation Top-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:TRYT - onFailure: retry - enabled: true - softwareTrigger: false -bm2_try: - readoutPriority: baseline - description: Beam Monitor 2 Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-BM2:TRY - onFailure: retry - enabled: true - softwareTrigger: false - -## Optics Slits 2 -- Virtual positioners - -sl2_centerx: - readoutPriority: baseline - description: Optics slits 2 X-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:CENTERX - onFailure: retry - enabled: true - softwareTrigger: false -sl2_gapx: - readoutPriority: baseline - description: Optics slits 2 X-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:GAPX - onFailure: retry - enabled: true - softwareTrigger: false -sl2_centery: - readoutPriority: baseline - description: Optics slits 2 Y-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:CENTERY - onFailure: retry - enabled: true - softwareTrigger: false -sl2_gapy: - readoutPriority: baseline - description: Optics slits 2 Y-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-SL2:GAPY - onFailure: retry - enabled: true - softwareTrigger: false - -############################### -###### EXPERIMENTAL HUTCH ##### -############################### - -########################################### -## Optical Table -- Physical Positioners ## -########################################### - -ot_tryu: - readoutPriority: baseline - description: Optical Table Y-Translation Upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES-OT:TRYU - onFailure: retry - enabled: true - softwareTrigger: false -ot_tryd: - readoutPriority: baseline - description: Optical Table Y-Translation Downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES-OT:TRYD - onFailure: retry - enabled: true - softwareTrigger: false - -############################################ -## Optical Table -- Virtual Positioners ### -############################################ - -ot_try: - readoutPriority: baseline - description: Optical Table Y-Translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES-OT:TRY - onFailure: retry - enabled: true - softwareTrigger: false -ot_pitch: - readoutPriority: baseline - description: Optical Table Pitch - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES-OT:ROTX - onFailure: retry - enabled: true - softwareTrigger: false - -######################################### -## Exit Window -- Physical Positioners ## -######################################### - -es0wi_try: - readoutPriority: baseline - description: End Station 0 Exit Window Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-WI:TRY - onFailure: retry - enabled: true - softwareTrigger: false - -############################################### -## End Station Slits -- Physical Positioners ## -############################################### - -es0sl_trxr: - readoutPriority: baseline - description: End Station slits X-translation Ring-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:TRXR - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_trxw: - readoutPriority: baseline - description: End Station slits X-translation Wall-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:TRXW - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_tryb: - readoutPriority: baseline - description: End Station slits Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:TRYB - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_tryt: - readoutPriority: baseline - description: End Station slits X-translation Top-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:TRYT - onFailure: retry - enabled: true - softwareTrigger: false - -############################################## -## End Station Slits -- Virtual positioners ## -############################################## - -es0sl_center: - readoutPriority: baseline - description: End Station slits X-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:CENTERX - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_gapx: - readoutPriority: baseline - description: End Station slits X-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:GAPX - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_centery: - readoutPriority: baseline - description: End Station slits Y-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:CENTERY - onFailure: retry - enabled: true - softwareTrigger: false -es0sl_gapy: - readoutPriority: baseline - description: End Station slits Y-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES0-SL:GAPY - onFailure: retry - enabled: true - softwareTrigger: false - -######################################################### -## Pinhole and alignment laser -- Physical Positioners ## -######################################################### - -es1pin_try: - readoutPriority: baseline - description: End Station pinhole and alignment laser Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:TRY - onFailure: retry - enabled: true - softwareTrigger: false -es1pin_trx: - readoutPriority: baseline - description: End Station pinhole and alignment laser X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:TRX - onFailure: retry - enabled: true - softwareTrigger: false -es1pin_rotx: - readoutPriority: baseline - description: End Station pinhole and alignment laser X-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:ROTX - onFailure: retry - enabled: true - softwareTrigger: false -es1pin_roty: - readoutPriority: baseline - description: End Station pinhole and alignment laser Y-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:ROTY - onFailure: retry - enabled: true - softwareTrigger: false - - -################################################ -## Sample Manipulator -- Physical Positioners ## -################################################ - -es1man_trx: - readoutPriority: baseline - description: End Station sample manipulator X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-MAN1:TRX - onFailure: retry - enabled: true - softwareTrigger: false -es1man_try: - readoutPriority: baseline - description: End Station sample manipulator Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-MAN1:TRY - onFailure: retry - enabled: true - softwareTrigger: false -es1man_trz: - readoutPriority: baseline - description: End Station sample manipulator Z-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-MAN1:TRZ - onFailure: retry - enabled: true - softwareTrigger: false -es1man_roty: - readoutPriority: baseline - description: End Station sample manipulator Y-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-MAN1:ROTY - onFailure: retry - enabled: true - softwareTrigger: false - -############################################ -## Segemented Arc -- Physical Positioners ## -############################################ - -es1arc_roty: - readoutPriority: baseline - description: End Station segmented arc Y-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-ARC:ROTY - onFailure: retry - enabled: true - softwareTrigger: false -es1det1_trx: - readoutPriority: baseline - description: End Station SDD 1 X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-DET1:TRX - onFailure: retry - enabled: true - softwareTrigger: false -es1bm1_trx: - readoutPriority: baseline - description: End Station X-ray Eye X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-BM1:TRX - onFailure: retry - enabled: true - softwareTrigger: false -es1det2_trx: - readoutPriority: baseline - description: End Station SDD 2 X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-DET2:TRX - onFailure: retry - enabled: true - softwareTrigger: false - -####################################### -## Beam Stop -- Physical Positioners ## -####################################### - -es2bs_trx: - readoutPriority: baseline - description: End Station beamstop X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES2-BS:TRX - onFailure: retry - enabled: true - softwareTrigger: false -es2bs_try: - readoutPriority: baseline - description: End Station beamstop Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES2-BS:TRY - onFailure: retry - enabled: true - softwareTrigger: false - -############################################## -## IC12 Manipulator -- Physical Positioners ## -############################################## - -es2ma2_try: - readoutPriority: baseline - description: End Station ionization chamber 1+2 Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES2-MA2:TRY - onFailure: retry - enabled: true - softwareTrigger: false -es2ma2_trz: - readoutPriority: baseline - description: End Station ionization chamber 1+2 Z-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES2-MA2:TRZ - onFailure: retry - enabled: true - softwareTrigger: false - -####################################################### -## XRD Detector Manipulator -- Physical Positioners ## -####################################################### - -es2ma3_try: - readoutPriority: baseline - description: End Station XRD detector Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES2-MA3:TRY - onFailure: retry - enabled: true - softwareTrigger: false diff --git a/debye_bec/device_configs/x01da_experimental_hutch.yaml b/debye_bec/device_configs/x01da_experimental_hutch.yaml new file mode 100644 index 0000000..f14d1aa --- /dev/null +++ b/debye_bec/device_configs/x01da_experimental_hutch.yaml @@ -0,0 +1,389 @@ +################################### +## Optical Table ## +################################### + +ot_tryu: + readoutPriority: baseline + description: Optical Table Y-Translation Upstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES-OT:TRYU + onFailure: retry + enabled: true + softwareTrigger: false + +ot_tryd: + readoutPriority: baseline + description: Optical Table Y-Translation Downstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES-OT:TRYD + onFailure: retry + enabled: true + softwareTrigger: false + +ot_es1_trz: + readoutPriority: baseline + description: Optical Table ES1 Z-Translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-OT:TRZ + onFailure: retry + enabled: true + softwareTrigger: false + +ot_es2_trz: + readoutPriority: baseline + description: Optical Table ES2 Z-Translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-OT:TRZ + onFailure: retry + enabled: true + softwareTrigger: false + +ot_try: + readoutPriority: baseline + description: Optical Table Y-Translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES-OT:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +ot_pitch: + readoutPriority: baseline + description: Optical Table Pitch + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES-OT:ROTX + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Exit Window ## +################################### + +es0wi_try: + readoutPriority: baseline + description: End Station 0 Exit Window Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-WI:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## ES0 Filter ## +################################### + +es0filter: + readoutPriority: baseline + description: ES0 filter station + deviceClass: debye_bec.devices.es0filter.ES0Filter + deviceConfig: + prefix: "X01DA-ES0-FI:" + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Slits ES0 ## +################################### + +es0sl_trxr: + readoutPriority: baseline + description: End Station slits X-translation Ring-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:TRXR + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_trxw: + readoutPriority: baseline + description: End Station slits X-translation Wall-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:TRXW + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_tryb: + readoutPriority: baseline + description: End Station slits Y-translation Bottom-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:TRYB + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_tryt: + readoutPriority: baseline + description: End Station slits X-translation Top-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:TRYT + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_center: + readoutPriority: baseline + description: End Station slits X-center + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:CENTERX + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_gapx: + readoutPriority: baseline + description: End Station slits X-gap + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:GAPX + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_centery: + readoutPriority: baseline + description: End Station slits Y-center + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:CENTERY + onFailure: retry + enabled: true + softwareTrigger: false + +es0sl_gapy: + readoutPriority: baseline + description: End Station slits Y-gap + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES0-SL:GAPY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Alignment Laser ## +################################### + +es1_alignment_laser: + readoutPriority: baseline + description: ES1 alignment laser + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: "X01DA-ES1-LAS:Relay" + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Sample Manipulator ## +################################### + +es1man_trx: + readoutPriority: baseline + description: End Station sample manipulator X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-MAN1:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +es1man_try: + readoutPriority: baseline + description: End Station sample manipulator Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-MAN1:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +es1man_trz: + readoutPriority: baseline + description: End Station sample manipulator Z-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-MAN1:TRZ + onFailure: retry + enabled: true + softwareTrigger: false + +es1man_roty: + readoutPriority: baseline + description: End Station sample manipulator Y-rotation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-MAN1:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Segmented Arc ## +################################### + +es1arc_roty: + readoutPriority: baseline + description: End Station segmented arc Y-rotation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-ARC:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + +es1det1_trx: + readoutPriority: baseline + description: End Station SDD 1 X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-DET1:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +es1bm1_trx: + readoutPriority: baseline + description: End Station X-ray Eye X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-BM1:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +es1det2_trx: + readoutPriority: baseline + description: End Station SDD 2 X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-DET2:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## IC1 + IC2 Manipulator ## +################################### + +es2ma2_try: + readoutPriority: baseline + description: End Station ionization chamber 1+2 Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-MA2:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +es2ma2_trz: + readoutPriority: baseline + description: End Station ionization chamber 1+2 Z-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-MA2:TRZ + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## XRD Detector Manipulator ## +################################### + +es2ma3_try: + readoutPriority: baseline + description: End Station XRD detector Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-MA3:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Hutch Env. Sensors + Light ## +################################### + +es_temperature1: + readoutPriority: baseline + description: ES temperature sensor 1 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH1:TEMP" + onFailure: retry + enabled: true + softwareTrigger: false + +es_humidity1: + readoutPriority: baseline + description: ES humidity sensor 1 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH1:HUMIREL" + onFailure: retry + enabled: true + softwareTrigger: false + +es_pressure1: + readoutPriority: baseline + description: ES ambient pressure sensor 1 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH1:PRES" + onFailure: retry + enabled: true + softwareTrigger: false + +es_temperature2: + readoutPriority: baseline + description: ES temperature sensor 2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH2:TEMP" + onFailure: retry + enabled: true + softwareTrigger: false + +es_humidity2: + readoutPriority: baseline + description: ES humidity sensor 2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH2:HUMIREL" + onFailure: retry + enabled: true + softwareTrigger: false + +es_pressure2: + readoutPriority: baseline + description: ES ambient pressure sensor 2 + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-PC-I2C:_CH2:PRES" + onFailure: retry + enabled: true + softwareTrigger: false + +es_light_toggle: + readoutPriority: baseline + description: ES light toggle + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: "X01DA-EH-LIGHT:TOGGLE" + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_frontend.yaml b/debye_bec/device_configs/x01da_frontend.yaml new file mode 100644 index 0000000..bc097a3 --- /dev/null +++ b/debye_bec/device_configs/x01da_frontend.yaml @@ -0,0 +1,218 @@ + +################################### +## Frontend Slits ## +################################### + +sldi_trxr: + readoutPriority: baseline + description: Front-end slit diaphragm X-translation Ring-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:TRXR + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_trxw: + readoutPriority: baseline + description: Front-end slit diaphragm X-translation Wall-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:TRXW + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_tryb: + readoutPriority: baseline + description: Front-end slit diaphragm Y-translation Bottom-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:TRYB + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_tryt: + readoutPriority: baseline + description: Front-end slit diaphragm X-translation Top-edge + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:TRYT + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_centerx: + readoutPriority: baseline + description: Front-end slit diaphragm X-center + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:CENTERX + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_gapx: + readoutPriority: baseline + description: Front-end slit diaphragm X-gap + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:GAPX + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_centery: + readoutPriority: baseline + description: Front-end slit diaphragm Y-center + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:CENTERY + onFailure: retry + enabled: true + softwareTrigger: false + +sldi_gapy: + readoutPriority: baseline + description: Front-end slit diaphragm Y-gap + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-SLDI:GAPY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Collimating Mirror ## +################################### + +cm_trxu: + readoutPriority: baseline + description: Collimating Mirror X-translation upstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:TRXU + onFailure: retry + enabled: true + softwareTrigger: false + +cm_trxd: + readoutPriority: baseline + description: Collimating Mirror X-translation downstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:TRXD + onFailure: retry + enabled: true + softwareTrigger: false + +cm_tryu: + readoutPriority: baseline + description: Collimating Mirror Y-translation upstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:TRYU + onFailure: retry + enabled: true + softwareTrigger: false + +cm_trydr: + readoutPriority: baseline + description: Collimating Mirror Y-translation downstream ring + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:TRYDR + onFailure: retry + enabled: true + softwareTrigger: false + +cm_trydw: + readoutPriority: baseline + description: Collimating Mirror Y-translation downstream wall + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:TRYDW + onFailure: retry + enabled: true + softwareTrigger: false + +cm_bnd: + readoutPriority: baseline + description: Collimating Mirror bender + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:BND + onFailure: retry + enabled: true + softwareTrigger: false + +cm_rotx: + readoutPriority: baseline + description: Collimating Morror Pitch + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:ROTX + onFailure: retry + enabled: true + softwareTrigger: false + +cm_roty: + readoutPriority: baseline + description: Collimating Morror Yaw + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + +cm_rotz: + readoutPriority: baseline + description: Collimating Morror Roll + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:ROTZ + onFailure: retry + enabled: true + softwareTrigger: false + +cm_trx: + readoutPriority: baseline + description: Collimating Morror Center Point X + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:XTCP + onFailure: retry + enabled: true + softwareTrigger: false + +cm_try: + readoutPriority: baseline + description: Collimating Morror Center Point Y + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:YTCP + onFailure: retry + enabled: true + softwareTrigger: false + +cm_ztcp: + readoutPriority: baseline + description: Collimating Morror Center Point Z + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:ZTCP + onFailure: retry + enabled: true + softwareTrigger: false + +cm_xstripe: + readoutPriority: baseline + description: Collimating Morror X Stripe + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-FE-CM:XSTRIPE + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_machine.yaml b/debye_bec/device_configs/x01da_machine.yaml index fe1ae5d..cd957dd 100644 --- a/debye_bec/device_configs/x01da_machine.yaml +++ b/debye_bec/device_configs/x01da_machine.yaml @@ -1,3 +1,8 @@ + +################################### +## SLS Machine ## +################################### + curr: readoutPriority: baseline description: SLS ring current diff --git a/debye_bec/device_configs/x01da_optic_slits.yaml b/debye_bec/device_configs/x01da_optics.yaml similarity index 55% rename from debye_bec/device_configs/x01da_optic_slits.yaml rename to debye_bec/device_configs/x01da_optics.yaml index 0b364f7..6d77341 100644 --- a/debye_bec/device_configs/x01da_optic_slits.yaml +++ b/debye_bec/device_configs/x01da_optics.yaml @@ -1,4 +1,41 @@ -## Optics Slits 1 -- Physical positioners + +################################### +## Monochromator ## +################################### + +mo_try: + readoutPriority: baseline + description: Monochromator Y Translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-MO1:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +mo_trx: + readoutPriority: baseline + description: Monochromator X Translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-MO1:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +mo_roty: + readoutPriority: baseline + description: Monochromator Yaw + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-MO1:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Optics Slits + Beam Monitor 1 ## +################################### sl1_trxr: readoutPriority: baseline @@ -12,6 +49,7 @@ sl1_trxr: deviceTags: - optics - slits + sl1_trxw: readoutPriority: baseline description: Optics slits 1 X-translation Wall-edge @@ -24,6 +62,7 @@ sl1_trxw: deviceTags: - optics - slits + sl1_tryb: readoutPriority: baseline description: Optics slits 1 Y-translation Bottom-edge @@ -36,6 +75,7 @@ sl1_tryb: deviceTags: - optics - slits + sl1_tryt: readoutPriority: baseline description: Optics slits 1 X-translation Top-edge @@ -48,6 +88,7 @@ sl1_tryt: deviceTags: - optics - slits + bm1_try: readoutPriority: baseline description: Beam Monitor 1 Y-translation @@ -61,8 +102,6 @@ bm1_try: - optics - slits -## Optics Slits 1 -- Virtual positioners - sl1_centerx: readoutPriority: baseline description: Optics slits 1 X-center @@ -75,6 +114,7 @@ sl1_centerx: deviceTags: - optics - slits + sl1_gapx: readoutPriority: baseline description: Optics slits 1 X-gap @@ -87,6 +127,7 @@ sl1_gapx: deviceTags: - optics - slits + sl1_centery: readoutPriority: baseline description: Optics slits 1 Y-center @@ -99,6 +140,7 @@ sl1_centery: deviceTags: - optics - slits + sl1_gapy: readoutPriority: baseline description: Optics slits 1 Y-gap @@ -112,7 +154,128 @@ sl1_gapy: - optics - slits -## Optics Slits 2 -- Physical positioners +################################### +## Focusing Mirror ## +################################### + +fm_trxu: + readoutPriority: baseline + description: Focusing Mirror X-translation upstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:TRXU + onFailure: retry + enabled: true + softwareTrigger: false +fm_trxd: + readoutPriority: baseline + description: Focusing Mirror X-translation downstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:TRXD + onFailure: retry + enabled: true + softwareTrigger: false +fm_tryd: + readoutPriority: baseline + description: Focusing Mirror Y-translation downstream + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:TRYD + onFailure: retry + enabled: true + softwareTrigger: false +fm_tryur: + readoutPriority: baseline + description: Focusing Mirror Y-translation upstream ring + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:TRYUR + onFailure: retry + enabled: true + softwareTrigger: false +fm_tryuw: + readoutPriority: baseline + description: Focusing Mirror Y-translation upstream wall + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:TRYUW + onFailure: retry + enabled: true + softwareTrigger: false +fm_bnd: + readoutPriority: baseline + description: Focusing Mirror bender + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:BND + onFailure: retry + enabled: true + softwareTrigger: false + +fm_rotx: + readoutPriority: baseline + description: Focusing Morror Pitch + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:ROTX + onFailure: retry + enabled: true + softwareTrigger: false + +fm_roty: + readoutPriority: baseline + description: Focusing Morror Yaw + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + +fm_rotz: + readoutPriority: baseline + description: Focusing Morror Roll + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:ROTZ + onFailure: retry + enabled: true + softwareTrigger: false + +fm_xctp: + readoutPriority: baseline + description: Focusing Morror Center Point X + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:XTCP + onFailure: retry + enabled: true + softwareTrigger: false + +fm_ytcp: + readoutPriority: baseline + description: Focusing Morror Center Point Y + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:YTCP + onFailure: retry + enabled: true + softwareTrigger: false + +fm_ztcp: + readoutPriority: baseline + description: Focusing Morror Center Point Z + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-OP-FM:ZTCP + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Optics Slits + Beam Monitor 2 ## +################################### sl2_trxr: readoutPriority: baseline @@ -126,6 +289,7 @@ sl2_trxr: deviceTags: - optics - slits + sl2_trxw: readoutPriority: baseline description: Optics slits 2 X-translation Wall-edge @@ -138,6 +302,7 @@ sl2_trxw: deviceTags: - optics - slits + sl2_tryb: readoutPriority: baseline description: Optics slits 2 Y-translation Bottom-edge @@ -150,6 +315,7 @@ sl2_tryb: deviceTags: - optics - slits + sl2_tryt: readoutPriority: baseline description: Optics slits 2 X-translation Top-edge @@ -162,6 +328,7 @@ sl2_tryt: deviceTags: - optics - slits + bm2_try: readoutPriority: baseline description: Beam Monitor 2 Y-translation @@ -175,8 +342,6 @@ bm2_try: - optics - slits -## Optics Slits 2 -- Virtual positioners - sl2_centerx: readoutPriority: baseline description: Optics slits 2 X-center @@ -189,6 +354,7 @@ sl2_centerx: deviceTags: - optics - slits + sl2_gapx: readoutPriority: baseline description: Optics slits 2 X-gap @@ -201,6 +367,7 @@ sl2_gapx: deviceTags: - optics - slits + sl2_centery: readoutPriority: baseline description: Optics slits 2 Y-center @@ -213,6 +380,7 @@ sl2_centery: deviceTags: - optics - slits + sl2_gapy: readoutPriority: baseline description: Optics slits 2 Y-gap diff --git a/debye_bec/device_configs/x01da_pilatus.yaml b/debye_bec/device_configs/x01da_pilatus.yaml deleted file mode 100644 index 33986f3..0000000 --- a/debye_bec/device_configs/x01da_pilatus.yaml +++ /dev/null @@ -1,34 +0,0 @@ -pilatus: - readoutPriority: async - description: Pilatus - deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus - deviceTags: - - detector - deviceConfig: - prefix: "X01DA-ES2-PIL:" - onFailure: retry - enabled: true - softwareTrigger: true -samx: - readoutPriority: baseline - deviceClass: ophyd_devices.SimPositioner - deviceConfig: - delay: 1 - limits: - - -50 - - 50 - tolerance: 0.01 - update_frequency: 400 - deviceTags: - - user motors - enabled: true - readOnly: false -bpm4i: - readoutPriority: monitored - deviceClass: ophyd_devices.SimMonitor - deviceConfig: - deviceTags: - - beamline - enabled: true - readOnly: false - diff --git a/debye_bec/device_configs/x01da_standard_config.yaml b/debye_bec/device_configs/x01da_standard_config.yaml new file mode 100644 index 0000000..5a0a701 --- /dev/null +++ b/debye_bec/device_configs/x01da_standard_config.yaml @@ -0,0 +1,75 @@ + +################################### +## General ## +################################### + +## SLS Machine +machine_config: + - !include ./x01da_machine.yaml + +## Beam Monitors OP + EH +beam_monitors_config: + - !include ./x01da_beam_monitors.yaml + +################################### +## Frontend ## +################################### + +## Frontend +frontend_config: + - !include ./x01da_frontend.yaml + +################################### +## Optics Hutch ## +################################### + +## Bragg Monochromator +mo1_bragg: + readoutPriority: monitored + description: Positioner for the Monochromator + deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg + deviceConfig: + prefix: "X01DA-OP-MO1:BRAGG:" + onFailure: retry + enabled: true + softwareTrigger: false +mo1_bragg_angle: + readoutPriority: baseline + description: Positioner for the Monochromator + deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg_angle.Mo1BraggAngle + deviceConfig: + prefix: "X01DA-OP-MO1:BRAGG:" + onFailure: retry + enabled: true + softwareTrigger: false + +## Remaining optics hutch +optics_config: + - !include ./x01da_optics.yaml + +################################### +## Experimental Hutch ## +################################### + +## NIDAQ +nidaq: + readoutPriority: monitored + description: NIDAQ backend for data reading for debye scans + deviceClass: debye_bec.devices.nidaq.nidaq.Nidaq + deviceConfig: + prefix: "X01DA-PC-SCANSERVER:" + onFailure: retry + enabled: true + softwareTrigger: false + +## XAS (ICx, SDD, ref foils) +xas_config: + - !include ./x01da_xas.yaml + +## XRD (Pilatus, pinhole, beamstop) +xrd_config: + - !include ./x01da_xrd.yaml + +## Remaining experimental hutch +es_config: + - !include ./x01da_experimental_hutch.yaml \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_test_config.yaml b/debye_bec/device_configs/x01da_test_config.yaml deleted file mode 100644 index 25c3fa1..0000000 --- a/debye_bec/device_configs/x01da_test_config.yaml +++ /dev/null @@ -1,636 +0,0 @@ -optic_slit_config: - - !include ./x01da_optic_slits.yaml -machine_config: - - !include ./x01da_machine.yaml -## Slit Diaphragm -- Physical positioners -sldi_trxr: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Ring-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRXR - onFailure: retry - enabled: true - softwareTrigger: false -sldi_trxw: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Wall-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRXW - onFailure: retry - enabled: true - softwareTrigger: false -sldi_tryb: - readoutPriority: baseline - description: Front-end slit diaphragm Y-translation Bottom-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRYB - onFailure: retry - enabled: true - softwareTrigger: false -sldi_tryt: - readoutPriority: baseline - description: Front-end slit diaphragm X-translation Top-edge - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:TRYT - onFailure: retry - enabled: true - softwareTrigger: false - -## Slit Diaphragm -- Virtual positioners - -sldi_centerx: - readoutPriority: baseline - description: Front-end slit diaphragm X-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:CENTERX - onFailure: retry - enabled: true - softwareTrigger: false -sldi_gapx: - readoutPriority: baseline - description: Front-end slit diaphragm X-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:GAPX - onFailure: retry - enabled: true - softwareTrigger: false -sldi_centery: - readoutPriority: baseline - description: Front-end slit diaphragm Y-center - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:CENTERY - onFailure: retry - enabled: true - softwareTrigger: false -sldi_gapy: - readoutPriority: baseline - description: Front-end slit diaphragm Y-gap - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-SLDI:GAPY - onFailure: retry - enabled: true - softwareTrigger: false - - -## Collimating Mirror -- Physical Positioners - -cm_trxu: - readoutPriority: baseline - description: Collimating Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRXU - onFailure: retry - enabled: true - softwareTrigger: false -cm_trxd: - readoutPriority: baseline - description: Collimating Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRXD - onFailure: retry - enabled: true - softwareTrigger: false -cm_tryu: - readoutPriority: baseline - description: Collimating Mirror Y-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYU - onFailure: retry - enabled: true - softwareTrigger: false -cm_trydr: - readoutPriority: baseline - description: Collimating Mirror Y-translation downstream ring - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYDR - onFailure: retry - enabled: true - softwareTrigger: false -cm_trydw: - readoutPriority: baseline - description: Collimating Mirror Y-translation downstream wall - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:TRYDW - onFailure: retry - enabled: true - softwareTrigger: false -cm_bnd: - readoutPriority: baseline - description: Collimating Mirror bender - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:BND - onFailure: retry - enabled: true - softwareTrigger: false - -## Collimating Mirror -- Virtual Positioners - -cm_rotx: - readoutPriority: baseline - description: Collimating Morror Pitch - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTX - onFailure: retry - enabled: true - softwareTrigger: false -cm_roty: - readoutPriority: baseline - description: Collimating Morror Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTY - onFailure: retry - enabled: true - softwareTrigger: false -cm_rotz: - readoutPriority: baseline - description: Collimating Morror Roll - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ROTZ - onFailure: retry - enabled: true - softwareTrigger: false -cm_trx: - readoutPriority: baseline - description: Collimating Morror Center Point X - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:XTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_try: - readoutPriority: baseline - description: Collimating Morror Center Point Y - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:YTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_ztcp: - readoutPriority: baseline - description: Collimating Morror Center Point Z - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:ZTCP - onFailure: retry - enabled: true - softwareTrigger: false -cm_xstripe: - readoutPriority: baseline - description: Collimating Morror X Stripe - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-FE-CM:XSTRIPE - onFailure: retry - enabled: true - softwareTrigger: false - -## Bragg Monochromator -mo1_bragg: - readoutPriority: baseline - description: Positioner for the Monochromator - deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg - deviceConfig: - prefix: "X01DA-OP-MO1:BRAGG:" - onFailure: retry - enabled: true - softwareTrigger: false -mo1_bragg_angle: - readoutPriority: baseline - description: Positioner for the Monochromator - deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg_angle.Mo1BraggAngle - deviceConfig: - prefix: "X01DA-OP-MO1:BRAGG:" - onFailure: retry - enabled: true - softwareTrigger: false - -# NIDAQ -nidaq: - readoutPriority: monitored - description: NIDAQ backend for data reading for debye scans - deviceClass: debye_bec.devices.nidaq.nidaq.Nidaq - deviceConfig: - prefix: "X01DA-PC-SCANSERVER:" - onFailure: retry - enabled: true - softwareTrigger: false - -## Monochromator -- Physical Positioners - -mo_try: - readoutPriority: baseline - description: Monochromator Y Translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:TRY - onFailure: retry - enabled: true - softwareTrigger: false -mo_trx: - readoutPriority: baseline - description: Monochromator X Translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:TRX - onFailure: retry - enabled: true - softwareTrigger: false -mo_roty: - readoutPriority: baseline - description: Monochromator Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-MO1:ROTY - onFailure: retry - enabled: true - softwareTrigger: false - - ## Focusing Mirror -- Physical Positioners - -fm_trxu: - readoutPriority: baseline - description: Focusing Mirror X-translation upstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRXU - onFailure: retry - enabled: true - softwareTrigger: false -fm_trxd: - readoutPriority: baseline - description: Focusing Mirror X-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRXD - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryd: - readoutPriority: baseline - description: Focusing Mirror Y-translation downstream - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYD - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryur: - readoutPriority: baseline - description: Focusing Mirror Y-translation upstream ring - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYUR - onFailure: retry - enabled: true - softwareTrigger: false -fm_tryuw: - readoutPriority: baseline - description: Focusing Mirror Y-translation upstream wall - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:TRYUW - onFailure: retry - enabled: true - softwareTrigger: false -fm_bnd: - readoutPriority: baseline - description: Focusing Mirror bender - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:BND - onFailure: retry - enabled: true - softwareTrigger: false - -## Focusing Mirror -- Virtual Positioners - -fm_rotx: - readoutPriority: baseline - description: Focusing Morror Pitch - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTX - onFailure: retry - enabled: true - softwareTrigger: false -fm_roty: - readoutPriority: baseline - description: Focusing Morror Yaw - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTY - onFailure: retry - enabled: true - softwareTrigger: false -fm_rotz: - readoutPriority: baseline - description: Focusing Morror Roll - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ROTZ - onFailure: retry - enabled: true - softwareTrigger: false -fm_xctp: - readoutPriority: baseline - description: Focusing Morror Center Point X - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:XTCP - onFailure: retry - enabled: true - softwareTrigger: false -fm_ytcp: - readoutPriority: baseline - description: Focusing Morror Center Point Y - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:YTCP - onFailure: retry - enabled: true - softwareTrigger: false -fm_ztcp: - readoutPriority: baseline - description: Focusing Morror Center Point Z - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-OP-FM:ZTCP - onFailure: retry - enabled: true - softwareTrigger: false - -# Ionization Chambers - -ic0: - readoutPriority: baseline - description: Ionization chamber 0 - deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 - deviceConfig: - prefix: "X01DA-" - onFailure: retry - enabled: true - softwareTrigger: false -ic1: - readoutPriority: baseline - description: Ionization chamber 1 - deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 - deviceConfig: - prefix: "X01DA-" - onFailure: retry - enabled: true - softwareTrigger: false -ic2: - readoutPriority: baseline - description: Ionization chamber 2 - deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 - deviceConfig: - prefix: "X01DA-" - onFailure: retry - enabled: true - softwareTrigger: false - -# ES0 Filter - -es0filter: - readoutPriority: baseline - description: ES0 filter station - deviceClass: debye_bec.devices.es0filter.ES0Filter - deviceConfig: - prefix: "X01DA-ES0-FI:" - onFailure: retry - enabled: true - softwareTrigger: false - -# Reference foil changer - -reffoilchanger: - readoutPriority: baseline - description: ES2 reference foil changer - deviceClass: debye_bec.devices.reffoilchanger.Reffoilchanger - deviceConfig: - prefix: "X01DA-" - onFailure: retry - enabled: true - softwareTrigger: false - -# Beam Monitors - -# beam_monitor_1: -# readoutPriority: async -# description: Beam monitor 1 -# deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam -# deviceConfig: -# prefix: "X01DA-OP-GIGE01:" -# onFailure: retry -# enabled: true -# softwareTrigger: false - -# beam_monitor_2: -# readoutPriority: async -# description: Beam monitor 2 -# deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam -# deviceConfig: -# prefix: "X01DA-OP-GIGE02:" -# onFailure: retry -# enabled: true -# softwareTrigger: false - -xray_eye: - readoutPriority: async - description: X-ray eye - deviceClass: debye_bec.devices.cameras.basler_cam.BaslerCam - deviceConfig: - prefix: "X01DA-ES-XRAYEYE:" - onFailure: retry - enabled: true - softwareTrigger: false - -# Pilatus Curtain -# pilatus_curtain: -# readoutPriority: baseline -# description: Pilatus Curtain -# deviceClass: debye_bec.devices.pilatus_curtain.PilatusCurtain -# deviceConfig: -# prefix: "X01DA-ES2-DET3:TRY-" -# onFailure: retry -# enabled: true -# softwareTrigger: false - - -################################ -## ES Hutch Sensors and Light ## -################################ - -es_temperature1: - readoutPriority: baseline - description: ES temperature sensor 1 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH1:TEMP" - onFailure: retry - enabled: true - softwareTrigger: false - -es_humidity1: - readoutPriority: baseline - description: ES humidity sensor 1 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH1:HUMIREL" - onFailure: retry - enabled: true - softwareTrigger: false - -es_pressure1: - readoutPriority: baseline - description: ES ambient pressure sensor 1 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH1:PRES" - onFailure: retry - enabled: true - softwareTrigger: false - -es_temperature2: - readoutPriority: baseline - description: ES temperature sensor 2 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH2:TEMP" - onFailure: retry - enabled: true - softwareTrigger: false - -es_humidity2: - readoutPriority: baseline - description: ES humidity sensor 2 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH2:HUMIREL" - onFailure: retry - enabled: true - softwareTrigger: false - -es_pressure2: - readoutPriority: baseline - description: ES ambient pressure sensor 2 - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-PC-I2C:_CH2:PRES" - onFailure: retry - enabled: true - softwareTrigger: false - -es_light_toggle: - readoutPriority: baseline - description: ES light toggle - deviceClass: ophyd.EpicsSignal - deviceConfig: - read_pv: "X01DA-EH-LIGHT:TOGGLE" - onFailure: retry - enabled: true - softwareTrigger: false - -################# -## SDD sensors ## -################# - -sdd1_temperature: - readoutPriority: baseline - description: SDD1 temperature sensor - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-ES1-DET1:Temperature" - onFailure: retry - enabled: true - softwareTrigger: false - -sdd1_humidity: - readoutPriority: baseline - description: SDD1 humidity sensor - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X01DA-ES1-DET1:Humidity" - kind: "config" - onFailure: retry - enabled: true - softwareTrigger: false - -##################### -## Alignment Laser ## -##################### - -es1_alignment_laser: - readoutPriority: baseline - description: ES1 alignment laser - deviceClass: ophyd.EpicsSignal - deviceConfig: - read_pv: "X01DA-ES1-LAS:Relay" - onFailure: retry - enabled: true - softwareTrigger: false - -## Pinhole alignment stages -- Physical Positioners - -pin1_trx: - readoutPriority: baseline - description: Pinhole X-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:TRX - onFailure: retry - enabled: true - softwareTrigger: false - tags: Endstation - -pin1_try: - readoutPriority: baseline - description: Pinhole Y-translation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:TRY - onFailure: retry - enabled: true - softwareTrigger: false - tags: Endstation - -pin1_rotx: - readoutPriority: baseline - description: Pinhole X-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:ROTX - onFailure: retry - enabled: true - softwareTrigger: false - tags: Endstation - -pin1_roty: - readoutPriority: baseline - description: Pinhole Y-rotation - deviceClass: ophyd.EpicsMotor - deviceConfig: - prefix: X01DA-ES1-PIN1:ROTY - onFailure: retry - enabled: true - softwareTrigger: false - tags: Endstation \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_xas.yaml b/debye_bec/device_configs/x01da_xas.yaml new file mode 100644 index 0000000..42e2876 --- /dev/null +++ b/debye_bec/device_configs/x01da_xas.yaml @@ -0,0 +1,73 @@ + +################################### +## Ionization Chambers ## +################################### + +# ic0: +# readoutPriority: baseline +# description: Ionization chamber 0 +# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 +# deviceConfig: +# prefix: "X01DA-" +# onFailure: retry +# enabled: true +# softwareTrigger: false + +# ic1: +# readoutPriority: baseline +# description: Ionization chamber 1 +# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 +# deviceConfig: +# prefix: "X01DA-" +# onFailure: retry +# enabled: true +# softwareTrigger: false + +# ic2: +# readoutPriority: baseline +# description: Ionization chamber 2 +# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 +# deviceConfig: +# prefix: "X01DA-" +# onFailure: retry +# enabled: true +# softwareTrigger: false + +################################### +## Reference Foil Changer ## +################################### + +reffoilchanger: + readoutPriority: baseline + description: ES2 reference foil changer + deviceClass: debye_bec.devices.reffoilchanger.Reffoilchanger + deviceConfig: + prefix: "X01DA-" + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## SDD Sensors ## +################################### + +sdd1_temperature: + readoutPriority: baseline + description: SDD1 temperature sensor + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-ES1-DET1:Temperature" + onFailure: retry + enabled: true + softwareTrigger: false + +sdd1_humidity: + readoutPriority: baseline + description: SDD1 humidity sensor + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: "X01DA-ES1-DET1:Humidity" + kind: "config" + onFailure: retry + enabled: true + softwareTrigger: false \ No newline at end of file diff --git a/debye_bec/device_configs/x01da_xrd.yaml b/debye_bec/device_configs/x01da_xrd.yaml new file mode 100644 index 0000000..22ffdba --- /dev/null +++ b/debye_bec/device_configs/x01da_xrd.yaml @@ -0,0 +1,108 @@ + +################################### +## Pinhole ## +################################### + +pin1_trx: + readoutPriority: baseline + description: Pinhole X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-PIN1:TRX + onFailure: retry + enabled: true + softwareTrigger: false + tags: Endstation + +pin1_try: + readoutPriority: baseline + description: Pinhole Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-PIN1:TRY + onFailure: retry + enabled: true + softwareTrigger: false + tags: Endstation + +pin1_rotx: + readoutPriority: baseline + description: Pinhole X-rotation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-PIN1:ROTX + onFailure: retry + enabled: true + softwareTrigger: false + tags: Endstation + +pin1_roty: + readoutPriority: baseline + description: Pinhole Y-rotation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES1-PIN1:ROTY + onFailure: retry + enabled: true + softwareTrigger: false + tags: Endstation + +################################### +## Beam Stop ## +################################### + +es2bs_trx: + readoutPriority: baseline + description: End Station beamstop X-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-BS:TRX + onFailure: retry + enabled: true + softwareTrigger: false + +es2bs_try: + readoutPriority: baseline + description: End Station beamstop Y-translation + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: X01DA-ES2-BS:TRY + onFailure: retry + enabled: true + softwareTrigger: false + +################################### +## Pilatus ## +################################### + +pilatus_curtain: + readoutPriority: baseline + description: Pilatus Curtain + deviceClass: debye_bec.devices.pilatus_curtain.PilatusCurtain + deviceConfig: + prefix: "X01DA-ES2-DET3:TRY-" + onFailure: retry + enabled: true + softwareTrigger: false + +pilatus: + readoutPriority: async + description: Pilatus + deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus + deviceTags: + - detector + deviceConfig: + prefix: "X01DA-ES2-PIL:" + onFailure: retry + enabled: true + softwareTrigger: true + +# sampl_pil: +# readoutPriority: baseline +# description: Sample to pilatus distance +# deviceClass: ophyd.EpicsSignalRO +# deviceConfig: +# read_pv: "X01DA-SAMPL-PIL" +# onFailure: retry +# enabled: true +# softwareTrigger: false -- 2.49.1 From a67394a9a2f097c2c6d6665140af60c2ea175870 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 08:39:17 +0200 Subject: [PATCH 12/21] refactoring --- .../ionization_chambers/ionization_chamber.py | 98 ++++-------- debye_bec/devices/pilatus_curtain.py | 148 ++++++++++++------ 2 files changed, 131 insertions(+), 115 deletions(-) diff --git a/debye_bec/devices/ionization_chambers/ionization_chamber.py b/debye_bec/devices/ionization_chambers/ionization_chamber.py index 2b7bfd6..deb970e 100644 --- a/debye_bec/devices/ionization_chambers/ionization_chamber.py +++ b/debye_bec/devices/ionization_chambers/ionization_chamber.py @@ -1,15 +1,19 @@ +"""Ionization chamber device class""" + from __future__ import annotations from typing import TYPE_CHECKING, Literal +from typeguard import typechecked import numpy as np from ophyd import Component as Cpt from ophyd import Device from ophyd import DynamicDeviceComponent as Dcpt -from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Kind +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV from ophyd.status import DeviceStatus, SubscriptionStatus +from ophyd_devices import CompareStatus, TransitionStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase -from typeguard import typechecked + from debye_bec.devices.ionization_chambers.ionization_chamber_enums import ( AmplifierEnable, @@ -110,18 +114,10 @@ class IonizationChamber0(PSIDeviceBase): """ if self.amp.cOnOff.get() == AmplifierEnable.OFF: + status = CompareStatus(self.amp.cOnOff, AmplifierEnable.ON) + self.cancel_on_stop(status) self.amp.cOnOff.put(AmplifierEnable.ON) - - # Wait until channel is switched on - def _wait_enabled(): - return self.amp.cOnOff.get() == AmplifierEnable.ON - - if not self.wait_for_condition( - _wait_enabled, check_stopped=True, timeout=self.timeout_for_pvwait - ): - raise TimeoutError( - f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds" - ) + status.wait(self.timeout_for_pvwait) match gain: case "1e6": @@ -144,21 +140,13 @@ class IonizationChamber0(PSIDeviceBase): """Configure the filter setting of the specified channel Args: - value (Literal['1us', '3us', '10us', '30us', '100us', '300us', '1ms', '3ms']) : Desired filter + value (Literal['1us','3us','10us','30us','100us','300us','1ms','3ms']) :Desired filter """ if self.amp.cOnOff.get() == AmplifierEnable.OFF: + status = CompareStatus(self.amp.cOnOff, AmplifierEnable.ON) + self.cancel_on_stop(status) self.amp.cOnOff.put(AmplifierEnable.ON) - - # Wait until channel is switched on - def _wait_enabled(): - return self.amp.cOnOff.get() == AmplifierEnable.ON - - if not self.wait_for_condition( - _wait_enabled, check_stopped=True, timeout=self.timeout_for_pvwait - ): - raise TimeoutError( - f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds" - ) + status.wait(self.timeout_for_pvwait) match value: case "1us": @@ -187,20 +175,16 @@ class IonizationChamber0(PSIDeviceBase): hv (float) : Desired voltage for the 'HV' terminal. Voltage has to be between 0...3000 """ - if not (0 <= hv <= 3000): + if not 0 <= hv <= 3000: raise ValueError(f"specified HV {hv} not within range [0 .. 3000]") if not np.isclose(np.abs(hv - self.hv.grid_v.get()), 0, atol=3): raise ValueError(f"Grid {self.hv.grid_v.get()} must not be higher than HV {hv}!") if not self.hv_en.ena.get() == 1: - - def check_ch_ena(*, old_value, value, **kwargs): - return value == 1 - - status = SubscriptionStatus(device=self.hv_en.ena, callback=check_ch_ena) + status = CompareStatus(self.hv_en.ena, 1) + self.cancel_on_stop(status) self.hv_en.ena.put(1) - # Wait after setting ena to 1 - status.wait(timeout=2) + status.wait(self.timeout_for_pvwait) # Set current fixed to 3 mA (max) self.hv.hv_i.put(3) @@ -212,23 +196,20 @@ class IonizationChamber0(PSIDeviceBase): enable the high voltage (if external enable is active)! Args: - grid (float) : Desired voltage for the 'Grid' terminal, Grid Voltage has to be between 0...3000 + grid (float) : Desired voltage for the 'Grid' terminal, + Grid Voltage has to be between 0...3000 """ - if not (0 <= grid <= 3000): + if not 0 <= grid <= 3000: raise ValueError(f"specified Grid {grid} not within range [0 .. 3000]") if not np.isclose(np.abs(grid - self.hv.hv_v.get()), 0, atol=3): raise ValueError(f"Grid {grid} must not be higher than HV {self.hv.hv_v.get()}!") if not self.hv_en.ena.get() == 1: - - def check_ch_ena(*, old_value, value, **kwargs): - return value == 1 - - status = SubscriptionStatus(device=self.hv_en.ena, callback=check_ch_ena) + status = CompareStatus(self.hv_en.ena, 1) + self.cancel_on_stop(status) self.hv_en.ena.put(1) - # Wait after setting ena to 1 - status.wait(timeout=2) + status.wait(self.timeout_for_pvwait) # Set current fixed to 3 mA (max) self.hv.grid_i.put(3) @@ -244,7 +225,7 @@ class IonizationChamber0(PSIDeviceBase): pressure: float, *, wait: bool = False, - ) -> DeviceStatus: + ) -> DeviceStatus | None: """Fill an ionization chamber with the specified gas mixture. Args: @@ -256,13 +237,13 @@ class IonizationChamber0(PSIDeviceBase): wait (bool): If you like to wait for the filling to finish. """ - if not (0 <= conc1 <= 100): + if not 0 <= conc1 <= 100: raise ValueError(f"Concentration 1 {conc1} out of range [0 .. 100 %]") - if not (0 <= conc2 <= 100): + if not 0 <= conc2 <= 100: raise ValueError(f"Concentration 2 {conc2} out of range [0 .. 100 %]") if not np.isclose((conc1 + conc2), 100, atol=0.1): raise ValueError(f"Conc1 {conc1} and conc2 {conc2} must sum to 100 +- 0.1") - if not (0 <= pressure <= 3): + if not 0 <= pressure <= 3: raise ValueError(f"Pressure {pressure} out of range [0 .. 3 bar abs]") self.gmes.gas1_req.set(gas1).wait(timeout=3) @@ -270,28 +251,13 @@ class IonizationChamber0(PSIDeviceBase): self.gmes.gas2_req.set(gas2).wait(timeout=3) self.gmes.conc2_req.set(conc2).wait(timeout=3) + status = TransitionStatus(self.gmes.status.get(), [0, 1]) + self.cancel_on_stop(status) self.gmes.fill.put(1) - - def wait_for_status(): - return self.gmes.status.get() == 0 - - timeout = 3 - if not self.wait_for_condition(wait_for_status, timeout=timeout, check_stopped=True): - raise TimeoutError( - f"Ionization chamber filling process did not start after {timeout}s. Last log message {self.gmes_status.get()}" - ) - - def wait_for_filling_finished(): - return self.gmes.status.get() == 1 - - # Wait until ionization chamber is filled successfully - status = self.task_handler.submit_task( - task=self.wait_for_condition, task_args=(wait_for_filling_finished, 360, True) - ) if wait: - status.wait() - return status - + status.wait(timeout=360) + else: + return status class IonizationChamber1(IonizationChamber0): """Ionization Chamber 1, prefix should be 'X01DA-'.""" diff --git a/debye_bec/devices/pilatus_curtain.py b/debye_bec/devices/pilatus_curtain.py index 94c05b8..0cc5c0a 100644 --- a/debye_bec/devices/pilatus_curtain.py +++ b/debye_bec/devices/pilatus_curtain.py @@ -1,75 +1,125 @@ """ES2 Pilatus Curtain""" -import time +from __future__ import annotations +import enum +from typing import TYPE_CHECKING from ophyd import Component as Cpt -from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind -from ophyd_devices.utils import bec_utils +from ophyd import EpicsSignal, EpicsSignalRO +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from ophyd_devices import CompareStatus, AndStatusWithList, DeviceStatus +if TYPE_CHECKING: + from bec_lib.devicemanager import ScanInfo -class GasMixSetup(Device): +class PilatusCurtainError(Exception): + """PilatusCurtain specific exception""" + +class COVER(int, enum.Enum): + """Pilatus Curtain States""" + + OPEN = 0 + CLOSED = 0 + ERROR = 1 + +class PilatusCurtain(PSIDeviceBase): """Class for the ES2 Pilatus Curtain""" USER_ACCESS = ["open", "close"] - open_cover = Cpt(EpicsSignal, suffix="OpenCover", kind="config", doc="Open Cover") - close_cover = Cpt(EpicsSignal, suffix="CloseCover", kind="config", doc="Close Cover") + open_cover = Cpt( + EpicsSignal, + suffix="OpenCover", + kind="config", + doc="Open Cover" + ) + + close_cover = Cpt( + EpicsSignal, + suffix="CloseCover", + kind="config", + doc="Close Cover" + ) + cover_is_closed = Cpt( - EpicsSignalRO, suffix="CoverIsClosed", kind="config", doc="Cover is closed" + EpicsSignalRO, + suffix="CoverIsClosed", + kind="config", + doc="Cover is closed" ) - cover_is_open = Cpt(EpicsSignalRO, suffix="CoverIsOpen", kind="config", doc="Cover is open") + + cover_is_open = Cpt( + EpicsSignalRO, + suffix="CoverIsOpen", + kind="config", + doc="Cover is open" + ) + cover_is_moving = Cpt( - EpicsSignalRO, suffix="CoverIsMoving", kind="config", doc="Cover is moving" + EpicsSignalRO, + suffix="CoverIsMoving", + kind="config", + doc="Cover is moving" ) - cover_error = Cpt(EpicsSignalRO, suffix="CoverError", kind="config", doc="Cover error") - def __init__( - self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs - ): - """Initialize the Pilatus Curtain. + cover_error = Cpt( + EpicsSignalRO, + suffix="CoverError", + kind="config", + doc="Cover error" + ) - Args: - prefix (str): EPICS prefix for the device - name (str): Name of the device - kind (Kind): Kind of the device - device_manager (DeviceManager): Device manager instance - parent (Device): Parent device - kwargs: Additional keyword arguments - """ - super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs) - self.device_manager = device_manager - self.service_cfg = None + def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): + super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) self.timeout_for_pvwait = 30 - self.readback.name = self.name # Wait for connection on all components, ensure IOC is connected self.wait_for_connection(all_signals=True, timeout=5) - if device_manager: - self.device_manager = device_manager - else: - self.device_manager = bec_utils.DMMock() + def on_connected(self) -> None: + """ + Called after the device is connected and its signals are connected. + Default values for signals should be set here. + """ + if self.cover_error.get() == COVER.ERROR: + raise PilatusCurtainError('Pilatus Curtain is in an error state!') - self.connector = self.device_manager.connector + def on_stage(self) -> DeviceStatus | None: + """Called while staging the device.""" + return self.open() - def open(self) -> None: + def on_unstage(self) -> DeviceStatus | None: + """Called while unstaging the device.""" + return self.close() + + def on_stop(self) -> DeviceStatus | None: + """Called when the device is stopped.""" + return self.close() + + def open(self) -> DeviceStatus | None: """Open the cover""" + if self.cover_is_closed.get() == COVER.CLOSED: + self.open_cover.put(1) + status_open = CompareStatus(self.cover_is_open, COVER.OPEN, timeout=5) + status_error = CompareStatus(self.cover_error, COVER.ERROR, operation='!=') + status = AndStatusWithList( + device=self, status_list=[status_open, status_error] + ) + self.cancel_on_stop(status) + return status + else: + return None - self.open_cover.put(1) - - while not self.cover_is_open.get(): - time.sleep(0.1) - if self.cover_error.get(): - raise TimeoutError("Curtain did not open successfully and is now in an error state") - - def close(self) -> None: + def close(self) -> DeviceStatus | None: """Close the cover""" - - self.close_cover.put(1) - - while not self.cover_is_closed.get(): - time.sleep(0.1) - if self.cover_error.get(): - raise TimeoutError( - "Curtain did not close successfully and is now in an error state" - ) + if self.cover_is_open.get() == COVER.OPEN: + self.close_cover.put(1) + status_close = CompareStatus(self.cover_is_closed, COVER.CLOSED, timeout=5) + status_error = CompareStatus(self.cover_error, COVER.ERROR, operation='!=') + status = AndStatusWithList( + device=self, status_list=[status_close, status_error] + ) + self.cancel_on_stop(status) + return status + else: + return None -- 2.49.1 From 0a83b59af8a5fbced472d8d1a19f861b4ebb2e26 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 08:39:46 +0200 Subject: [PATCH 13/21] refacotring --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 2 +- debye_bec/devices/mo1_bragg/mo1_bragg_devices.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index ea163ba..fa6e15b 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -160,7 +160,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): ) self.set_trig_settings( enable_low=self.scan_parameter.break_enable_low, - enable_high=self.scan_parameter.break_enable_high, + enable_high=self.scan_parameter.break_enable_high, break_time_low=self.scan_parameter.break_time_low, break_time_high=self.scan_parameter.break_time_high, cycle_low=self.scan_parameter.cycle_low, diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 6a4fe1a..6bbddab 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -233,7 +233,7 @@ class Mo1BraggPositioner(Device, PositionerBase): setpoint = Cpt( EpicsSignalWithRBV, suffix="set_abs_pos_energy", kind="normal", auto_monitor=True ) - motor_is_moving = Cpt( + motor_mov_done = Cpt( EpicsSignalRO, suffix="move_abs_done_RBV", kind="normal", auto_monitor=True ) low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_energy_RBV", kind="config", auto_monitor=True) @@ -347,7 +347,7 @@ class Mo1BraggPositioner(Device, PositionerBase): # Currently sleep is needed due to delay in updates on PVs, maybe time can be reduced time.sleep(0.5) motor_name = self.name - while self.motor_is_moving.get() == 0: + while self.motor_mov_done.get() == 0: if self.stopped: raise Mo1BraggStoppedError(f"Device {self.name} was stopped") time.sleep(update_frequency) -- 2.49.1 From ed9148ed96aa6799b3959e98677b63ae5150e5b1 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 09:11:31 +0200 Subject: [PATCH 14/21] bugfix --- debye_bec/devices/pilatus_curtain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/debye_bec/devices/pilatus_curtain.py b/debye_bec/devices/pilatus_curtain.py index 0cc5c0a..6304800 100644 --- a/debye_bec/devices/pilatus_curtain.py +++ b/debye_bec/devices/pilatus_curtain.py @@ -105,7 +105,6 @@ class PilatusCurtain(PSIDeviceBase): status = AndStatusWithList( device=self, status_list=[status_open, status_error] ) - self.cancel_on_stop(status) return status else: return None @@ -119,7 +118,6 @@ class PilatusCurtain(PSIDeviceBase): status = AndStatusWithList( device=self, status_list=[status_close, status_error] ) - self.cancel_on_stop(status) return status else: return None -- 2.49.1 From 158175f5459bcadcc0d2bfb08a3554cc8ede7b51 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 09:11:48 +0200 Subject: [PATCH 15/21] add baseline signals (not workling yet) --- debye_bec/devices/pilatus/pilatus.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 5326417..1e08e05 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -214,6 +214,17 @@ class Pilatus(PSIDeviceBase, ADBase): ) file_event = Cpt(FileEventSignal, name="file_event") + @property + def baseline_signals(self): + """Define baseline signals""" + return[ + self.cam.acquire_time, + self.cam.num_exposures, + self.cam.threshold_energy, + self.cam.gain_menu, + self.cam.pixel_cut_off + ] + def __init__( self, *, -- 2.49.1 From ed759da14f4ee8b57841025be6fadc53653bbbd1 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 16 Sep 2025 13:08:24 +0200 Subject: [PATCH 16/21] bugfix for XAS only scans --- debye_bec/devices/pilatus/pilatus.py | 95 ++++++++++++++++------------ test_commit.yml | 0 test_commit_2.yml | 0 test_commit_3.yml | 0 test_commit_4 | 0 5 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 test_commit.yml create mode 100644 test_commit_2.yml create mode 100644 test_commit_3.yml create mode 100644 test_commit_4 diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 1e08e05..b62ca35 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -536,22 +536,27 @@ class Pilatus(PSIDeviceBase, ADBase): def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" - status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) - status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) - status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) - status = AndStatusWithList( - device=self, status_list=[status_hdf, status_cam, status_cam_server] - ) - self.cam.acquire.put(1) - self.hdf.capture.put(1) - self.cancel_on_stop(status) - return status + scan_msg: ScanStatusMessage = self.scan_info.msg + if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_cam_server] + ) + self.cam.acquire.put(1) + self.hdf.capture.put(1) + self.cancel_on_stop(status) + return status + else: + return None def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" - if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: + scan_msg: ScanStatusMessage = self.scan_info.msg + if scan_msg.scan_name in self.xas_xrd_scan_names: return None - else: + elif scan_msg.scan_type == 'step': start_time = time.time() logger.warning(f"Triggering image with num_captured {self.hdf.num_captured.get()}") img_counter = self.hdf.num_captured.get() @@ -560,42 +565,52 @@ class Pilatus(PSIDeviceBase, ADBase): self.trigger_shot.put(1) self.cancel_on_stop(status) return status + else: + return None def _complete_callback(self, status: DeviceStatus): """Callback for when the device completes a scan.""" - if status.success: - status.device.file_event.put( - file_path=status.device._full_path, # pylint: disable:protected-access - done=True, - successful=True, - hinted_h5_entries={"data": "/entry/data/data"}, - ) + scan_msg: ScanStatusMessage = self.scan_info.msg + if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + if status.success: + status.device.file_event.put( + file_path=status.device._full_path, # pylint: disable:protected-access + done=True, + successful=True, + hinted_h5_entries={"data": "/entry/data/data"}, + ) + else: + status.device.file_event.put( + file_path=status.device._full_path, # pylint: disable:protected-access + done=True, + successful=False, + hinted_h5_entries={"data": "/entry/data/data"}, + ) else: - status.device.file_event.put( - file_path=status.device._full_path, # pylint: disable:protected-access - done=True, - successful=False, - hinted_h5_entries={"data": "/entry/data/data"}, - ) + return None def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" - status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) - status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) - status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) - if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: - # For long scans, it can be that the mono will execute one cycle more, - # meaning a few more XRD triggers will be sent - status_img_written = CompareStatus(self.hdf.num_captured, self.n_images, operation='>=') - else: + scan_msg: ScanStatusMessage = self.scan_info.msg + if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) + if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: + # For long scans, it can be that the mono will execute one cycle more, + # meaning a few more XRD triggers will be sent + status_img_written = CompareStatus(self.hdf.num_captured, self.n_images, operation='>=') + else: + status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) - status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) - status = AndStatusWithList( - device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] - ) - status.add_callback(self._complete_callback) # Callback that writing was successful - self.cancel_on_stop(status) - return status + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] + ) + status.add_callback(self._complete_callback) # Callback that writing was successful + self.cancel_on_stop(status) + return status + else: + return None def on_kickoff(self) -> None: """Called to kickoff a device for a fly scan. Has to be called explicitly.""" diff --git a/test_commit.yml b/test_commit.yml new file mode 100644 index 0000000..e69de29 diff --git a/test_commit_2.yml b/test_commit_2.yml new file mode 100644 index 0000000..e69de29 diff --git a/test_commit_3.yml b/test_commit_3.yml new file mode 100644 index 0000000..e69de29 diff --git a/test_commit_4 b/test_commit_4 new file mode 100644 index 0000000..e69de29 -- 2.49.1 From 9d9a2e9681b8f2182c18ae5a9c3c745ec7d2f33c Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 16 Sep 2025 17:33:40 +0200 Subject: [PATCH 17/21] refactor(pilatus): Cleanupt PIlatus integration --- debye_bec/devices/pilatus/pilatus.py | 219 +++++++++++++----------- debye_bec/devices/pilatus/utils.py | 238 +++++++++++++++++++++++++++ 2 files changed, 360 insertions(+), 97 deletions(-) create mode 100644 debye_bec/devices/pilatus/utils.py diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index b62ca35..e12aac1 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -17,16 +17,12 @@ from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin from ophyd.status import WaitTimeoutError -from ophyd_devices import ( - AndStatusWithList, - CompareStatus, - DeviceStatus, - FileEventSignal, - PreviewSignal, -) +from ophyd_devices import CompareStatus, DeviceStatus, FileEventSignal, PreviewSignal from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from pydantic import BaseModel, Field +from debye_bec.devices.pilatus.utils import AndStatusWithList + if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage @@ -83,19 +79,20 @@ class TRIGGERMODE(int, enum.Enum): MULT_TRIGGER = 3 ALIGNMENT = 4 + class MONOTRIGGERSOURCE(int, enum.Enum): - """"Mono XRD trigger source""" + """ "Mono XRD trigger source""" EPICS = 0 INPOS = 1 + class MONOTRIGGERMODE(int, enum.Enum): - """"Mono XRD trigger mode""" + """ "Mono XRD trigger mode""" PULSE = 0 CONDITION = 1 - def description(self) -> str: """Return a description of the trigger mode.""" descriptions = { @@ -109,7 +106,6 @@ class MONOTRIGGERMODE(int, enum.Enum): def __str__(self): return self.description() - class ScanParameter(BaseModel): @@ -217,12 +213,12 @@ class Pilatus(PSIDeviceBase, ADBase): @property def baseline_signals(self): """Define baseline signals""" - return[ + return [ self.cam.acquire_time, self.cam.num_exposures, self.cam.threshold_energy, self.cam.gain_menu, - self.cam.pixel_cut_off + self.cam.pixel_cut_off, ] def __init__( @@ -246,10 +242,7 @@ class Pilatus(PSIDeviceBase, ADBase): ) self._poll_thread_kill_event = threading.Event() self._poll_rate = 1 # Poll rate in Hz - self.xas_xrd_scan_names = [ - "xas_simple_scan_with_xrd", - "xas_advanced_scan_with_xrd", - ] + self.xas_xrd_scan_names = ["xas_simple_scan_with_xrd", "xas_advanced_scan_with_xrd"] self.n_images = None # self._live_mode_thread = threading.Thread( # target=self._live_mode_loop, daemon=True, name=f"{self.name}_live_mode_thread" @@ -377,6 +370,69 @@ class Pilatus(PSIDeviceBase, ADBase): ) return status + def _calculate_trigger(self, scan_msg: ScanStatusMessage): + self._update_scan_parameter() + total_osc = 0 + total_trig_lo = 0 + total_trig_hi = 0 + calc_duration = 0 + n_trig_lo = 1 + n_trig_hi = 1 + init_lo = 1 + init_hi = 1 + lo_done = 0 + hi_done = 0 + if not self.scan_parameter.break_enable_low: + lo_done = 1 + if not self.scan_parameter.break_enable_high: + hi_done = 1 + start_time = time.time() + while True: + # TODO, we should not use infinite loops, for now let's add the escape Timeout of 20s, but should eventually be reviewed. + if time.time() - start_time > 20: + raise RuntimeError( + f"Calculating the number of triggers for scan {scan_msg.scan_name} took more than 20 seconds, aborting." + ) + total_osc = total_osc + 2 + calc_duration = calc_duration + 2 * self.scan_parameter.scan_time + + if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low: + n_trig_lo = 1 + calc_duration = calc_duration + self.scan_parameter.break_time_low + if init_lo: + lo_done = 1 + init_lo = 0 + else: + n_trig_lo += 1 + + if ( + self.scan_parameter.break_enable_high + and n_trig_hi >= self.scan_parameter.cycle_high + ): + n_trig_hi = 1 + calc_duration = calc_duration + self.scan_parameter.break_time_high + if init_hi: + hi_done = 1 + init_hi = 0 + else: + n_trig_hi += 1 + + if lo_done and hi_done: + n = np.floor(self.scan_parameter.scan_duration / calc_duration) + total_osc = total_osc * n + if self.scan_parameter.break_enable_low: + total_trig_lo = n + 1 + if self.scan_parameter.break_enable_high: + total_trig_hi = n + 1 + calc_duration = calc_duration * n + lo_done = 0 + hi_done = 0 + + if calc_duration >= self.scan_parameter.scan_duration: + break + + return total_trig_lo + total_trig_hi + ######################################## # Beamline Specific Implementations # ######################################## @@ -427,84 +483,45 @@ class Pilatus(PSIDeviceBase, ADBase): (self.scan_info.msg) object. """ # self.stop_live_mode() # Make sure that live mode is stopped if scan runs - self._update_scan_parameter() + scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name in self.xas_xrd_scan_names: - total_osc = 0 - total_trig_lo = 0 - total_trig_hi = 0 - calc_duration = 0 - n_trig_lo = 1 - n_trig_hi = 1 - init_lo = 1 - init_hi = 1 - lo_done = 0 - hi_done = 0 - if not self.scan_parameter.break_enable_low: - lo_done = 1 - if not self.scan_parameter.break_enable_high: - hi_done = 1 - while True: - total_osc = total_osc + 2 - calc_duration = calc_duration + 2 * self.scan_parameter.scan_time - - if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low: - n_trig_lo = 1 - calc_duration = calc_duration + self.scan_parameter.break_time_low - if init_lo: - lo_done = 1 - init_lo = 0 - else: - n_trig_lo += 1 - - if self.scan_parameter.break_enable_high and n_trig_hi >= self.scan_parameter.cycle_high: - n_trig_hi = 1 - calc_duration = calc_duration + self.scan_parameter.break_time_high - if init_hi: - hi_done = 1 - init_hi = 0 - else: - n_trig_hi += 1 - - if lo_done and hi_done: - n = np.floor(self.scan_parameter.scan_duration / calc_duration) - total_osc = total_osc * n - if self.scan_parameter.break_enable_low: - total_trig_lo = n + 1 - if self.scan_parameter.break_enable_high: - total_trig_hi = n + 1 - calc_duration = calc_duration * n - lo_done = 0 - hi_done = 0 - - if calc_duration >= self.scan_parameter.scan_duration: - break - - # logger.info(f'total_osc: {total_osc}') - # logger.info(f'total trig low: {total_trig_lo}') - # logger.info(f'total trig high: {total_trig_hi}') - + self._update_scan_parameter() + # Compute number of triggers + total_trig_lo, total_trig_hi = self._calculate_trigger(scan_msg) + # Set the number of images, we may also set this to a higher values if preferred and stop the acquisition + # TODO This logic is prone to errors, as we rely on the scans to nicely resolve to n_images. We should + # use here instead a way of settings the n_images independently of the scan parameters to avoid running out of sync + # with the complete method. Ideally we comput them in the scan itself.. This is much safer IMO! self.n_images = (total_trig_lo + total_trig_hi) * self.scan_parameter.n_of_trigger exp_time = self.scan_parameter.exp_time self.trigger_source.set(MONOTRIGGERSOURCE.INPOS).wait(5) self.trigger_n_of.set(self.scan_parameter.n_of_trigger).wait(5) - elif scan_msg.scan_type == 'step': - self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get("frames_per_trigger", 1) + elif scan_msg.scan_type == "step": + self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get( + "frames_per_trigger", 1 + ) exp_time = scan_msg.scan_parameters.get("exp_time") self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5) - self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition + self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition else: + # TODO how to deal with fly scans? return None # Common settings self.trigger_mode.set(MONOTRIGGERMODE.PULSE).wait(5) self.trigger_period.set(exp_time).wait(5) - self.trigger_pulse_length.set(0.005).wait(5) # Pulse length of 5 ms enough for Pilatus and NIDAQ + self.trigger_pulse_length.set(0.005).wait( + 5 + ) # Pulse length of 5 ms enough for Pilatus and NIDAQ if exp_time - self._readout_time <= 0: - raise ValueError((f"Exposure time {exp_time} is too short ", - f"for Pilatus with readout_time {self._readout_time}." - )) + raise ValueError( + ( + f"Exposure time {exp_time} is too short ", + f"for Pilatus with readout_time {self._readout_time}.", + ) + ) detector_exp_time = exp_time - self._readout_time self._full_path = get_full_path(scan_msg, name="pilatus") file_path = "/".join(self._full_path.split("/")[:-1]) @@ -519,7 +536,9 @@ class Pilatus(PSIDeviceBase, ADBase): self.cam.acquire_period.set(exp_time).wait(5) self.filter_number.set(0).wait(5) # HDF5 settings - logger.debug(f"Setting HDF5 file path to {file_path} and file name to {file_name}. full_path is {self._full_path}") + logger.debug( + f"Setting HDF5 file path to {file_path} and file name to {file_name}. full_path is {self._full_path}" + ) self.hdf.file_path.set(file_path).wait(5) self.hdf.file_name.set(file_name).wait(5) self.hdf.num_capture.set(self.n_images).wait(5) @@ -537,7 +556,9 @@ class Pilatus(PSIDeviceBase, ADBase): def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + if ( + scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value) @@ -554,24 +575,23 @@ class Pilatus(PSIDeviceBase, ADBase): def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name in self.xas_xrd_scan_names: - return None - elif scan_msg.scan_type == 'step': - start_time = time.time() - logger.warning(f"Triggering image with num_captured {self.hdf.num_captured.get()}") - img_counter = self.hdf.num_captured.get() - status = CompareStatus(self.hdf.num_captured, img_counter + 1) - logger.warning(f"Triggering took image {time.time() - start_time:.3f} seconds") - self.trigger_shot.put(1) - self.cancel_on_stop(status) - return status - else: + if not scan_msg.scan_type == "step": return None + start_time = time.time() + img_counter = self.hdf.num_captured.get() + logger.debug(f"Triggering image with num_captured {img_counter}") + status = CompareStatus(self.hdf.num_captured, img_counter + 1) + logger.debug(f"Triggering took image {time.time() - start_time:.3f} seconds") + self.trigger_shot.put(1) + self.cancel_on_stop(status) + return status def _complete_callback(self, status: DeviceStatus): """Callback for when the device completes a scan.""" scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + if ( + scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + ): # TODO how to deal with fly scans? if status.success: status.device.file_event.put( file_path=status.device._full_path, # pylint: disable:protected-access @@ -592,19 +612,24 @@ class Pilatus(PSIDeviceBase, ADBase): def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == 'step': + if ( + scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: # For long scans, it can be that the mono will execute one cycle more, # meaning a few more XRD triggers will be sent - status_img_written = CompareStatus(self.hdf.num_captured, self.n_images, operation='>=') + status_img_written = CompareStatus( + self.hdf.num_captured, self.n_images, operation=">=" + ) else: status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) status = AndStatusWithList( - device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] + device=self, + status_list=[status_hdf, status_cam, status_img_written, status_cam_server], ) status.add_callback(self._complete_callback) # Callback that writing was successful self.cancel_on_stop(status) diff --git a/debye_bec/devices/pilatus/utils.py b/debye_bec/devices/pilatus/utils.py new file mode 100644 index 0000000..d3f3de3 --- /dev/null +++ b/debye_bec/devices/pilatus/utils.py @@ -0,0 +1,238 @@ +"""Temporary utility module for Status Object implementations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ophyd import Device, DeviceStatus, StatusBase + + +class AndStatusWithList(DeviceStatus): + """ + Custom implementation of the AndStatus that combines the + option to add multiple statuses as a list, and in addition + allows for adding the Device as an object to access its + methods. + + Args""" + + def __init__( + self, + device: Device, + status_list: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus], + **kwargs, + ): + self.all_statuses = status_list if isinstance(status_list, list) else [status_list] + super().__init__(device=device, **kwargs) + self._trace_attributes["all"] = [st._trace_attributes for st in self.all_statuses] + + def inner(status): + with self._lock: + if self._externally_initiated_completion: + return + if self.done: # Return if status is already done.. It must be resolved already + return + + for st in self.all_statuses: + with st._lock: + if st.done and not st.success: + self.set_exception(st.exception()) # st._exception + return + + if all(st.done for st in self.all_statuses) and all( + st.success for st in self.all_statuses + ): + self.set_finished() + + for st in self.all_statuses: + with st._lock: + st.add_callback(inner) + + # TODO improve __repr__ and __str__ + def __repr__(self): + return "".format(self=self) + + def __str__(self): + return "".format(self=self) + + def __contains__(self, status: StatusBase | DeviceStatus) -> bool: + for child in self.all_statuses: + if child == status: + return True + if isinstance(child, AndStatusWithList): + if status in child: + return True + + return False + + # TODO Check if this actually works.... + def set_exception(self, exc): + super().set_exception(exc) + # Propagate the exception to all sub-statuses that are not done yet. + with self._lock: + for st in self.all_statuses: + with st._lock: + if not st.done: + st.set_exception(exc) + + def _run_callbacks(self): + """ + Set the Event and run the callbacks. + """ + if self.timeout is None: + timeout = None + else: + timeout = self.timeout + self.settle_time + if not self._settled_event.wait(timeout): + self.log.warning("%r has timed out", self) + with self._externally_initiated_completion_lock: + if self._exception is None: + exc = TimeoutError( + f"AndStatus from device {self.device.name} failed to complete in specified timeout of {self.timeout + self.settle_time}." + ) + self._exception = exc + # Mark this as "settled". + try: + self._settled() + except Exception: + self.log.exception("%r encountered error during _settled()", self) + with self._lock: + self._event.set() + if self._exception is not None: + try: + self._handle_failure() + except Exception: + self.log.exception("%r encountered an error during _handle_failure()", self) + for cb in self._callbacks: + try: + cb(self) + except Exception: + self.log.exception( + "An error was raised on a background thread while " + "running the callback %r(%r).", + cb, + self, + ) + self._callbacks.clear() + + +class AndStatus(StatusBase): + """Custom AndStatus for TimePix detector.""" + + def __init__( + self, + left: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus] | None, + name: str | Device | None = None, + right: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus] | None = None, + **kwargs, + ): + self.left = left if isinstance(left, list) else [left] + if right is not None: + self.right = right if isinstance(right, list) else [right] + else: + self.right = [] + self.all_statuses = self.left + self.right + if name is None: + name = "unname_status" + elif isinstance(name, Device): + name = name.name + else: + name = name + self.name = name + super().__init__(**kwargs) + self._trace_attributes["left"] = [st._trace_attributes for st in self.left] + self._trace_attributes["right"] = [st._trace_attributes for st in self.right] + + def inner(status): + with self._lock: + if self._externally_initiated_completion: + return + if self.done: # Return if status is already done.. It must be resolved already + return + + for st in self.all_statuses: + with st._lock: + if st.done and not st.success: + self.set_exception(st.exception()) # st._exception + return + + if all(st.done for st in self.all_statuses) and all( + st.success for st in self.all_statuses + ): + self.set_finished() + + for st in self.all_statuses: + with st._lock: + st.add_callback(inner) + + def __repr__(self): + return "({self.left!r} & {self.right!r})".format(self=self) + + def __str__(self): + return "{0}(done={1.done}, " "success={1.success})" "".format(self.__class__.__name__, self) + + def __contains__(self, status: StatusBase) -> bool: + for child in [self.left, self.right]: + if child == status: + return True + if isinstance(child, AndStatus): + if status in child: + return True + + return False + + def _run_callbacks(self): + """ + Set the Event and run the callbacks. + """ + if self.timeout is None: + timeout = None + else: + timeout = self.timeout + self.settle_time + if not self._settled_event.wait(timeout): + # We have timed out. It's possible that set_finished() has already + # been called but we got here before the settle_time timer expired. + # And it's possible that in this space be between the above + # statement timing out grabbing the lock just below, + # set_exception(exc) has been called. Both of these possibilties + # are accounted for. + self.log.warning("%r has timed out", self) + with self._externally_initiated_completion_lock: + # Set the exception and mark the Status as done, unless + # set_exception(exc) was called externally before we grabbed + # the lock. + if self._exception is None: + exc = TimeoutError( + f"Status with name {self.name} failed to complete in specified timeout of {self.timeout + self.settle_time}." + ) + self._exception = exc + # Mark this as "settled". + try: + self._settled() + except Exception: + # No alternative but to log this. We can't supersede set_exception, + # and we have to continue and run the callbacks. + self.log.exception("%r encountered error during _settled()", self) + # Now we know whether or not we have succeed or failed, either by + # timeout above or by set_exception(exc), so we can set the Event that + # will mark this Status as done. + with self._lock: + self._event.set() + if self._exception is not None: + try: + self._handle_failure() + except Exception: + self.log.exception("%r encountered an error during _handle_failure()", self) + # The callbacks have access to self, from which they can distinguish + # success or failure. + for cb in self._callbacks: + try: + cb(self) + except Exception: + self.log.exception( + "An error was raised on a background thread while " + "running the callback %r(%r).", + cb, + self, + ) + self._callbacks.clear() -- 2.49.1 From 9b739c852d8e505470eaf9f88c6fa68d088700eb Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 16 Sep 2025 17:34:51 +0200 Subject: [PATCH 18/21] fix(mo1-bragg-devices): revert signal name change, motor_is_moving --- debye_bec/devices/mo1_bragg/mo1_bragg_devices.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 6bbddab..6a4fe1a 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -233,7 +233,7 @@ class Mo1BraggPositioner(Device, PositionerBase): setpoint = Cpt( EpicsSignalWithRBV, suffix="set_abs_pos_energy", kind="normal", auto_monitor=True ) - motor_mov_done = Cpt( + motor_is_moving = Cpt( EpicsSignalRO, suffix="move_abs_done_RBV", kind="normal", auto_monitor=True ) low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_energy_RBV", kind="config", auto_monitor=True) @@ -347,7 +347,7 @@ class Mo1BraggPositioner(Device, PositionerBase): # Currently sleep is needed due to delay in updates on PVs, maybe time can be reduced time.sleep(0.5) motor_name = self.name - while self.motor_mov_done.get() == 0: + while self.motor_is_moving.get() == 0: if self.stopped: raise Mo1BraggStoppedError(f"Device {self.name} was stopped") time.sleep(update_frequency) -- 2.49.1 From 1d6caa22918cd1bbc3696dbb0df507f2ed6d3773 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 16 Sep 2025 17:35:27 +0200 Subject: [PATCH 19/21] refactor(pilatus-curtain): cleanup pilatus curtain --- debye_bec/devices/pilatus_curtain.py | 63 ++++++++++------------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/debye_bec/devices/pilatus_curtain.py b/debye_bec/devices/pilatus_curtain.py index 6304800..4f8ab4e 100644 --- a/debye_bec/devices/pilatus_curtain.py +++ b/debye_bec/devices/pilatus_curtain.py @@ -1,73 +1,54 @@ """ES2 Pilatus Curtain""" from __future__ import annotations + import enum from typing import TYPE_CHECKING from ophyd import Component as Cpt from ophyd import EpicsSignal, EpicsSignalRO +from ophyd_devices import CompareStatus, DeviceStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase -from ophyd_devices import CompareStatus, AndStatusWithList, DeviceStatus + +from debye_bec.devices.pilatus.utils import AndStatusWithList if TYPE_CHECKING: from bec_lib.devicemanager import ScanInfo + class PilatusCurtainError(Exception): """PilatusCurtain specific exception""" + class COVER(int, enum.Enum): """Pilatus Curtain States""" + # TODO What are the proper states here? - Probably enums for the states are better. OPEN = 0 CLOSED = 0 ERROR = 1 + class PilatusCurtain(PSIDeviceBase): """Class for the ES2 Pilatus Curtain""" USER_ACCESS = ["open", "close"] - open_cover = Cpt( - EpicsSignal, - suffix="OpenCover", - kind="config", - doc="Open Cover" - ) + open_cover = Cpt(EpicsSignal, suffix="OpenCover", kind="config", doc="Open Cover") - close_cover = Cpt( - EpicsSignal, - suffix="CloseCover", - kind="config", - doc="Close Cover" - ) + close_cover = Cpt(EpicsSignal, suffix="CloseCover", kind="config", doc="Close Cover") cover_is_closed = Cpt( - EpicsSignalRO, - suffix="CoverIsClosed", - kind="config", - doc="Cover is closed" + EpicsSignalRO, suffix="CoverIsClosed", kind="config", doc="Cover is closed" ) - cover_is_open = Cpt( - EpicsSignalRO, - suffix="CoverIsOpen", - kind="config", - doc="Cover is open" - ) + cover_is_open = Cpt(EpicsSignalRO, suffix="CoverIsOpen", kind="config", doc="Cover is open") cover_is_moving = Cpt( - EpicsSignalRO, - suffix="CoverIsMoving", - kind="config", - doc="Cover is moving" + EpicsSignalRO, suffix="CoverIsMoving", kind="config", doc="Cover is moving" ) - cover_error = Cpt( - EpicsSignalRO, - suffix="CoverError", - kind="config", - doc="Cover error" - ) + cover_error = Cpt(EpicsSignalRO, suffix="CoverError", kind="config", doc="Cover error") def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) @@ -82,7 +63,7 @@ class PilatusCurtain(PSIDeviceBase): Default values for signals should be set here. """ if self.cover_error.get() == COVER.ERROR: - raise PilatusCurtainError('Pilatus Curtain is in an error state!') + raise PilatusCurtainError("Pilatus Curtain is in an error state!") def on_stage(self) -> DeviceStatus | None: """Called while staging the device.""" @@ -100,11 +81,10 @@ class PilatusCurtain(PSIDeviceBase): """Open the cover""" if self.cover_is_closed.get() == COVER.CLOSED: self.open_cover.put(1) + # TODO timeout ok? status_open = CompareStatus(self.cover_is_open, COVER.OPEN, timeout=5) - status_error = CompareStatus(self.cover_error, COVER.ERROR, operation='!=') - status = AndStatusWithList( - device=self, status_list=[status_open, status_error] - ) + status_error = CompareStatus(self.cover_error, COVER.ERROR, operation="!=") + status = AndStatusWithList(device=self, status_list=[status_open, status_error]) return status else: return None @@ -113,11 +93,10 @@ class PilatusCurtain(PSIDeviceBase): """Close the cover""" if self.cover_is_open.get() == COVER.OPEN: self.close_cover.put(1) + # TODO timeout ok? status_close = CompareStatus(self.cover_is_closed, COVER.CLOSED, timeout=5) - status_error = CompareStatus(self.cover_error, COVER.ERROR, operation='!=') - status = AndStatusWithList( - device=self, status_list=[status_close, status_error] - ) + status_error = CompareStatus(self.cover_error, COVER.ERROR, operation="!=") + status = AndStatusWithList(device=self, status_list=[status_close, status_error]) return status else: return None -- 2.49.1 From 190eae2c3f95db5934730d1816ce7187f1ff5e6e Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 16 Sep 2025 17:36:43 +0200 Subject: [PATCH 20/21] fix: formatting --- .../ionization_chambers/ionization_chamber.py | 4 ++-- debye_bec/devices/mo1_bragg/mo1_bragg.py | 9 +++------ debye_bec/devices/nidaq/nidaq.py | 12 +++--------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/debye_bec/devices/ionization_chambers/ionization_chamber.py b/debye_bec/devices/ionization_chambers/ionization_chamber.py index deb970e..276f9b3 100644 --- a/debye_bec/devices/ionization_chambers/ionization_chamber.py +++ b/debye_bec/devices/ionization_chambers/ionization_chamber.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal -from typeguard import typechecked import numpy as np from ophyd import Component as Cpt from ophyd import Device @@ -13,7 +12,7 @@ from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV from ophyd.status import DeviceStatus, SubscriptionStatus from ophyd_devices import CompareStatus, TransitionStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase - +from typeguard import typechecked from debye_bec.devices.ionization_chambers.ionization_chamber_enums import ( AmplifierEnable, @@ -259,6 +258,7 @@ class IonizationChamber0(PSIDeviceBase): else: return status + class IonizationChamber1(IonizationChamber0): """Ionization Chamber 1, prefix should be 'X01DA-'.""" diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index fa6e15b..7d37e9e 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -223,7 +223,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.SUCCESS) self.cancel_on_stop(status) self.scan_control.scan_load.put(1) - # Wait for params to be checked from controller + # Wait for params to be checked from controller status.wait(self.timeout_for_pvwait) return None @@ -321,7 +321,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status_list.append(self.scan_settings.s_scan_scantime.set(scan_time)) self.cancel_on_stop(status_list[-1]) - + for s in status_list: s.wait(timeout=self.timeout_for_pvwait) @@ -352,7 +352,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status = CompareStatus(self.calculator.calc_done, 1) self.cancel_on_stop(status) status.wait(self.timeout_for_pvwait) - time.sleep(0.25) #TODO needed still? Needed due to update frequency of softIOC + time.sleep(0.25) # TODO needed still? Needed due to update frequency of softIOC if mode == "AngleToEnergy": return self.calculator.calc_energy.get() elif mode == "EnergyToAngle": @@ -407,7 +407,6 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): cycle_high: int, exp_time: float, n_of_trigger: int, - ) -> None: """Set TRIG settings for the upcoming scan. @@ -451,7 +450,6 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): for s in status_list: s.wait(timeout=self.timeout_for_pvwait) - def set_scan_control_settings(self, mode: ScanControlMode, scan_duration: float) -> None: """Set the scan control settings for the upcoming scan. @@ -472,7 +470,6 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): for s in status_list: s.wait(timeout=self.timeout_for_pvwait) - def _update_scan_parameter(self): """Get the scan_info parameters for the scan.""" for key, value in self.scan_info.msg.request_inputs["inputs"].items(): diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 4b2a792..3b7bb93 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -302,17 +302,11 @@ class NidaqControl(Device): SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD" ) - xas_timestamp = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp" - ) + xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp") - xrd_timestamp = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp" - ) + xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp") - xrd_energy = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy" - ) + xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy") di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") -- 2.49.1 From c70088e7bc6fac65147afe0cab83b31d63404646 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 17 Sep 2025 07:37:44 +0200 Subject: [PATCH 21/21] tests: fix test cases after refactoring --- tests/tests_devices/test_mo1_bragg.py | 8 +++-- tests/tests_devices/test_pilatus.py | 34 +++++++++++++++------- tests/tests_scans/test_mono_bragg_scans.py | 25 ++++++++-------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/tests/tests_devices/test_mo1_bragg.py b/tests/tests_devices/test_mo1_bragg.py index ac7066b..9dc2e31 100644 --- a/tests/tests_devices/test_mo1_bragg.py +++ b/tests/tests_devices/test_mo1_bragg.py @@ -132,10 +132,12 @@ def test_set_trig_settings(mock_bragg): dev.set_trig_settings( enable_low=True, enable_high=False, - exp_time_high=0.1, - exp_time_low=0.01, + break_time_high=0.1, + break_time_low=0.01, cycle_low=1, cycle_high=3, + exp_time=0.5, + n_of_trigger=7, ) assert dev.scan_settings.trig_ena_lo_enum.get() == True assert dev.scan_settings.trig_ena_hi_enum.get() == False @@ -143,6 +145,8 @@ def test_set_trig_settings(mock_bragg): assert dev.scan_settings.trig_every_n_hi.get() == 3 assert dev.scan_settings.trig_time_lo.get() == 0.01 assert dev.scan_settings.trig_time_hi.get() == 0.1 + assert dev.trigger_settings.xrd_trig_period.get() == 0.5 + assert dev.trigger_settings.xrd_n_of_trig.get() == 7 def test_set_control_settings(mock_bragg): diff --git a/tests/tests_devices/test_pilatus.py b/tests/tests_devices/test_pilatus.py index 75c7c69..1ad3347 100644 --- a/tests/tests_devices/test_pilatus.py +++ b/tests/tests_devices/test_pilatus.py @@ -35,13 +35,18 @@ if TYPE_CHECKING: # pragma no cover @pytest.fixture( scope="function", - params=[(0.1, 1, 1, "line_scan"), (0.2, 2, 2, "time_scan"), (0.5, 5, 5, "xas_advanced_scan")], + params=[ + (0.1, 1, 1, "line_scan", "step"), + (0.2, 2, 2, "time_scan", "step"), + (0.5, 5, 5, "xas_advanced_scan", "fly"), + ], ) def mock_scan_info(request, tmpdir): - exp_time, frames_per_trigger, num_points, scan_name = request.param + exp_time, frames_per_trigger, num_points, scan_name, scan_type = request.param scan_info = ScanStatusMessage( scan_id="test_id", status="open", + scan_type=scan_type, scan_number=1, scan_parameters={ "exp_time": exp_time, @@ -71,7 +76,10 @@ def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]: try: yield dev finally: - dev.destroy() + try: + dev.on_destroy() + except ophyd.utils.DestroyedError: + pass # TODO figure out how to test as set calls on the PV below seem to break it.. @@ -102,7 +110,7 @@ def test_pilatus_on_destroy(pilatus): with mock.patch.object(pilatus, "on_stop") as mock_on_stop: pilatus.destroy() assert mock_on_stop.call_count == 1 - assert pilatus._poll_thread_stop_event.is_set() + assert pilatus._poll_thread_kill_event.is_set() def test_pilatus_on_failure_callback(pilatus): @@ -119,7 +127,8 @@ def test_pilatus_on_failure_callback(pilatus): def test_pilatus_on_pre_scan(pilatus): """Test the on_pre_scan logic of the Pilatus detector.""" - if pilatus.scan_info.msg.scan_name.startswith("xas"): + scan_msg = pilatus.scan_info.msg + if scan_msg.scan_type != "step" and scan_msg.scan_name not in pilatus.xas_xrd_scan_names: assert pilatus.on_pre_scan() is None return pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.DONE.value @@ -135,7 +144,8 @@ def test_pilatus_on_pre_scan(pilatus): def test_pilatus_on_trigger(pilatus): """test on trigger logic of the Pilatus detector.""" - if pilatus.scan_info.msg.scan_name.startswith("xas"): + scan_msg = pilatus.scan_info.msg + if scan_msg.scan_type != "step" and scan_msg.scan_name not in pilatus.xas_xrd_scan_names: status = pilatus.trigger() assert status.done is True assert status.success is True @@ -169,10 +179,12 @@ def test_pilatus_on_trigger_cancel_on_stop(pilatus): def test_pilatus_on_complete(pilatus): """Test the on_complete logic of the Pilatus detector.""" + if pilatus.scan_info.msg.scan_name.startswith("xas"): - status = pilatus.complete() - assert status.done is True - assert status.success is True + # TODO add test cases for xas scans + # status = pilatus.complete() + # assert status.done is True + # assert status.success is True return # Check in addition that the file event is set properly, once with if it works, and once if not (i.e. when cancelled) for success in [True, False]: @@ -191,6 +203,7 @@ def test_pilatus_on_complete(pilatus): ) pilatus.hdf.num_captured._read_pv.mock_data = num_images - 1 # Call on complete + pilatus.n_images = num_images status = pilatus.complete() # Should not be finished assert status.done is False @@ -265,7 +278,8 @@ def test_pilatus_on_complete(pilatus): def test_pilatus_on_stage_raises_low_exp_time(pilatus): """Test that on_stage raises a ValueError if the exposure time is too low.""" pilatus.scan_info.msg.scan_parameters["exp_time"] = 0.09 - if pilatus.scan_info.msg.scan_name.startswith("xas"): + scan_msg = pilatus.scan_info.msg + if scan_msg.scan_type != "step" and scan_msg.scan_name not in pilatus.xas_xrd_scan_names: return with pytest.raises(ValueError): pilatus.on_stage() diff --git a/tests/tests_scans/test_mono_bragg_scans.py b/tests/tests_scans/test_mono_bragg_scans.py index 52dec76..e0bafd6 100644 --- a/tests/tests_scans/test_mono_bragg_scans.py +++ b/tests/tests_scans/test_mono_bragg_scans.py @@ -141,17 +141,18 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock): stop=5, scan_time=1, scan_duration=10, - xrd_enable_low=True, - num_trigger_low=1, - exp_time_low=1, + break_enable_low=True, + break_time_low=1, cycle_low=1, - xrd_enable_high=True, - num_trigger_high=2, - exp_time_high=3, + break_enable_high=True, + break_time_high=2, + exp_time=1, + n_of_trigger=1, cycle_high=4, ) request.device_manager.add_device("nidaq") reference_commands = get_instructions(request, ScanStubStatusMock) + # TODO #64 based on creating this ScanStatusMessage, we should test the logic of stage/kickoff/complete/unstage in Pilatus and mo1Bragg assert reference_commands == [ None, @@ -339,13 +340,13 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock): scan_duration=10, p_kink=50, e_kink=8500, - xrd_enable_low=True, - num_trigger_low=1, - exp_time_low=1, + break_enable_low=True, + break_time_low=1, cycle_low=1, - xrd_enable_high=True, - num_trigger_high=2, - exp_time_high=3, + break_enable_high=True, + break_time_high=2, + exp_time=1, + n_of_trigger=1, cycle_high=4, ) request.device_manager.add_device("nidaq") -- 2.49.1