From 96d746c2860221d336a755535e920ea0af8375b9 Mon Sep 17 00:00:00 2001 From: e21206 Date: Fri, 18 Aug 2023 08:48:41 +0200 Subject: [PATCH 01/53] fix: stage works again, unstage not yet --- ophyd_devices/epics/devices/pilatus_csaxs.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index a9f639f..8b710ae 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -19,6 +19,7 @@ class PilatusDetectorCamEx(PilatusDetectorCam, FileBase): pass +# TODO refactor class -> move away from DetectorBase and PilatusDetectorCamEx class to Device. -> this will be cleaner class PilatusCsaxs(DetectorBase): """ @@ -95,6 +96,7 @@ class PilatusCsaxs(DetectorBase): self.destination_path = os.path.join( self.service_cfg["base_path"] ) # os.path.join(self.service_cfg["base_path"], scan_dir) + data_msg = { "source": [ { @@ -156,20 +158,22 @@ class PilatusCsaxs(DetectorBase): { "frmCnt": self.num_frames, "timeout": 2000, - "ifType": "PULL", - "user": self.username, }, ] logger.info(data_msg) res = requests.put( - url="http://xbl-daq-34:8091/pilatus_1/run", + url="http://xbl-daq-34:8091/pilatus_2/wait", data=json.dumps(data_msg), headers=headers, ) # Reset triggermode to internal self.triggermode = 0 + if not res.ok: + res.raise_for_status() + + res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") if not res.ok: res.raise_for_status() return super().unstage() @@ -213,5 +217,6 @@ class PilatusCsaxs(DetectorBase): def stop(self, *, success=False) -> None: self.cam.acquire.set(0) + self.unstage() super().stop(success=success) self._stopped = True From 88b238ac13856ed1593dd01b9d2f7a762ca00111 Mon Sep 17 00:00:00 2001 From: e21206 Date: Fri, 18 Aug 2023 11:03:12 +0200 Subject: [PATCH 02/53] feat: added falcon ophyd device to repo --- ophyd_devices/epics/devices/falcon_csaxs.py | 88 +++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ophyd_devices/epics/devices/falcon_csaxs.py diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py new file mode 100644 index 0000000..1b3f9a8 --- /dev/null +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -0,0 +1,88 @@ +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device + +from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDXPMultiElementSystem +from ophyd.areadetector.plugins import HDF5Plugin + + +class EpicsDXPFalcon(Device): + """All high-level DXP parameters for each channel""" + + # Preset options + preset_mode = Cpt(EpicsSignalWithRBV, "PresetMode", string=True) + preset_real = Cpt(EpicsSignalWithRBV, "PresetReal") + preset_triggers = Cpt(EpicsSignalWithRBV, "PresetTriggers") + preset_events = Cpt(EpicsSignalWithRBV, "PresetEvents") + + elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime") + elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime") + elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime") + + # Energy Filter PVs + energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold") + min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation") + detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True) + scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor") + risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization") + + # Misc PVs + detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity") + decay_time = Cpt(EpicsSignalWithRBV, "DecayTime") + + # read-only diagnostics + triggers = Cpt(EpicsSignalRO, "Triggers", lazy=True) + events = Cpt(EpicsSignalRO, "Events", lazy=True) + input_count_rate = Cpt(EpicsSignalRO, "InputCountRate", lazy=True) + output_count_rate = Cpt(EpicsSignalRO, "OutputCountRate", lazy=True) + current_pixel = Cpt(EpicsSignal, "CurrentPixel") + + # Trace options + trace_data = Cpt(EpicsSignal, "TraceData") + + +class FalconDxp(EpicsDXPFalcon, EpicsDXPLowLevel): + pass + + +class FalconCsaxs(EpicsDXPMultiElementSystem, EpicsDXPMapping): + """FalxonX1 with HDF5 writer""" + + dxp = Cpt(FalconDxp, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + hdf5 = Cpt(HDF5Plugin, "HDF1:") + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + **kwargs, + ): + self.device_manager = device_manager + self.username = "e21206" # TODO get from config + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + # TODO how to get base_path + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/falcon/"} + self.filewriter = FileWriterMixin(self.service_cfg) + self.num_frames = 0 + self.readout = 0.003 # 3 ms + self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + + def stage(self) -> List[object]: + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() From eccacf169bf0a265fac1e20e8fe86af6277a9d4a Mon Sep 17 00:00:00 2001 From: e21206 Date: Fri, 18 Aug 2023 16:57:21 +0200 Subject: [PATCH 03/53] fix: adjust ophyd class layout --- ophyd_devices/epics/devices/falcon_csaxs.py | 108 +++++++++++++++----- 1 file changed, 84 insertions(+), 24 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 1b3f9a8..9f7cfa6 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -1,18 +1,19 @@ +import os +from typing import List from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDXPMultiElementSystem -from ophyd.areadetector.plugins import HDF5Plugin +from ophyd.areadetector.plugins import HDF5Plugin, HDF5Plugin_V21, FilePlugin_V22 + +from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core import bec_logger + +logger = bec_logger.logger class EpicsDXPFalcon(Device): """All high-level DXP parameters for each channel""" - # Preset options - preset_mode = Cpt(EpicsSignalWithRBV, "PresetMode", string=True) - preset_real = Cpt(EpicsSignalWithRBV, "PresetReal") - preset_triggers = Cpt(EpicsSignalWithRBV, "PresetTriggers") - preset_events = Cpt(EpicsSignalWithRBV, "PresetEvents") - elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime") elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime") elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime") @@ -28,27 +29,34 @@ class EpicsDXPFalcon(Device): detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity") decay_time = Cpt(EpicsSignalWithRBV, "DecayTime") + +class FalconHDF5Plugins(HDF5Plugin_V21, FilePlugin_V22): + pass + + +class FalconCsaxs(Device): + """FalxonX1 with HDF5 writer""" + + dxp = Cpt(EpicsDXPFalcon, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") + + # Preset options + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + # 0 No preset 1 Real time 2 Events 3 Triggers + preset_real = Cpt(EpicsSignal, "PresetReal") + preset_triggers = Cpt(EpicsSignal, "PresetTriggers") + preset_events = Cpt(EpicsSignal, "PresetEvents") # read-only diagnostics triggers = Cpt(EpicsSignalRO, "Triggers", lazy=True) events = Cpt(EpicsSignalRO, "Events", lazy=True) input_count_rate = Cpt(EpicsSignalRO, "InputCountRate", lazy=True) output_count_rate = Cpt(EpicsSignalRO, "OutputCountRate", lazy=True) - current_pixel = Cpt(EpicsSignal, "CurrentPixel") + # current_pixel = Cpt(EpicsSignal, "CurrentPixel") - # Trace options - trace_data = Cpt(EpicsSignal, "TraceData") - - -class FalconDxp(EpicsDXPFalcon, EpicsDXPLowLevel): - pass - - -class FalconCsaxs(EpicsDXPMultiElementSystem, EpicsDXPMapping): - """FalxonX1 with HDF5 writer""" - - dxp = Cpt(FalconDxp, "dxp1:") - mca = Cpt(EpicsMCARecord, "mca1") - hdf5 = Cpt(HDF5Plugin, "HDF1:") + # # Trace options + # trace_data = Cpt(EpicsSignal, "TraceData") def __init__( self, @@ -63,7 +71,9 @@ class FalconCsaxs(EpicsDXPMultiElementSystem, EpicsDXPMapping): **kwargs, ): self.device_manager = device_manager - self.username = "e21206" # TODO get from config + self.username = ( + "e21206" # self.device_manager.producer.get(MessageEndpoints.account()).decode() + ) # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() super().__init__( prefix=prefix, @@ -74,15 +84,65 @@ class FalconCsaxs(EpicsDXPMultiElementSystem, EpicsDXPMapping): parent=parent, **kwargs, ) - # TODO how to get base_path + # TODO meaningful to use FileWriterMixin self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/falcon/"} self.filewriter = FileWriterMixin(self.service_cfg) self.num_frames = 0 self.readout = 0.003 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + # Init script for falcon + + # self._clean_up_dxp() + self._init_hdf5_saving() + + # self._set_up_mapping_mode() def stage(self) -> List[object]: + # scan_msg = self._get_current_scan_msg() + # self.metadata = { + # "scanID": scan_msg.content["scanID"], + # "RID": scan_msg.content["info"]["RID"], + # "queueID": scan_msg.content["info"]["queueID"], + # } + self.scan_number = 10 # scan_msg.content["info"]["scan_number"] + self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] + self.num_frames = 3 # scan_msg.content["info"]["num_points"] + # TODO remove + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + + self.destination_path = os.path.join(self.service_cfg["base_path"]) + return super().stage() def unstage(self) -> List[object]: return super().unstage() + + def _clean_up_hdf5plugin(self) -> None: + # TODO propably better monitored! similar as threshold + self.hdf5.capture.set(0) + + def _clean_up_dxp(self) -> None: + """Clean up""" + self.stop_all.set(1) + self.erase_all.set(1) + + def _acquisition_parameters(self) -> None: + self.preset_real_time.set(self.exposure_time) + + def _init_hdf5_saving(self) -> None: + """Set up hdf5 save parameters""" + self.hdf5.enable.set(1) # EnableCallbacks + self.hdf5.xml_file_name.set("layout.xml") + self.hdf5.lazy_open.set(1) # Yes -> To be checked how to add FilePlugin_V21+ + self.hdf5.temp_suffix.set("temps") # -> To be checked how to add FilePlugin_V22+ + + def _set_up_mapping_mode(self) -> None: + """Set up mapping mode params""" + self.input_logic_polarity.set(0) + self.preset_mode.set(1) + self.collect_mode.set(1) + self.pixel_advance_mode.set(1) + self.ignore_gate.set(1) + self.auto_pixels_per_buffer.set(0) # 0 Manual 1 Auto + # self.auto + # self.ignore From 41e0e40bc70d4b4547d331cf237feeaa39b5d721 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 09:02:05 +0200 Subject: [PATCH 04/53] fix: add initialization functionality --- ophyd_devices/epics/devices/falcon_csaxs.py | 62 ++++++++++++--------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 9f7cfa6..f215bad 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -41,22 +41,29 @@ class FalconCsaxs(Device): mca = Cpt(EpicsMCARecord, "mca1") hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") - # Preset options + # Control stop_all = Cpt(EpicsSignal, "StopAll") erase_all = Cpt(EpicsSignal, "EraseAll") - # 0 No preset 1 Real time 2 Events 3 Triggers + start_all = Cpt(EpicsSignal, "StartAll") + # Preset options + preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers preset_real = Cpt(EpicsSignal, "PresetReal") - preset_triggers = Cpt(EpicsSignal, "PresetTriggers") preset_events = Cpt(EpicsSignal, "PresetEvents") + preset_triggers = Cpt(EpicsSignal, "PresetTriggers") # read-only diagnostics triggers = Cpt(EpicsSignalRO, "Triggers", lazy=True) events = Cpt(EpicsSignalRO, "Events", lazy=True) input_count_rate = Cpt(EpicsSignalRO, "InputCountRate", lazy=True) output_count_rate = Cpt(EpicsSignalRO, "OutputCountRate", lazy=True) - # current_pixel = Cpt(EpicsSignal, "CurrentPixel") - # # Trace options - # trace_data = Cpt(EpicsSignal, "TraceData") + # Mapping control + collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping + pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode") + ignore_gate = Cpt(EpicsSignal, "IgnoreGate") + input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity") + auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer") + pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer") + pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") def __init__( self, @@ -92,10 +99,9 @@ class FalconCsaxs(Device): self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered # Init script for falcon - # self._clean_up_dxp() + self._clean_up() self._init_hdf5_saving() - - # self._set_up_mapping_mode() + self._init_mapping_mode() def stage(self) -> List[object]: # scan_msg = self._get_current_scan_msg() @@ -117,32 +123,38 @@ class FalconCsaxs(Device): def unstage(self) -> List[object]: return super().unstage() - def _clean_up_hdf5plugin(self) -> None: - # TODO propably better monitored! similar as threshold - self.hdf5.capture.set(0) - - def _clean_up_dxp(self) -> None: + def _clean_up(self) -> None: """Clean up""" self.stop_all.set(1) self.erase_all.set(1) - def _acquisition_parameters(self) -> None: - self.preset_real_time.set(self.exposure_time) - def _init_hdf5_saving(self) -> None: """Set up hdf5 save parameters""" self.hdf5.enable.set(1) # EnableCallbacks self.hdf5.xml_file_name.set("layout.xml") self.hdf5.lazy_open.set(1) # Yes -> To be checked how to add FilePlugin_V21+ self.hdf5.temp_suffix.set("temps") # -> To be checked how to add FilePlugin_V22+ + self.hdf5.capture.set(0) - def _set_up_mapping_mode(self) -> None: + def _init_mapping_mode(self) -> None: """Set up mapping mode params""" - self.input_logic_polarity.set(0) - self.preset_mode.set(1) - self.collect_mode.set(1) - self.pixel_advance_mode.set(1) - self.ignore_gate.set(1) + self.collect_mode.set(1) # 1 MCA Mapping, 0 MCA Spectrum + self.preset_mode.set(1) # 1 Realtime + self.input_logic_polarity.set(0) # 0 Normal, 1 Inverted + self.pixel_advance_mode.set(1) # 0 User, 1 Gate, 2 Sync + self.ignore_gate.set(1) # 1 Yes self.auto_pixels_per_buffer.set(0) # 0 Manual 1 Auto - # self.auto - # self.ignore + self.pixels_per_buffer.set(16) # + + def _prep_mca_acquisition(self) -> None: + """Prepare detector for acquisition""" + self.collect_mode.set(1) + self.preset_real.set(self.exposure_time) + self.pixels_per_run.set(self.num_frames) + self.auto_pixels_per_buffer.set(0) + self.pixels_per_buffer.set(16) + + # HDF prep + self.hdf5.file_path(self.destination_path) + self.hdf5.file_name("falcon") + self.hdf5.file_template("%sfalcon.h5") From 83c395cf3511bb56d0f58b75bb5c00c5bc00992f Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 11:57:41 +0200 Subject: [PATCH 05/53] fix: add bec producer message to stage --- ophyd_devices/epics/devices/falcon_csaxs.py | 94 +++++++++++++++++---- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index f215bad..561531f 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -1,4 +1,5 @@ import os +import time from typing import List from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device @@ -6,6 +7,7 @@ from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDX from ophyd.areadetector.plugins import HDF5Plugin, HDF5Plugin_V21, FilePlugin_V22 from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core import MessageEndpoints, BECMessage, RedisConnector from bec_lib.core import bec_logger logger = bec_logger.logger @@ -29,6 +31,8 @@ class EpicsDXPFalcon(Device): detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity") decay_time = Cpt(EpicsSignalWithRBV, "DecayTime") + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + class FalconHDF5Plugins(HDF5Plugin_V21, FilePlugin_V22): pass @@ -45,16 +49,17 @@ class FalconCsaxs(Device): stop_all = Cpt(EpicsSignal, "StopAll") erase_all = Cpt(EpicsSignal, "EraseAll") start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") # Preset options preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers preset_real = Cpt(EpicsSignal, "PresetReal") preset_events = Cpt(EpicsSignal, "PresetEvents") preset_triggers = Cpt(EpicsSignal, "PresetTriggers") # read-only diagnostics - triggers = Cpt(EpicsSignalRO, "Triggers", lazy=True) - events = Cpt(EpicsSignalRO, "Events", lazy=True) - input_count_rate = Cpt(EpicsSignalRO, "InputCountRate", lazy=True) - output_count_rate = Cpt(EpicsSignalRO, "OutputCountRate", lazy=True) + triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True) + events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True) + input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True) + output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True) # Mapping control collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping @@ -77,11 +82,6 @@ class FalconCsaxs(Device): device_manager=None, **kwargs, ): - self.device_manager = device_manager - self.username = ( - "e21206" # self.device_manager.producer.get(MessageEndpoints.account()).decode() - ) - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() super().__init__( prefix=prefix, name=name, @@ -91,12 +91,21 @@ class FalconCsaxs(Device): parent=parent, **kwargs, ) + self.device_manager = device_manager + self.username = ( + "e21206" # self.device_manager.producer.get(MessageEndpoints.account()).decode() + ) + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + self.name = name # TODO meaningful to use FileWriterMixin self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/falcon/"} self.filewriter = FileWriterMixin(self.service_cfg) self.num_frames = 0 self.readout = 0.003 # 3 ms - self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + self._value_pixel_per_buffer = 16 + self._file_template = f"%s%s_{self.name}.h5" + # TODO localhost:6379 + self._producer = RedisConnector(["localhost:6379"]).producer() # Init script for falcon self._clean_up() @@ -113,28 +122,62 @@ class FalconCsaxs(Device): self.scan_number = 10 # scan_msg.content["info"]["scan_number"] self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] self.num_frames = 3 # scan_msg.content["info"]["num_points"] - # TODO remove + # TODO update service config for file path gen.. - But problem with path # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - self.destination_path = os.path.join(self.service_cfg["base_path"]) + self.filename = f"test_{self.scan_number}" + self._prep_mca_acquisition() + + # Filename to Redis + path_to_file = self._file_template % (self.destination_path, self.filename) + msg = BECMessage.FileMessage(file_path=path_to_file, done=False) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) + + # TODO BEC message on where file is going to be written to return super().stage() + def acquire(self) -> None: + self.start_all.set(1) + def unstage(self) -> List[object]: + # Check number of acquisitions + while not self._check_falcon_done(): + logger.info("Waiting for acquisition to finish, sleeping 0.1s ") + time.sleep(0.1) + # Compare expected vs measured number of pixel + # logger.info( + # f'Falcon: number of measured frames from expected {self.current_pixel.read()}/{self.pixels_per_run.read()}' + # ) + # logger.info( + # "Falcon write file state{self.hdf5.capture.read()}/{self.hdf5.writestatus}" + # ) + # if not self.hdf5.write_status.read()[f'{self.name}_hdf5_write_status']['value'] : + + self._clean_up() + msg = BECMessage.FileMessage(file_path=path_to_file, done=True, successful=state) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) + return super().unstage() def _clean_up(self) -> None: """Clean up""" + self.hdf5.capture.set(0) self.stop_all.set(1) self.erase_all.set(1) def _init_hdf5_saving(self) -> None: """Set up hdf5 save parameters""" self.hdf5.enable.set(1) # EnableCallbacks - self.hdf5.xml_file_name.set("layout.xml") + self.hdf5.xml_file_name.set("layout.xml") # Points to hardcopy of HDF5 Layout xml file self.hdf5.lazy_open.set(1) # Yes -> To be checked how to add FilePlugin_V21+ self.hdf5.temp_suffix.set("temps") # -> To be checked how to add FilePlugin_V22+ - self.hdf5.capture.set(0) def _init_mapping_mode(self) -> None: """Set up mapping mode params""" @@ -152,9 +195,24 @@ class FalconCsaxs(Device): self.preset_real.set(self.exposure_time) self.pixels_per_run.set(self.num_frames) self.auto_pixels_per_buffer.set(0) - self.pixels_per_buffer.set(16) + self.pixels_per_buffer.set(self._value_pixel_per_buffer) # HDF prep - self.hdf5.file_path(self.destination_path) - self.hdf5.file_name("falcon") - self.hdf5.file_template("%sfalcon.h5") + self.hdf5.file_path.set(self.destination_path) + self.hdf5.file_name.set(self.filename) + self.hdf5.file_template.set(self._file_template) + self.hdf5.num_capture.set(self.num_frames // self._value_pixel_per_buffer + 1) + self.hdf5.file_write_mode.set(2) + self.hdf5.capture.set(1) + + # Start acquisition + # Check falcon status?? self.state --> 1 for acquiring. + + def _check_falcon_done(self) -> bool: + state = self.state.read()[f"{self.name }_state"]["value"] + if state is [0, 1]: + return not bool(state) + else: + # TODO raise error + logger.warning("Returned in unknown state") + return state From 448890ab27ba1bfeb24870d792c498b96aa7cc47 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 11:58:09 +0200 Subject: [PATCH 06/53] feat: add mcs ophyd device --- ophyd_devices/epics/devices/mcs_csaxs.py | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 ophyd_devices/epics/devices/mcs_csaxs.py diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py new file mode 100644 index 0000000..6ed79ac --- /dev/null +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -0,0 +1,77 @@ +from ophyd import EpicsSignal, EpicsSignalRO, Component as Cpt, Device +from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDXPMultiElementSystem +from ophyd.scaler import ScalerCH + + +class SIS38XX(Device): + """SIS38XX control""" + + # Acquisition + erase_all = Cpt(EpicsSignal, "EraseAll") + erase_start = Cpt(EpicsSignal, "EraseStart", trigger_value=1) + start_all = Cpt(EpicsSignal, "StartAll") + stop_all = Cpt(EpicsSignal, "StopAll") + + acquiring = Cpt(EpicsSignal, "Acquiring") + + preset_real = Cpt(EpicsSignal, "PresetReal") + elapsed_real = Cpt(EpicsSignal, "ElapsedReal") + + read_all = Cpt(EpicsSignal, "ReadAll") + num_use_all = Cpt(EpicsSignal, "NuseAll") + current_channel = Cpt(EpicsSignal, "CurrentChannel") + dwell = Cpt(EpicsSignal, "Dwell") + channel_advance = Cpt(EpicsSignal, "ChannelAdvance") + count_on_start = Cpt(EpicsSignal, "CountOnStart") + software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance") + channel1_source = Cpt(EpicsSignal, "Channel1Source") + prescale = Cpt(EpicsSignal, "Prescale") + enable_client_wait = Cpt(EpicsSignal, "EnableClientWait") + client_wait = Cpt(EpicsSignal, "ClientWait") + acquire_mode = Cpt(EpicsSignal, "AcquireMode") + mux_output = Cpt(EpicsSignal, "MUXOutput") + user_led = Cpt(EpicsSignal, "UserLED") + input_mode = Cpt(EpicsSignal, "InputMode") + input_polarity = Cpt(EpicsSignal, "InputPolarity") + output_mode = Cpt(EpicsSignal, "OutputMode") + output_polarity = Cpt(EpicsSignal, "OutputPolarity") + model = Cpt(EpicsSignalRO, "Model", string=True) + firmware = Cpt(EpicsSignalRO, "Firmware") + max_channels = Cpt(EpicsSignalRO, "MaxChannels") + + +class SIS3820(SIS38XX): + scaler = Cpt(ScalerCH, "scaler1") + + mca1 = Cpt(EpicsMCARecord, "mca1") + mca2 = Cpt(EpicsMCARecord, "mca2") + mca3 = Cpt(EpicsMCARecord, "mca3") + mca4 = Cpt(EpicsMCARecord, "mca4") + mca5 = Cpt(EpicsMCARecord, "mca5") + mca6 = Cpt(EpicsMCARecord, "mca6") + mca7 = Cpt(EpicsMCARecord, "mca7") + mca8 = Cpt(EpicsMCARecord, "mca8") + mca9 = Cpt(EpicsMCARecord, "mca9") + mca10 = Cpt(EpicsMCARecord, "mca10") + mca11 = Cpt(EpicsMCARecord, "mca11") + mca12 = Cpt(EpicsMCARecord, "mca12") + mca13 = Cpt(EpicsMCARecord, "mca13") + mca14 = Cpt(EpicsMCARecord, "mca14") + mca15 = Cpt(EpicsMCARecord, "mca15") + mca16 = Cpt(EpicsMCARecord, "mca16") + mca17 = Cpt(EpicsMCARecord, "mca17") + mca18 = Cpt(EpicsMCARecord, "mca18") + mca19 = Cpt(EpicsMCARecord, "mca19") + mca20 = Cpt(EpicsMCARecord, "mca20") + mca21 = Cpt(EpicsMCARecord, "mca21") + mca22 = Cpt(EpicsMCARecord, "mca22") + mca23 = Cpt(EpicsMCARecord, "mca23") + mca24 = Cpt(EpicsMCARecord, "mca24") + mca25 = Cpt(EpicsMCARecord, "mca25") + mca26 = Cpt(EpicsMCARecord, "mca26") + mca27 = Cpt(EpicsMCARecord, "mca27") + mca28 = Cpt(EpicsMCARecord, "mca28") + mca29 = Cpt(EpicsMCARecord, "mca29") + mca30 = Cpt(EpicsMCARecord, "mca30") + mca31 = Cpt(EpicsMCARecord, "mca31") + mca32 = Cpt(EpicsMCARecord, "mca32") From b122de69acfd88d82eaba85534840e7fae21b718 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 18:23:19 +0200 Subject: [PATCH 07/53] fix: falcon updates --- ophyd_devices/epics/devices/falcon_csaxs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 561531f..74bbaad 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -156,6 +156,7 @@ class FalconCsaxs(Device): # "Falcon write file state{self.hdf5.capture.read()}/{self.hdf5.writestatus}" # ) # if not self.hdf5.write_status.read()[f'{self.name}_hdf5_write_status']['value'] : + # state = self.hdf5.write_status.read()[f'{self.name}_hdf5'] self._clean_up() msg = BECMessage.FileMessage(file_path=path_to_file, done=True, successful=state) From 14ca550af143cdca9237271311b9c5ea280d7809 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 18:24:27 +0200 Subject: [PATCH 08/53] fix: mcs updates --- ophyd_devices/epics/devices/mcs_csaxs.py | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 6ed79ac..a34a140 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -48,30 +48,30 @@ class SIS3820(SIS38XX): mca3 = Cpt(EpicsMCARecord, "mca3") mca4 = Cpt(EpicsMCARecord, "mca4") mca5 = Cpt(EpicsMCARecord, "mca5") - mca6 = Cpt(EpicsMCARecord, "mca6") - mca7 = Cpt(EpicsMCARecord, "mca7") - mca8 = Cpt(EpicsMCARecord, "mca8") - mca9 = Cpt(EpicsMCARecord, "mca9") - mca10 = Cpt(EpicsMCARecord, "mca10") - mca11 = Cpt(EpicsMCARecord, "mca11") - mca12 = Cpt(EpicsMCARecord, "mca12") - mca13 = Cpt(EpicsMCARecord, "mca13") - mca14 = Cpt(EpicsMCARecord, "mca14") - mca15 = Cpt(EpicsMCARecord, "mca15") - mca16 = Cpt(EpicsMCARecord, "mca16") - mca17 = Cpt(EpicsMCARecord, "mca17") - mca18 = Cpt(EpicsMCARecord, "mca18") - mca19 = Cpt(EpicsMCARecord, "mca19") - mca20 = Cpt(EpicsMCARecord, "mca20") - mca21 = Cpt(EpicsMCARecord, "mca21") - mca22 = Cpt(EpicsMCARecord, "mca22") - mca23 = Cpt(EpicsMCARecord, "mca23") - mca24 = Cpt(EpicsMCARecord, "mca24") - mca25 = Cpt(EpicsMCARecord, "mca25") - mca26 = Cpt(EpicsMCARecord, "mca26") - mca27 = Cpt(EpicsMCARecord, "mca27") - mca28 = Cpt(EpicsMCARecord, "mca28") - mca29 = Cpt(EpicsMCARecord, "mca29") - mca30 = Cpt(EpicsMCARecord, "mca30") - mca31 = Cpt(EpicsMCARecord, "mca31") - mca32 = Cpt(EpicsMCARecord, "mca32") + # mca6 = Cpt(EpicsMCARecord, "mca6") + # mca7 = Cpt(EpicsMCARecord, "mca7") + # mca8 = Cpt(EpicsMCARecord, "mca8") + # mca9 = Cpt(EpicsMCARecord, "mca9") + # mca10 = Cpt(EpicsMCARecord, "mca10") + # mca11 = Cpt(EpicsMCARecord, "mca11") + # mca12 = Cpt(EpicsMCARecord, "mca12") + # mca13 = Cpt(EpicsMCARecord, "mca13") + # mca14 = Cpt(EpicsMCARecord, "mca14") + # mca15 = Cpt(EpicsMCARecord, "mca15") + # mca16 = Cpt(EpicsMCARecord, "mca16") + # mca17 = Cpt(EpicsMCARecord, "mca17") + # mca18 = Cpt(EpicsMCARecord, "mca18") + # mca19 = Cpt(EpicsMCARecord, "mca19") + # mca20 = Cpt(EpicsMCARecord, "mca20") + # mca21 = Cpt(EpicsMCARecord, "mca21") + # mca22 = Cpt(EpicsMCARecord, "mca22") + # mca23 = Cpt(EpicsMCARecord, "mca23") + # mca24 = Cpt(EpicsMCARecord, "mca24") + # mca25 = Cpt(EpicsMCARecord, "mca25") + # mca26 = Cpt(EpicsMCARecord, "mca26") + # mca27 = Cpt(EpicsMCARecord, "mca27") + # mca28 = Cpt(EpicsMCARecord, "mca28") + # mca29 = Cpt(EpicsMCARecord, "mca29") + # mca30 = Cpt(EpicsMCARecord, "mca30") + # mca31 = Cpt(EpicsMCARecord, "mca31") + # mca32 = Cpt(EpicsMCARecord, "mca32") From f3e4575359994c134e1b207915fadb9f8f92e4d9 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 21 Aug 2023 18:24:54 +0200 Subject: [PATCH 09/53] feat: add eiger9m csaxs --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 187 +++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 ophyd_devices/epics/devices/eiger9m_csaxs.py diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py new file mode 100644 index 0000000..8c8f7e7 --- /dev/null +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -0,0 +1,187 @@ +import json +import os +import requests +import numpy as np + +from typing import List + +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd import CamBase, DetectorBase +from ophyd import ADComponent as ADCpt +from ophyd.areadetector.plugins import FileBase + +from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector +from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core import bec_logger + +from std_daq_client import StdDaqClient + + +logger = bec_logger.logger + + +class slsDetectorCam(CamBase, FileBase): + detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") + setting = ADCpt(EpicsSignalWithRBV, "Setting") + delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") + threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits") + bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") + num_gates = ADCpt(EpicsSignalWithRBV, "NumGates") + num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") + num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") + timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") + trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") + high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage") + # Receiver and data callback + receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode") + receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream") + enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback") + missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV") + # Direct settings access + setup_file = ADCpt(EpicsSignal, "SetupFile") + load_setup = ADCpt(EpicsSignal, "LoadSetup") + command = ADCpt(EpicsSignal, "Command") + # Mythen 3 + counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask") + counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold") + counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold") + counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold") + gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay") + gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width") + gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay") + gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width") + gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay") + gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width") + # Moench + json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode") + json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") + + +# TODO refactor class -> move away from DetectorBase and PilatusDetectorCamEx class to Device. -> this will be cleaner +class Eiger9mCsaxs(DetectorBase): + """ + + in device config, device_access needs to be set true to inject the device manager + """ + + cam = ADCpt(slsDetectorCam, "cam1:") + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + **kwargs, + ): + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.device_manager = device_manager + self.name = name + self.username = "e21206" # + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + + self._init_eiger9m() + self._init_standard_daq() + + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} + self.filewriter = FileWriterMixin(self.service_cfg) # TODO check FileWriterMixin if generix + self._producer = RedisConnector(["localhost:6379"]).producer() + # self.num_frames = 0 + self.readout = 0.003 # 3 ms + self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + + def _init_eiger9m(self) -> None: + """Init parameters for Eiger 9m""" + pass + + def _init_standard_daq(self) -> None: + self.std_rest_server_url = "http://xbl-daq-29:5000" + self.std_client = StdDaqClient(url_base=self.std_rest_server_url) + + def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: + msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + return BECMessage.ScanStatusMessage.loads(msg) + + def _prepare_standard_daq(self) -> None: + self.std_client.start_writer_async({"output_file": filename, "n_images": self.num_frames}) + + def stage(self) -> List[object]: + # TODO remove + # scan_msg = self._get_current_scan_msg() + # self.metadata = { + # "scanID": scan_msg.content["scanID"], + # "RID": scan_msg.content["info"]["RID"], + # "queueID": scan_msg.content["info"]["queueID"], + # } + self.scan_number = 10 # scan_msg.content["info"]["scan_number"] + self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] + self.num_frames = 3 # scan_msg.content["info"]["num_points"] + self.mokev = 12 # self.device_manager.devices.mokev.read()['mokev']['value'] + # TODO remove + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + + # set pilatus threshold + self._set_eiger_threshold() + self._set_acquisition_params( + exp_time=self.exp_time, + readout=self.readout, + num_frames=self.num_frames, + triggermode=self.triggermode, + ) + + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() + + def _set_eiger_threshold(self) -> None: + pil_threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + if not np.isclose(self.mokev / 2, pil_threshold, rtol=0.05): + self.cam.threshold_energy.set(self.mokev / 2) + + def _set_acquisition_params( + self, exp_time: float, readout: float, num_frames: int, triggermode: int + ) -> None: + """set acquisition parameters on the detector + + Args: + exp_time (float): exposure time + readout (float): readout time + num_frames (int): images per scan + triggermode (int): + 0 Internal + 1 Ext. Enable + 2 Ext. Trigger + 3 Mult. Trigger + 4 Alignment + Returns: + None + """ + self.cam.acquire_time.set(exp_time) + self.cam.acquire_period.set(exp_time + readout) + self.cam.num_images.set(num_frames) + self.cam.num_exposures.set(1) + # trigger_mode exists in baseclass + self.cam.timing_mode.set(triggermode) + + def acquire(self) -> None: + self.cam.acquire.set(1) + + def stop(self, *, success=False) -> None: + self.cam.acquire.set(0) + self.unstage() + super().stop(success=success) + self._stopped = True From 287c667621582506dd85c02c022a4aeddba1fb7b Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 21 Aug 2023 21:51:36 +0200 Subject: [PATCH 10/53] refactor: refactoring of eiger9m class, alsmost compatible with pilatus --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 153 +++++++++++-------- 1 file changed, 87 insertions(+), 66 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 8c8f7e7..3d5f3ed 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,9 +1,5 @@ -import json -import os -import requests -import numpy as np - from typing import List +import numpy as np from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV from ophyd import CamBase, DetectorBase @@ -20,7 +16,7 @@ from std_daq_client import StdDaqClient logger = bec_logger.logger -class slsDetectorCam(CamBase, FileBase): +class SlsDetectorCam(CamBase, FileBase): detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") setting = ADCpt(EpicsSignalWithRBV, "Setting") delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") @@ -58,14 +54,19 @@ class slsDetectorCam(CamBase, FileBase): json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") -# TODO refactor class -> move away from DetectorBase and PilatusDetectorCamEx class to Device. -> this will be cleaner class Eiger9mCsaxs(DetectorBase): + """Eiger 9M detector for CSAXS + + Parent class: DetectorBase + Device class: SlsDetectorCam + + Attributes: + name str: 'eiger' + prefix (str): PV prefix (X12SA-ES-EIGER9M:) + """ - in device config, device_access needs to be set true to inject the device manager - """ - - cam = ADCpt(slsDetectorCam, "cam1:") + cam = ADCpt(SlsDetectorCam, "cam1:") def __init__( self, @@ -90,16 +91,16 @@ class Eiger9mCsaxs(DetectorBase): ) self.device_manager = device_manager self.name = name - self.username = "e21206" # + self.username = "e21206" + # TODO once running from BEC # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() self._init_eiger9m() self._init_standard_daq() self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} - self.filewriter = FileWriterMixin(self.service_cfg) # TODO check FileWriterMixin if generix + self.filewriter = FileWriterMixin(self.service_cfg) self._producer = RedisConnector(["localhost:6379"]).producer() - # self.num_frames = 0 self.readout = 0.003 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -115,73 +116,93 @@ class Eiger9mCsaxs(DetectorBase): msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) return BECMessage.ScanStatusMessage.loads(msg) - def _prepare_standard_daq(self) -> None: - self.std_client.start_writer_async({"output_file": filename, "n_images": self.num_frames}) + def _load_scan_metadata(self) -> None: + scan_msg = self._get_current_scan_msg() + self.metadata = { + "scanID": scan_msg.content["scanID"], + "RID": scan_msg.content["info"]["RID"], + "queueID": scan_msg.content["info"]["queueID"], + } + self.scanID = scan_msg.content["scanID"] + self.scan_number = scan_msg.content["info"]["scan_number"] + self.exp_time = scan_msg.content["info"]["exp_time"] + self.num_frames = scan_msg.content["info"]["num_points"] + self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + # self.triggermode = scan_msg.content["info"]["trigger_mode"] + self.filepath = self.filewriter.compile_full_filename( + self.scan_number, "eiger", 1000, 5, True + ) + + def _prep_det(self) -> None: + self._set_det_threshold() + self._set_acquisition_params() + + def _set_det_threshold(self) -> None: + # threshold_energy PV exists on Eiger 9M? + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + if not np.isclose(self.mokev / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(self.mokev / 2) + + def _set_acquisition_params(self) -> None: + self.cam.acquire_time.set(self.exp_time) + self.cam.acquire_period.set(self.exp_time + self.readout) + self.cam.num_images.set(self.num_frames) + self.cam.num_exposures.set(1) + # trigger_mode vs timing mode ??!! + self.cam.timing_mode.set(self.triggermode) + + def _prep_file_writer(self) -> None: + self.std_client.start_writer_async( + {"output_file": self.filepath, "n_images": self.num_frames} + ) + + def _close_file_writer(self) -> None: + pass def stage(self) -> List[object]: - # TODO remove - # scan_msg = self._get_current_scan_msg() - # self.metadata = { - # "scanID": scan_msg.content["scanID"], - # "RID": scan_msg.content["info"]["RID"], - # "queueID": scan_msg.content["info"]["queueID"], - # } - self.scan_number = 10 # scan_msg.content["info"]["scan_number"] - self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] - self.num_frames = 3 # scan_msg.content["info"]["num_points"] - self.mokev = 12 # self.device_manager.devices.mokev.read()['mokev']['value'] - # TODO remove - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + """stage the detector and file writer""" + # TODO remove once running from BEC + # self._load_scan_metadata() + self.scan_number = 10 + self.exp_time = 0.5 + self.num_frames = 3 + self.mokev = 12 + self.triggermode = 0 - # set pilatus threshold - self._set_eiger_threshold() - self._set_acquisition_params( - exp_time=self.exp_time, - readout=self.readout, - num_frames=self.num_frames, - triggermode=self.triggermode, + self._prep_det() + self._prep_file_writer() + + msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scanID, "eiger9m"), + msg.dumps(), ) return super().stage() def unstage(self) -> List[object]: + """unstage the detector and file writer""" + self.timing_mode = 0 + self._close_file_writer() + # TODO file succesfully written? + state = True + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) return super().unstage() - def _set_eiger_threshold(self) -> None: - pil_threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] - if not np.isclose(self.mokev / 2, pil_threshold, rtol=0.05): - self.cam.threshold_energy.set(self.mokev / 2) - - def _set_acquisition_params( - self, exp_time: float, readout: float, num_frames: int, triggermode: int - ) -> None: - """set acquisition parameters on the detector - - Args: - exp_time (float): exposure time - readout (float): readout time - num_frames (int): images per scan - triggermode (int): - 0 Internal - 1 Ext. Enable - 2 Ext. Trigger - 3 Mult. Trigger - 4 Alignment - Returns: - None - """ - self.cam.acquire_time.set(exp_time) - self.cam.acquire_period.set(exp_time + readout) - self.cam.num_images.set(num_frames) - self.cam.num_exposures.set(1) - # trigger_mode exists in baseclass - self.cam.timing_mode.set(triggermode) - def acquire(self) -> None: + """Start acquisition in software trigger mode, + or arm the detector in hardware of the detector + """ self.cam.acquire.set(1) def stop(self, *, success=False) -> None: + """Stop the scan, with camera and file writer""" self.cam.acquire.set(0) + self.std_client.stop_writer() self.unstage() super().stop(success=success) self._stopped = True From b1150c41fe4199be91ed164e089db5379a8f0435 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 21 Aug 2023 21:52:09 +0200 Subject: [PATCH 11/53] refactor: class refactoring, pending change to SlsDetectorCam --- ophyd_devices/epics/devices/pilatus_csaxs.py | 190 +++++++++++-------- 1 file changed, 107 insertions(+), 83 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 8b710ae..06eec96 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -1,17 +1,17 @@ import json import os +from typing import List import requests import numpy as np -from typing import List - from ophyd.areadetector import ADComponent as ADCpt, PilatusDetectorCam, DetectorBase from ophyd.areadetector.plugins import FileBase -from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger + logger = bec_logger.logger @@ -19,14 +19,18 @@ class PilatusDetectorCamEx(PilatusDetectorCam, FileBase): pass -# TODO refactor class -> move away from DetectorBase and PilatusDetectorCamEx class to Device. -> this will be cleaner class PilatusCsaxs(DetectorBase): + """Pilatus_2 300k detector for CSAXS + + Parent class: DetectorBase + Device class: PilatusDetectorCamEx + + Attributes: + name str: 'eiger' + prefix (str): PV prefix (X12SA-ES-PILATUS300K:) + """ - in device config, device_access needs to be set true to inject the device manager - """ - - _html_docs = ["PilatusDoc.html"] cam = ADCpt(PilatusDetectorCamEx, "cam1:") def __init__( @@ -41,9 +45,6 @@ class PilatusCsaxs(DetectorBase): device_manager=None, **kwargs, ): - self.device_manager = device_manager - self.username = "e21206" # TODO get from config - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() super().__init__( prefix=prefix, name=name, @@ -53,10 +54,15 @@ class PilatusCsaxs(DetectorBase): parent=parent, **kwargs, ) - # TODO how to get base_path - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/pilatus_2/"} + self.device_manager = device_manager + self.name = name + self.username = "e21206" + # TODO once running from BEC + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} self.filewriter = FileWriterMixin(self.service_cfg) - self.num_frames = 0 + self._producer = RedisConnector(["localhost:6379"]).producer() self.readout = 0.003 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -64,24 +70,46 @@ class PilatusCsaxs(DetectorBase): msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) return BECMessage.ScanStatusMessage.loads(msg) - def stage(self) -> List[object]: - # TODO remove - # scan_msg = self._get_current_scan_msg() - # self.metadata = { - # "scanID": scan_msg.content["scanID"], - # "RID": scan_msg.content["info"]["RID"], - # "queueID": scan_msg.content["info"]["queueID"], - # } - self.scan_number = 10 # scan_msg.content["info"]["scan_number"] - self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] - self.num_frames = 3 # scan_msg.content["info"]["num_points"] - # TODO remove - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + def _load_scan_metadata(self) -> None: + scan_msg = self._get_current_scan_msg() + self.metadata = { + "scanID": scan_msg.content["scanID"], + "RID": scan_msg.content["info"]["RID"], + "queueID": scan_msg.content["info"]["queueID"], + } + self.scanID = scan_msg.content["scanID"] + self.scan_number = scan_msg.content["info"]["scan_number"] + self.exp_time = scan_msg.content["info"]["exp_time"] + self.num_frames = scan_msg.content["info"]["num_points"] + self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + self.device_manager.devices.mokev.read()["mokev"]["value"] + # self.triggermode = scan_msg.content["info"]["trigger_mode"] + self.filename = self.filewriter.compile_full_filename( + self.scan_number, "pilatus_2", 1000, 5, True + ) - # set pilatus threshol - self._set_threshold() + def _prep_det(self) -> None: + self._set_det_threshold() + self._set_acquisition_params() - # set Epic PVs for filewriting + def _set_det_threshold(self) -> None: + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + if not np.isclose(self.mokev / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(self.mokev / 2) + + def _set_acquisition_params(self) -> None: + """set acquisition parameters on the detector""" + self.cam.acquire_time.set(self.exp_time) + self.cam.acquire_period.set(self.exp_time + self.readout) + self.cam.num_images.set(self.num_frames) + self.cam.num_exposures.set(1) + self.cam.trigger_mode.set(self.triggermode) + + def _prep_file_writer(self) -> None: + """Prepare the file writer for pilatus_2 + a zmq service is running on xbl-daq-34 that is waiting + for a zmq message to start the writer for the pilatus_2 x12sa-pd-2 + """ self.cam.file_path.set(f"/dev/shm/zmq/") self.cam.file_name.set(f"{self.username}_2_{self.scan_number:05d}") self.cam.auto_increment.set(1) # auto increment @@ -89,13 +117,12 @@ class PilatusCsaxs(DetectorBase): self.cam.file_format.set(0) # 0: TIFF self.cam.file_template.set("%s%s_%5.5d.cbf") - # compile zmq stream for data transfer - scan_dir = self.filewriter._get_scan_directory( - scan_bundle=1000, scan_number=self.scan_number, leading_zeros=5 - ) - self.destination_path = os.path.join( - self.service_cfg["base_path"] - ) # os.path.join(self.service_cfg["base_path"], scan_dir) + # TODO Filewriter Plugin to write cbfs to h5! + # Pilatus_2 writes cbf files -> where do we like to write those! + # scan_dir = self.filewriter._get_scan_directory( + # scan_bundle=1000, scan_number=self.scan_number, leading_zeros=5 + # ) # os.path.join(self.service_cfg["base_path"], scan_dir) + self.destination_path = "/sls/X12SA/data/{self.username}/Data10/pilatus_2/" data_msg = { "source": [ @@ -106,6 +133,7 @@ class PilatusCsaxs(DetectorBase): } ] } + logger.info(data_msg) headers = {"Content-Type": "application/json", "Accept": "application/json"} @@ -141,16 +169,11 @@ class PilatusCsaxs(DetectorBase): if not res.ok: res.raise_for_status() - self._set_acquisition_params( - exp_time=self.exp_time, - readout=self.readout, - num_frames=self.num_frames, - triggermode=self.triggermode, - ) - - return super().stage() - - def unstage(self) -> List[object]: + def _close_file_writer(self) -> None: + """Close the file writer for pilatus_2 + a zmq service is running on xbl-daq-34 that is waiting + for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2 + """ headers = {"Content-Type": "application/json", "Accept": "application/json"} data_msg = [ "zmqWriter", @@ -167,8 +190,6 @@ class PilatusCsaxs(DetectorBase): data=json.dumps(data_msg), headers=headers, ) - # Reset triggermode to internal - self.triggermode = 0 if not res.ok: res.raise_for_status() @@ -176,46 +197,49 @@ class PilatusCsaxs(DetectorBase): res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") if not res.ok: res.raise_for_status() + + def stage(self) -> List[object]: + """stage the detector and file writer""" + # TODO remove once running from BEC + # self._load_scan_metadata() + self.scan_number = 10 + self.exp_time = 0.5 + self.num_frames = 3 + self.mokev = 12 + + self._prep_det() + self._prep_file_writer() + + msg = BECMessage.FileMessage(file_path=self.filename, done=False) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scanID, "pilatus_2"), + msg.dumps(), + ) + + return super().stage() + + def unstage(self) -> List[object]: + """unstage the detector and file writer""" + # Reset to software trigger + self.triggermode = 0 + self._close_file_writer() + # TODO check if acquisition is done and successful! + state = True + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) return super().unstage() - def _set_threshold(self) -> None: - # TODO readout mono, monitor threshold and set it if mokev is different - # mokev = self.device_manager.devices.mokev.obj.read()["mokev"]["value"] - # TODO remove - mokev = 16 - # TODO refactor naming from name, pilatus_2 - pil_threshold = self.cam.threshold_energy.read()["pilatus_2_cam_threshold_energy"]["value"] - if not np.isclose(mokev / 2, pil_threshold, rtol=0.05): - self.cam.threshold_energy.set(mokev / 2) - - def _set_acquisition_params( - self, exp_time: float, readout: float, num_frames: int, triggermode: int - ) -> None: - """set acquisition parameters on the detector - - Args: - exp_time (float): exposure time - readout (float): readout time - num_frames (int): images per scan - triggermode (int): - 0 Internal - 1 Ext. Enable - 2 Ext. Trigger - 3 Mult. Trigger - 4 Alignment - Returns: - None - """ - self.cam.acquire_time.set(exp_time) - self.cam.acquire_period.set(exp_time + readout) - self.cam.num_images.set(num_frames) - self.cam.num_exposures.set(1) - self.cam.trigger_mode.set(triggermode) - def acquire(self) -> None: + """Start acquisition in software trigger mode, + or arm the detector in hardware of the detector + """ self.cam.acquire.set(1) def stop(self, *, success=False) -> None: + """Stop the scan, with camera and file writer""" self.cam.acquire.set(0) self.unstage() super().stop(success=success) From fb8619d0473dbd16ed147503662f7372b3b5d638 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 21 Aug 2023 21:52:48 +0200 Subject: [PATCH 12/53] refactor: class refactoring, with other 2 detectors --- ophyd_devices/epics/devices/falcon_csaxs.py | 139 ++++++++++---------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 74bbaad..372ad68 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -92,81 +92,25 @@ class FalconCsaxs(Device): **kwargs, ) self.device_manager = device_manager - self.username = ( - "e21206" # self.device_manager.producer.get(MessageEndpoints.account()).decode() - ) - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() self.name = name - # TODO meaningful to use FileWriterMixin - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/falcon/"} + self.username = "e21206" + # TODO once running from BEC + # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} self.filewriter = FileWriterMixin(self.service_cfg) - self.num_frames = 0 + self._producer = RedisConnector(["localhost:6379"]).producer() self.readout = 0.003 # 3 ms self._value_pixel_per_buffer = 16 + # TODO create file template from filewriter compile filename self._file_template = f"%s%s_{self.name}.h5" - # TODO localhost:6379 - self._producer = RedisConnector(["localhost:6379"]).producer() - # Init script for falcon + + self.num_frames = 0 self._clean_up() self._init_hdf5_saving() self._init_mapping_mode() - def stage(self) -> List[object]: - # scan_msg = self._get_current_scan_msg() - # self.metadata = { - # "scanID": scan_msg.content["scanID"], - # "RID": scan_msg.content["info"]["RID"], - # "queueID": scan_msg.content["info"]["queueID"], - # } - self.scan_number = 10 # scan_msg.content["info"]["scan_number"] - self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"] - self.num_frames = 3 # scan_msg.content["info"]["num_points"] - # TODO update service config for file path gen.. - But problem with path - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - self.destination_path = os.path.join(self.service_cfg["base_path"]) - self.filename = f"test_{self.scan_number}" - self._prep_mca_acquisition() - - # Filename to Redis - path_to_file = self._file_template % (self.destination_path, self.filename) - msg = BECMessage.FileMessage(file_path=path_to_file, done=False) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), - msg.dumps(), - ) - - # TODO BEC message on where file is going to be written to - - return super().stage() - - def acquire(self) -> None: - self.start_all.set(1) - - def unstage(self) -> List[object]: - # Check number of acquisitions - while not self._check_falcon_done(): - logger.info("Waiting for acquisition to finish, sleeping 0.1s ") - time.sleep(0.1) - # Compare expected vs measured number of pixel - # logger.info( - # f'Falcon: number of measured frames from expected {self.current_pixel.read()}/{self.pixels_per_run.read()}' - # ) - # logger.info( - # "Falcon write file state{self.hdf5.capture.read()}/{self.hdf5.writestatus}" - # ) - # if not self.hdf5.write_status.read()[f'{self.name}_hdf5_write_status']['value'] : - # state = self.hdf5.write_status.read()[f'{self.name}_hdf5'] - - self._clean_up() - msg = BECMessage.FileMessage(file_path=path_to_file, done=True, successful=state) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), - msg.dumps(), - ) - - return super().unstage() - def _clean_up(self) -> None: """Clean up""" self.hdf5.capture.set(0) @@ -190,7 +134,29 @@ class FalconCsaxs(Device): self.auto_pixels_per_buffer.set(0) # 0 Manual 1 Auto self.pixels_per_buffer.set(16) # - def _prep_mca_acquisition(self) -> None: + def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: + msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + return BECMessage.ScanStatusMessage.loads(msg) + + def _load_scan_metadata(self) -> None: + scan_msg = self._get_current_scan_msg() + self.metadata = { + "scanID": scan_msg.content["scanID"], + "RID": scan_msg.content["info"]["RID"], + "queueID": scan_msg.content["info"]["queueID"], + } + self.scanID = scan_msg.content["scanID"] + self.scan_number = scan_msg.content["info"]["scan_number"] + self.exp_time = scan_msg.content["info"]["exp_time"] + self.num_frames = scan_msg.content["info"]["num_points"] + self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + self.device_manager.devices.mokev.read()["mokev"]["value"] + # self.triggermode = scan_msg.content["info"]["trigger_mode"] + self.filepath = self.filewriter.compile_full_filename( + self.scan_number, "falcon", 1000, 5, True + ) + + def _prep_det(self) -> None: """Prepare detector for acquisition""" self.collect_mode.set(1) self.preset_real.set(self.exposure_time) @@ -198,7 +164,11 @@ class FalconCsaxs(Device): self.auto_pixels_per_buffer.set(0) self.pixels_per_buffer.set(self._value_pixel_per_buffer) - # HDF prep + def _prep_file_writer(self) -> None: + """Prep HDF5 weriting""" + # TODO creta filename and destination path from filepath + self.destination_path = os.path.join(self.service_cfg["base_path"]) + self.filename = f"test_{self.scan_number}" self.hdf5.file_path.set(self.destination_path) self.hdf5.file_name.set(self.filename) self.hdf5.file_template.set(self._file_template) @@ -206,8 +176,39 @@ class FalconCsaxs(Device): self.hdf5.file_write_mode.set(2) self.hdf5.capture.set(1) - # Start acquisition - # Check falcon status?? self.state --> 1 for acquiring. + def stage(self) -> List[object]: + """stage the detector and file writer""" + # TODO remove once running from BEC + # self._load_scan_metadata() + self.scan_number = 10 + self.exp_time = 0.5 + self.num_frames = 3 + self.mokev = 12 + + self._prep_det() + self._prep_file_writer() + + msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) + + return super().stage() + + def acquire(self) -> None: + self.start_all.set(1) + + def unstage(self) -> List[object]: + self._clean_up() + # TODO check if acquisition is done and successful! + state = True + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + self.producer.set_and_publish( + MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg.dumps(), + ) + return super().unstage() def _check_falcon_done(self) -> bool: state = self.state.read()[f"{self.name }_state"]["value"] From 39142ffc92440916b6c68beb260222f4dd8a0548 Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 09:42:22 +0200 Subject: [PATCH 13/53] fix: add status update std_daq --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 3d5f3ed..b633fca 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -111,6 +111,15 @@ class Eiger9mCsaxs(DetectorBase): def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" self.std_client = StdDaqClient(url_base=self.std_rest_server_url) + timeout = 0 + # TODO + while not std_status["state"] == "READY": + time.sleep(0.1) + timeout = timeout + 0.1 + if timeout > 2: + logger.info("Timeout of STD") + + # TODO check status after sleep def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) @@ -184,7 +193,16 @@ class Eiger9mCsaxs(DetectorBase): """unstage the detector and file writer""" self.timing_mode = 0 self._close_file_writer() - # TODO file succesfully written? + std_status = self.std_client.get_status() + if ( + not std_status["acquisition"]["message"] == "Completed" + or std_status["acquisition"]["state"] == "FINISHED" + ): + logger.info(std_status) + # TODO add BEC error message + # raise + # TODO check detector stateX12SA-ES-EIGER9M:cam1:DetectorState_RBV + state = True msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) self.producer.set_and_publish( From 49f95e04765e2e4035c030a35272bdb7f06f8d8f Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 14:17:31 +0200 Subject: [PATCH 14/53] feat: bec_scaninfo_mixin class for scaninfo --- .../epics/devices/DelayGeneratorDG645.py | 240 +++++++++++++++++- 1 file changed, 234 insertions(+), 6 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 283aa95..caac4ec 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -5,6 +5,8 @@ Created on Tue Nov 9 16:12:47 2021 @author: mohacsi_i """ +import enum +import time from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind from ophyd import PVPositioner, Signal from ophyd.pseudopos import ( @@ -14,6 +16,92 @@ from ophyd.pseudopos import ( PseudoPositioner, ) +from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector +from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core import bec_logger + +from ophyd_devices.utils.socket import data_shape, data_type + + +logger = bec_logger.logger +DEFAULT_EPICSSIGNAL_VALUE = object() + + +class DDGError(Exception): + pass + + +class DDGConfigSignal(Signal): + def get(self): + self._readback = self.parent.ddg_configs[self.name] + return self._readback + + def put( + self, + value, + connection_timeout=1, + callback=None, + timeout=1, + **kwargs, + ): + """Using channel access, set the write PV to `value`. + + Keyword arguments are passed on to callbacks + + Parameters + ---------- + value : any + The value to set + connection_timeout : float, optional + If not already connected, allow up to `connection_timeout` seconds + for the connection to complete. + use_complete : bool, optional + Override put completion settings + callback : callable + Callback for when the put has completed + timeout : float, optional + Timeout before assuming that put has failed. (Only relevant if + put completion is used.) + """ + + old_value = self.get() + timestamp = time.time() + self.parent.ddg_configs[self.name] = value + super().put(value, timestamp=timestamp, force=True) + self._run_subs( + sub_type=self.SUB_VALUE, + old_value=old_value, + value=value, + timestamp=timestamp, + ) + + def describe(self): + """Provide schema and meta-data for :meth:`~BlueskyInterface.read` + + This keys in the `OrderedDict` this method returns must match the + keys in the `OrderedDict` return by :meth:`~BlueskyInterface.read`. + + This provides schema related information, (ex shape, dtype), the + source (ex PV name), and if available, units, limits, precision etc. + + Returns + ------- + data_keys : OrderedDict + The keys must be strings and the values must be dict-like + with the ``event_model.event_descriptor.data_key`` schema. + """ + if self._readback is DEFAULT_EPICSSIGNAL_VALUE: + val = self.get() + else: + val = self._readback + return { + self.name: { + "source": f"{self.parent.prefix}:{self.name}", + "dtype": data_type(val), + "shape": data_shape(val), + } + } + class DelayStatic(Device): """Static axis for the T0 output channel @@ -85,6 +173,16 @@ class DelayPair(PseudoPositioner): return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) +class TriggerSource(int, enum.Enum): + INTERNAL = 0 + EXT_RISING_EDGE = 1 + EXT_FALLING_EDGE = 2 + SS_EXT_RISING_EDGE = 3 + SS_EXT_FALLING_EDGE = 4 + SINGLE_SHOT = 5 + LINE = 6 + + class DelayGeneratorDG645(Device): """DG645 delay generator @@ -111,6 +209,7 @@ class DelayGeneratorDG645(Device): state = Component(EpicsSignalRO, "EventStatusLI", name="status_register") status = Component(EpicsSignalRO, "StatusSI", name="status") + clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") # Front Panel channelT0 = Component(DelayStatic, "T0", name="T0") @@ -155,10 +254,6 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) - - # Command PVs - # arm = Component(EpicsSignal, "TriggerDelayBO", name="arm", kind=Kind.omitted) - # Burst mode burstMode = Component( EpicsSignal, "BurstModeBI", write_pv="BurstModeBO", name="burstmode", kind=Kind.config @@ -176,15 +271,148 @@ class DelayGeneratorDG645(Device): EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config ) + delta_delay = Component(DDGConfigSignal, name="delta_delay", kind="config") + delta_width = Component(DDGConfigSignal, name="delta_width", kind="config") + delta_triggers = Component(DDGConfigSignal, name="delta_triggers", kind="config") + polarity = Component(DDGConfigSignal, name="polarity", kind="config") + amplitude = Component(DDGConfigSignal, name="amplitude", kind="config") + offset = Component(DDGConfigSignal, name="offset", kind="config") + thres_trig_level = Component(DDGConfigSignal, name="thres_trig_level", kind="config") + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + **kwargs, + ): + """_summary_ + + Args: + name (_type_): _description_ + prefix (str, optional): _description_. Defaults to "". + kind (_type_, optional): _description_. Defaults to None. + read_attrs (_type_, optional): _description_. Defaults to None. + configuration_attrs (_type_, optional): _description_. Defaults to None. + parent (_type_, optional): _description_. Defaults to None. + device_manager (_type_, optional): _description_. Defaults to None. + polarity (_type_, optional): _description_. Defaults to None. + amplitude (_type_, optional): _description_. Defaults to None. + offset (_type_, optional): _description_. Defaults to None. + thres_trig_level (_type_, optional): _description_. Defaults to None. + delta_delay (_type_, float): Add delay for triggering in software trigger mode to allow fast shutter to open. Defaults to 0. + delta_width (_type_, float): Add width to fast shutter signal to make sure its open during acquisition. Defaults to 0. + delta_triggers (_type_, int): Add additional triggers to burst mode (mcs card needs +1 triggers per line). Defaults to 0. + """ + self.ddg_configs = { + f"{name}_delta_delay": 0, + f"{name}_delta_width": 0, + f"{name}_delta_triggers": 0, + f"{name}_polarity": 1, + f"{name}_amplitude": 2.5, # half amplitude -> 5V peak signal + f"{name}_offset": 0, + f"{name}_thres_trig_level": 1.75, # -> 3.5V + } + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.device_manager = device_manager + self._producer = self.device_manager.producer + self.wait_for_connection() + self._init_ddg() + self._ddg_is_okay() + + def _set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source to value of list below, or string + Accepts integer 0-6 or TriggerSource.* with * + INTERNAL = 0 + EXT_RISING_EDGE = 1 + EXT_FALLING_EDGE = 2 + SS_EXT_RISING_EDGE = 3 + SS_EXT_FALLING_EDGE = 4 + SINGLE_SHOT = 5 + LINE = 6 + """ + value = int(trigger_source) + self.source.set(value) + + def _ddg_is_okay(self, raise_on_error=False) -> None: + status = self.status.read()[self.status.name]["value"] + if status != "STATUS OK" and not raise_on_error: + logger.warning(f"DDG returns {status}, trying to clear ERROR") + self.clear_error() + time.sleep(1) + self._ddg_is_okay(rais_on_error=True) + elif status != "STATUS OK": + raise DDGError(f"DDG failed to start with status: {status}") + + def _init_ddg_pol_allchannels(self, polarity: int = 1) -> None: + """Set Polarity for all channels (including T0) upon init + Args: + polarity: int | 0 negative, 1 positive defaults to 1 + """ + self.channelT0.polarity.set(polarity) + self.channelAB.io.polarity.set(polarity) + self.channelCD.io.polarity.set(polarity) + self.channelEF.io.polarity.set(polarity) + self.channelGH.io.polarity.set(polarity) + + def _init_ddg_amp_allchannels(self, amplitude: float = 2.5) -> None: + """Set amplitude for all channels (including T0) upon init + Args: + amplitude: float | defaults to 2.5 (value is equivalent to half amplitude -> 5V difference between low and high) + """ + # TODO add check for range!! + self.channelT0.amplitude.set(amplitude) + self.channelAB.io.amplitude.set(amplitude) + self.channelCD.io.amplitude.set(amplitude) + self.channelEF.io.amplitude.set(amplitude) + self.channelGH.io.amplitude.set(amplitude) + + def _init_ddg_offset_allchannels(self, offset: float = 0) -> None: + """Set offset for all channels (including T0) upon init + Args: + offset: float | defaults to 0 + """ + # TODO add check for range!! + self.channelT0.offset.set(offset) + self.channelAB.io.offset.set(offset) + self.channelCD.io.offset.set(offset) + self.channelEF.io.offset.set(offset) + self.channelGH.io.offset.set(offset) + + def _cleanup_ddg(self) -> None: + self._set_trigger(TriggerSource.SINGLE_SHOT) + + def _init_ddg(self) -> None: + self._init_ddg_pol_allchannels(self.polarity.get()) + self._init_ddg_amp_allchannels(self.amplitude.get()) + self._init_ddg_offset_allchannels(self.offset.get()) + self._set_trigger(TriggerSource.SINGLE_SHOT) + self.level.set(self.thres_trig_level.get()) + + # TODO add delta_delay, delta_width, delta triggers! + def stage(self): """Trigger the generator by arming to accept triggers""" # TODO check PV TriggerDelayBO, seems to be a bug in the IOC - # self.arm.write(1).wait() + super().stage() def unstage(self): """Stop the trigger generator from accepting triggers""" - # self.arm.write(0).wait() + self._set_trigger(TriggerSource.SINGLE_SHOT) super().stage() def burstEnable(self, count, delay, period, config="all"): From 6ee819de53d39d8d14a4c4df29b0781f83f930ec Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 14:18:35 +0200 Subject: [PATCH 15/53] fix: use bec_scaninfo_mixin in ophyd class --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 62 ++++++++------------ 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index b633fca..d656bac 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,3 +1,4 @@ +import time from typing import List import numpy as np @@ -6,12 +7,14 @@ from ophyd import CamBase, DetectorBase from ophyd import ADComponent as ADCpt from ophyd.areadetector.plugins import FileBase -from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector +from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger from std_daq_client import StdDaqClient +from .bec_scaninfo_mixin import BecScaninfoMixin + logger = bec_logger.logger @@ -91,16 +94,17 @@ class Eiger9mCsaxs(DetectorBase): ) self.device_manager = device_manager self.name = name - self.username = "e21206" + self.scaninfo.username = "e21206" # TODO once running from BEC - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - + # self.scaninfo.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + self.wait_for_connection() # Make sure to be connected before talking to PVs self._init_eiger9m() self._init_standard_daq() - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/data/"} self.filewriter = FileWriterMixin(self.service_cfg) - self._producer = RedisConnector(["localhost:6379"]).producer() + self.scaninfo = BecScaninfoMixin(device_manager) + self._producer = self.device_manager.producer self.readout = 0.003 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -112,8 +116,8 @@ class Eiger9mCsaxs(DetectorBase): self.std_rest_server_url = "http://xbl-daq-29:5000" self.std_client = StdDaqClient(url_base=self.std_rest_server_url) timeout = 0 - # TODO - while not std_status["state"] == "READY": + # TODO check std_daq status + while not self.std_client.std_status["state"] == "READY": time.sleep(0.1) timeout = timeout + 0.1 if timeout > 2: @@ -121,27 +125,6 @@ class Eiger9mCsaxs(DetectorBase): # TODO check status after sleep - def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return BECMessage.ScanStatusMessage.loads(msg) - - def _load_scan_metadata(self) -> None: - scan_msg = self._get_current_scan_msg() - self.metadata = { - "scanID": scan_msg.content["scanID"], - "RID": scan_msg.content["info"]["RID"], - "queueID": scan_msg.content["info"]["queueID"], - } - self.scanID = scan_msg.content["scanID"] - self.scan_number = scan_msg.content["info"]["scan_number"] - self.exp_time = scan_msg.content["info"]["exp_time"] - self.num_frames = scan_msg.content["info"]["num_points"] - self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - # self.triggermode = scan_msg.content["info"]["trigger_mode"] - self.filepath = self.filewriter.compile_full_filename( - self.scan_number, "eiger", 1000, 5, True - ) - def _prep_det(self) -> None: self._set_det_threshold() self._set_acquisition_params() @@ -153,16 +136,19 @@ class Eiger9mCsaxs(DetectorBase): self.cam.threshold_energy.set(self.mokev / 2) def _set_acquisition_params(self) -> None: - self.cam.acquire_time.set(self.exp_time) - self.cam.acquire_period.set(self.exp_time + self.readout) - self.cam.num_images.set(self.num_frames) + self.cam.acquire_time.set(self.scaninfo.exp_time) + self.cam.acquire_period.set(self.scaninfo.exp_time + self.readout) + self.cam.num_images.set(self.scaninfo.num_frames) self.cam.num_exposures.set(1) # trigger_mode vs timing mode ??!! self.cam.timing_mode.set(self.triggermode) def _prep_file_writer(self) -> None: + self.filepath = self.filewriter.compile_full_filename( + self.scaninfo.scan_number, "eiger", 1000, 5, True + ) self.std_client.start_writer_async( - {"output_file": self.filepath, "n_images": self.num_frames} + {"output_file": self.filepath, "n_images": self.scaninfo.num_frames} ) def _close_file_writer(self) -> None: @@ -171,10 +157,10 @@ class Eiger9mCsaxs(DetectorBase): def stage(self) -> List[object]: """stage the detector and file writer""" # TODO remove once running from BEC - # self._load_scan_metadata() - self.scan_number = 10 - self.exp_time = 0.5 - self.num_frames = 3 + # self.scaninfo.load_scan_metadata() + self.scaninfo.scan_number = 10 + self.scaninfo.exp_time = 0.5 + self.scaninfo.num_frames = 3 self.mokev = 12 self.triggermode = 0 @@ -183,7 +169,7 @@ class Eiger9mCsaxs(DetectorBase): msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( - MessageEndpoints.public_file(self.scanID, "eiger9m"), + MessageEndpoints.public_file(self.scaninfo.scanID, "eiger9m"), msg.dumps(), ) From c365b8e9543ac0eca3bc3da34f662422e7daeef7 Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 17:56:41 +0200 Subject: [PATCH 16/53] fix: stepscan logic implemented in ddg --- .../epics/devices/DelayGeneratorDG645.py | 102 +++++++++++------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index caac4ec..f51576a 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -1,12 +1,6 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Nov 9 16:12:47 2021 - -@author: mohacsi_i -""" - import enum import time +from typing import Any, List from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind from ophyd import PVPositioner, Signal from ophyd.pseudopos import ( @@ -15,12 +9,12 @@ from ophyd.pseudopos import ( PseudoSingle, PseudoPositioner, ) +from ophyd_devices.utils.socket import data_shape, data_type +import ophyd_devices.utils.bec_utils as bec_utils -from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector -from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger -from ophyd_devices.utils.socket import data_shape, data_type +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger @@ -145,9 +139,6 @@ class DelayPair(PseudoPositioner): # The pseudo positioner axes delay = Component(PseudoSingle, limits=(0, 2000.0), name="delay") width = Component(PseudoSingle, limits=(0, 2000.0), name="pulsewidth") - # The real delay axes - # ch1 = Component(EpicsSignal, "DelayAI", write_pv="DelayAO", name="ch1", put_complete=True, kind=Kind.config) - # ch2 = Component(EpicsSignal, "DelayAI", write_pv="DelayAO", name="ch2", put_complete=True, kind=Kind.config) ch1 = Component(DummyPositioner, name="ch1") ch2 = Component(DummyPositioner, name="ch2") io = Component(DelayStatic, name="io") @@ -254,6 +245,7 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) + trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") # Burst mode burstMode = Component( EpicsSignal, "BurstModeBI", write_pv="BurstModeBO", name="burstmode", kind=Kind.config @@ -271,13 +263,15 @@ class DelayGeneratorDG645(Device): EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config ) - delta_delay = Component(DDGConfigSignal, name="delta_delay", kind="config") + delay_burst = Component(DDGConfigSignal, name="delay_burst", kind="config") delta_width = Component(DDGConfigSignal, name="delta_width", kind="config") - delta_triggers = Component(DDGConfigSignal, name="delta_triggers", kind="config") + additional_triggers = Component(DDGConfigSignal, name="additional_triggers", kind="config") polarity = Component(DDGConfigSignal, name="polarity", kind="config") amplitude = Component(DDGConfigSignal, name="amplitude", kind="config") offset = Component(DDGConfigSignal, name="offset", kind="config") thres_trig_level = Component(DDGConfigSignal, name="thres_trig_level", kind="config") + set_high_on_exposure = Component(DDGConfigSignal, name="set_high_on_exposure", kind="config") + set_high_on_stage = Component(DDGConfigSignal, name="set_high_on_stage", kind="config") def __init__( self, @@ -289,6 +283,7 @@ class DelayGeneratorDG645(Device): configuration_attrs=None, parent=None, device_manager=None, + sim_mode=False, **kwargs, ): """_summary_ @@ -301,6 +296,7 @@ class DelayGeneratorDG645(Device): configuration_attrs (_type_, optional): _description_. Defaults to None. parent (_type_, optional): _description_. Defaults to None. device_manager (_type_, optional): _description_. Defaults to None. + Signals: polarity (_type_, optional): _description_. Defaults to None. amplitude (_type_, optional): _description_. Defaults to None. offset (_type_, optional): _description_. Defaults to None. @@ -310,13 +306,15 @@ class DelayGeneratorDG645(Device): delta_triggers (_type_, int): Add additional triggers to burst mode (mcs card needs +1 triggers per line). Defaults to 0. """ self.ddg_configs = { - f"{name}_delta_delay": 0, + f"{name}_delay_burst": 0, f"{name}_delta_width": 0, - f"{name}_delta_triggers": 0, + f"{name}_additional_triggers": 0, f"{name}_polarity": 1, f"{name}_amplitude": 2.5, # half amplitude -> 5V peak signal f"{name}_offset": 0, f"{name}_thres_trig_level": 1.75, # -> 3.5V + f"{name}_set_high_on_exposure": False, + f"{name}_set_high_on_stage": False, } super().__init__( prefix=prefix, @@ -327,9 +325,16 @@ class DelayGeneratorDG645(Device): parent=parent, **kwargs, ) + if device_manager is None and not sim_mode: + raise DDGError("Add DeviceManager to initialization or init with sim_mode=True") self.device_manager = device_manager - self._producer = self.device_manager.producer - self.wait_for_connection() + if not sim_mode: + self._producer = self.device_manager.producer + else: + self._producer = bec_utils.MockProducer() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] + self.wait_for_connection() # Make sure to be connected before talking to PVs self._init_ddg() self._ddg_is_okay() @@ -362,35 +367,34 @@ class DelayGeneratorDG645(Device): Args: polarity: int | 0 negative, 1 positive defaults to 1 """ - self.channelT0.polarity.set(polarity) - self.channelAB.io.polarity.set(polarity) - self.channelCD.io.polarity.set(polarity) - self.channelEF.io.polarity.set(polarity) - self.channelGH.io.polarity.set(polarity) + self._set_channels("polarity", polarity) def _init_ddg_amp_allchannels(self, amplitude: float = 2.5) -> None: """Set amplitude for all channels (including T0) upon init Args: amplitude: float | defaults to 2.5 (value is equivalent to half amplitude -> 5V difference between low and high) """ - # TODO add check for range!! - self.channelT0.amplitude.set(amplitude) - self.channelAB.io.amplitude.set(amplitude) - self.channelCD.io.amplitude.set(amplitude) - self.channelEF.io.amplitude.set(amplitude) - self.channelGH.io.amplitude.set(amplitude) + self._set_channels("amplitude", amplitude) def _init_ddg_offset_allchannels(self, offset: float = 0) -> None: """Set offset for all channels (including T0) upon init Args: offset: float | defaults to 0 """ - # TODO add check for range!! - self.channelT0.offset.set(offset) - self.channelAB.io.offset.set(offset) - self.channelCD.io.offset.set(offset) - self.channelEF.io.offset.set(offset) - self.channelGH.io.offset.set(offset) + self._set_channels("offset", offset) + + def _set_channels(self, signal: str, value: Any, channels: List = None) -> None: + if not channels: + channels = self.all_channels + for chname in channels: + channel = getattr(self, chname, None) + if not channel: + continue + if signal in channel.component_names: + getattr(channel, signal).set(value) + continue + if "io" in channel.component_names and signal in channel.io.component_names: + getattr(channel.io, signal).set(value) def _cleanup_ddg(self) -> None: self._set_trigger(TriggerSource.SINGLE_SHOT) @@ -406,7 +410,24 @@ class DelayGeneratorDG645(Device): def stage(self): """Trigger the generator by arming to accept triggers""" - # TODO check PV TriggerDelayBO, seems to be a bug in the IOC + self.scaninfo.load_scan_metadata() + if self.scaninfo.scan_type == "step": + # define parameters + self._set_trigger(TriggerSource.SINGLE_SHOT) + exp_time = ( + self.delta_width.get() + self.scaninfo.exp_time + ) # TODO add readout+ self.scantype.readout + delay_burst = self.delay_burst.get() + num_burst_cycle = 1 + self.additional_triggers.get() + # set parameters in DDG + self.burstEnable(num_burst_cycle, delay_burst, exp_time, config="first") + self._set_channels("delay", 0) + self._set_channels("width", exp_time) + elif self.scaninfo.scan_type == "fly": + if self.set_high_on_exposure.get(): + ... + else: + raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") super().stage() @@ -415,6 +436,10 @@ class DelayGeneratorDG645(Device): self._set_trigger(TriggerSource.SINGLE_SHOT) super().stage() + def trigger(self) -> None: + if self.scaninfo.scan_type == "step": + self.trigger_shot.set(1).wait() + def burstEnable(self, count, delay, period, config="all"): """Enable the burst mode""" # Validate inputs @@ -441,4 +466,5 @@ class DelayGeneratorDG645(Device): # Automatically connect to test environmenr if directly invoked if __name__ == "__main__": - dgen = DelayGeneratorDG645("X01DA-PC-DGEN:", name="delayer") + dgen = DelayGeneratorDG645("delaygen:DG1:", name="dgen", sim_mode=True) + dgen.stage() From 01c824ecead89a1c83cefacff53bf9f76b02d423 Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 17:57:20 +0200 Subject: [PATCH 17/53] feat: add bec_scaninfo_mixin to repo --- .../epics/devices/bec_scaninfo_mixin.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 ophyd_devices/epics/devices/bec_scaninfo_mixin.py diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py new file mode 100644 index 0000000..552c069 --- /dev/null +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -0,0 +1,46 @@ +import os + +from bec_lib.core import DeviceManagerBase, BECMessage, MessageEndpoints + + +class BecScaninfoMixin: + def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: + self.device_manager = device_manager + self.sim_mode = sim_mode + + def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: + if not self.sim_mode: + msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + return BECMessage.ScanStatusMessage.loads(msg) + + return BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid", + "queueID": "mockqueuid", + "scan_number": 1, + "exp_time": 0.1, + "num_points": 10, + "scan_type": "step", + }, + ) + + def _get_username(self) -> str: + if not self.sim_mode: + return self.device_manager.producer.get(MessageEndpoints.account()).decode() + return os.getlogin() + + def load_scan_metadata(self) -> None: + scan_msg = self._get_current_scan_msg() + self.metadata = { + "scanID": scan_msg.content["scanID"], + "RID": scan_msg.content["info"]["RID"], + "queueID": scan_msg.content["info"]["queueID"], + } + self.scanID = scan_msg.content["scanID"] + self.scan_number = scan_msg.content["info"]["scan_number"] + self.exp_time = scan_msg.content["info"]["exp_time"] + self.num_frames = scan_msg.content["info"]["num_points"] + self.scan_type = scan_msg.content["info"].get("scan_type", "step") + self.username = self._get_username() From 86e93afe28fc91b5c0a773c489d99cf272c52878 Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 23 Aug 2023 17:57:55 +0200 Subject: [PATCH 18/53] feat: add bec_utils to repo for generic functions --- ophyd_devices/utils/bec_utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ophyd_devices/utils/bec_utils.py diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py new file mode 100644 index 0000000..6a944d0 --- /dev/null +++ b/ophyd_devices/utils/bec_utils.py @@ -0,0 +1,8 @@ +from bec_lib.core import bec_logger + +logger = bec_logger.logger + + +class MockProducer: + def set_and_publish(endpoint: str, msgdump: str): + logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") From 3258e3a1c7e799c4d718dc9cb7f5abfcf87e59f3 Mon Sep 17 00:00:00 2001 From: e21206 Date: Thu, 24 Aug 2023 13:09:20 +0200 Subject: [PATCH 19/53] fix: add flyscan option --- .../epics/devices/DelayGeneratorDG645.py | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index f51576a..375e6d4 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -414,9 +414,7 @@ class DelayGeneratorDG645(Device): if self.scaninfo.scan_type == "step": # define parameters self._set_trigger(TriggerSource.SINGLE_SHOT) - exp_time = ( - self.delta_width.get() + self.scaninfo.exp_time - ) # TODO add readout+ self.scantype.readout + exp_time = self.delta_width.get() + self.scaninfo.exp_time delay_burst = self.delay_burst.get() num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG @@ -424,20 +422,53 @@ class DelayGeneratorDG645(Device): self._set_channels("delay", 0) self._set_channels("width", exp_time) elif self.scaninfo.scan_type == "fly": + # Prepare FSH DDG if self.set_high_on_exposure.get(): - ... + # define parameters + self._set_trigger(TriggerSource.SINGLE_SHOT) + exp_time = ( + self.delta_width.get() + + self.scaninfo.exp_time * self.scaninfo.num_frames + + self.scaninfo.readout_time * (self.scaninfo.num_frames - 1) + ) + total_exposure = exp_time + delay_burst = self.delay_burst.get() + # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. + num_burst_cycle = 1 + self.additional_triggers.get() + # set parameters in DDG + self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") + self._set_channels("delay", 0) + self._set_channels("width", exp_time) + else: + # define parameters + self._set_trigger(TriggerSource.SINGLE_SHOT) + exp_time = self.delta_width.get() + self.scaninfo.exp_time + total_exposure = exp_time + self.scaninfo.readout_time + delay_burst = self.delay_burst.get() + num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + # set parameters in DDG + self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") + self._set_channels("delay", 0) + self._set_channels("width", exp_time) + else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") + # Check status + self._ddg_is_okay() + super().stage() def unstage(self): """Stop the trigger generator from accepting triggers""" self._set_trigger(TriggerSource.SINGLE_SHOT) - super().stage() + # Check status + self._ddg_is_okay() + super().unstage() def trigger(self) -> None: - if self.scaninfo.scan_type == "step": + # if self.scaninfo.scan_type == "step": + if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() def burstEnable(self, count, delay, period, config="all"): @@ -467,4 +498,8 @@ class DelayGeneratorDG645(Device): # Automatically connect to test environmenr if directly invoked if __name__ == "__main__": dgen = DelayGeneratorDG645("delaygen:DG1:", name="dgen", sim_mode=True) + # dgen. + start = time.time() dgen.stage() + dgen.trigger() + print(f"Time passed for stage and trigger {time.time()-start}s") From 8dda7f30c1e797287ddf52f6448604c1052ce3ce Mon Sep 17 00:00:00 2001 From: e21206 Date: Thu, 24 Aug 2023 13:09:48 +0200 Subject: [PATCH 20/53] fix: add readout time to mock scaninfo --- ophyd_devices/epics/devices/bec_scaninfo_mixin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 552c069..67669ae 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -22,7 +22,8 @@ class BecScaninfoMixin: "scan_number": 1, "exp_time": 0.1, "num_points": 10, - "scan_type": "step", + "readout_time": 3e-3, + "scan_type": "fly", }, ) @@ -43,4 +44,5 @@ class BecScaninfoMixin: self.exp_time = scan_msg.content["info"]["exp_time"] self.num_frames = scan_msg.content["info"]["num_points"] self.scan_type = scan_msg.content["info"].get("scan_type", "step") + self.readout_time = scan_msg.content["info"]["readout_time"] self.username = self._get_username() From c96992798d291773e202caabf421000c74fa79d3 Mon Sep 17 00:00:00 2001 From: e21206 Date: Thu, 24 Aug 2023 13:12:29 +0200 Subject: [PATCH 21/53] refactor: remove some unnecessary test code --- ophyd_devices/epics/devices/DelayGeneratorDG645.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 375e6d4..eddc8a3 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -498,8 +498,8 @@ class DelayGeneratorDG645(Device): # Automatically connect to test environmenr if directly invoked if __name__ == "__main__": dgen = DelayGeneratorDG645("delaygen:DG1:", name="dgen", sim_mode=True) - # dgen. - start = time.time() - dgen.stage() - dgen.trigger() - print(f"Time passed for stage and trigger {time.time()-start}s") + + # start = time.time() + # dgen.stage() + # dgen.trigger() + # print(f"Time passed for stage and trigger {time.time()-start}s") From e8f2f8203934381898d05709e06cd32e66692914 Mon Sep 17 00:00:00 2001 From: e21206 Date: Thu, 24 Aug 2023 13:17:04 +0200 Subject: [PATCH 22/53] refactor: bugfix --- ophyd_devices/epics/devices/DelayGeneratorDG645.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index eddc8a3..4f5536d 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -10,7 +10,7 @@ from ophyd.pseudopos import ( PseudoPositioner, ) from ophyd_devices.utils.socket import data_shape, data_type -import ophyd_devices.utils.bec_utils as bec_utils +from ophyd_devices.utils import bec_utils as bec_utils from bec_lib.core import bec_logger From 17347ac93032c9b57247d9f565f638340a9973af Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:29:03 +0200 Subject: [PATCH 23/53] fix: adjusted delaygen --- .../epics/devices/DelayGeneratorDG645.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 4f5536d..ab3a316 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -310,9 +310,9 @@ class DelayGeneratorDG645(Device): f"{name}_delta_width": 0, f"{name}_additional_triggers": 0, f"{name}_polarity": 1, - f"{name}_amplitude": 2.5, # half amplitude -> 5V peak signal + f"{name}_amplitude": 4.5, f"{name}_offset": 0, - f"{name}_thres_trig_level": 1.75, # -> 3.5V + f"{name}_thres_trig_level": 2.5, f"{name}_set_high_on_exposure": False, f"{name}_set_high_on_stage": False, } @@ -332,6 +332,7 @@ class DelayGeneratorDG645(Device): self._producer = self.device_manager.producer else: self._producer = bec_utils.MockProducer() + self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] self.wait_for_connection() # Make sure to be connected before talking to PVs @@ -369,10 +370,10 @@ class DelayGeneratorDG645(Device): """ self._set_channels("polarity", polarity) - def _init_ddg_amp_allchannels(self, amplitude: float = 2.5) -> None: + def _init_ddg_amp_allchannels(self, amplitude: float = 5) -> None: """Set amplitude for all channels (including T0) upon init Args: - amplitude: float | defaults to 2.5 (value is equivalent to half amplitude -> 5V difference between low and high) + amplitude: float | defaults to 5 """ self._set_channels("amplitude", amplitude) @@ -420,7 +421,8 @@ class DelayGeneratorDG645(Device): # set parameters in DDG self.burstEnable(num_burst_cycle, delay_burst, exp_time, config="first") self._set_channels("delay", 0) - self._set_channels("width", exp_time) + # Set burst length to half of the experimental time! + self._set_channels("width", exp_time / 2) elif self.scaninfo.scan_type == "fly": # Prepare FSH DDG if self.set_high_on_exposure.get(): @@ -438,7 +440,8 @@ class DelayGeneratorDG645(Device): # set parameters in DDG self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") self._set_channels("delay", 0) - self._set_channels("width", exp_time) + # Set burst length to half of the experimental time! + self._set_channels("width", exp_time / 2) else: # define parameters self._set_trigger(TriggerSource.SINGLE_SHOT) @@ -449,7 +452,8 @@ class DelayGeneratorDG645(Device): # set parameters in DDG self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") self._set_channels("delay", 0) - self._set_channels("width", exp_time) + # Set burst length to half of the experimental time! + self._set_channels("width", exp_time / 2) else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") From 7de0ff236c4afe14dbe16540653183af25353e36 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:29:33 +0200 Subject: [PATCH 24/53] refactor: updated scaninfo mix --- ophyd_devices/epics/devices/bec_scaninfo_mixin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 67669ae..b9b3ac3 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -20,9 +20,9 @@ class BecScaninfoMixin: "RID": "mockrid", "queueID": "mockqueuid", "scan_number": 1, - "exp_time": 0.1, - "num_points": 10, - "readout_time": 3e-3, + "exp_time": 26e-3, + "num_points": 10000, + "readout_time": 2e-3, "scan_type": "fly", }, ) From 053f1d91814905fa3fa20a79f9a986ac19942c7b Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:30:45 +0200 Subject: [PATCH 25/53] refactor: eiger9m updates, operation in gating mode --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 203 ++++++++++++++----- 1 file changed, 150 insertions(+), 53 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index d656bac..99f8d89 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,5 +1,6 @@ +import enum import time -from typing import List +from typing import Any, List import numpy as np from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV @@ -10,20 +11,26 @@ from ophyd.areadetector.plugins import FileBase from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger +from ophyd_devices.utils import bec_utils as bec_utils from std_daq_client import StdDaqClient -from .bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger +class EigerError(Exception): + pass + + class SlsDetectorCam(CamBase, FileBase): detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") setting = ADCpt(EpicsSignalWithRBV, "Setting") delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits") bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") num_gates = ADCpt(EpicsSignalWithRBV, "NumGates") @@ -57,6 +64,27 @@ class SlsDetectorCam(CamBase, FileBase): json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") +class TriggerSource(int, enum.Enum): + AUTO = 0 + TRIGGER = 1 + GATING = 2 + BURST_TRIGGER = 3 + + +class DetectorState(int, enum.Enum): + IDLE = 0 + ERROR = 1 + WAITING = 2 + FINISHED = 3 + TRANSMITTING = 4 + RUNNING = 5 + STOPPED = 6 + STILL_WAITING = 7 + INITIALIZING = 8 + DISCONNECTED = 9 + ABORTED = 10 + + class Eiger9mCsaxs(DetectorBase): """Eiger 9M detector for CSAXS @@ -81,6 +109,7 @@ class Eiger9mCsaxs(DetectorBase): configuration_attrs=None, parent=None, device_manager=None, + sim_mode=False, **kwargs, ): super().__init__( @@ -92,77 +121,128 @@ class Eiger9mCsaxs(DetectorBase): parent=parent, **kwargs, ) - self.device_manager = device_manager + if device_manager is None and not sim_mode: + raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") + self.name = name - self.scaninfo.username = "e21206" - # TODO once running from BEC - # self.scaninfo.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() self.wait_for_connection() # Make sure to be connected before talking to PVs + if not sim_mode: + self._producer = self.device_manager.producer + self.device_manager = device_manager + else: + self._producer = bec_utils.MockProducer() + self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + # TODO + self.scaninfo.username = "e21206" + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.filewriter = FileWriterMixin(self.service_cfg) + self.reduce_readout = 1e-3 # 3 ms + self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + self.mokev = 12 self._init_eiger9m() self._init_standard_daq() - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/data/"} - self.filewriter = FileWriterMixin(self.service_cfg) - self.scaninfo = BecScaninfoMixin(device_manager) - self._producer = self.device_manager.producer - self.readout = 0.003 # 3 ms - self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + # self.mokev = self.device_manager.devices.mokev.read()[ + # self.device_manager.devices.mokev.name + # ]["value"] def _init_eiger9m(self) -> None: """Init parameters for Eiger 9m""" - pass + self._set_det_threshold() + self._set_trigger(TriggerSource.GATING) + self.cam.acquire.set(0) + + def _update_std_cfg(self, cfg_key: str, value: Any) -> None: + cfg = self.std_client.get_config() + old_value = cfg.get(cfg_key) + if old_value is None: + raise EigerError( + f"Tried to change entry for key {cfg_key} in std_config that does not exist" + ) + if not isinstance(value, type(old_value)): + raise EigerError( + f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}" + ) + cfg.update({cfg_key: value}) + logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" self.std_client = StdDaqClient(url_base=self.std_rest_server_url) + self.std_client.stop_writer() timeout = 0 - # TODO check std_daq status - while not self.std_client.std_status["state"] == "READY": + self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) + while not self.std_client.get_status()["state"] == "READY": time.sleep(0.1) timeout = timeout + 0.1 if timeout > 2: - logger.info("Timeout of STD") - - # TODO check status after sleep + raise EigerError( + f"Std client not in READY state, returns: {self.std_client.get_status()}" + ) def _prep_det(self) -> None: self._set_det_threshold() self._set_acquisition_params() + self._set_trigger(TriggerSource.TRIGGER) def _set_det_threshold(self) -> None: # threshold_energy PV exists on Eiger 9M? + factor = 1 + if self.cam.threshold_energy._metadata["units"] == "eV": + factor = 1000 + setp_energy = int(self.mokev * factor) + energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] + if setp_energy != energy: + self.cam.beam_energy.set(setp_energy) # .wait() threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] - if not np.isclose(self.mokev / 2, threshold, rtol=0.05): - self.cam.threshold_energy.set(self.mokev / 2) + if not np.isclose(setp_energy / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(setp_energy / 2) # .wait() def _set_acquisition_params(self) -> None: - self.cam.acquire_time.set(self.scaninfo.exp_time) - self.cam.acquire_period.set(self.scaninfo.exp_time + self.readout) - self.cam.num_images.set(self.scaninfo.num_frames) - self.cam.num_exposures.set(1) - # trigger_mode vs timing mode ??!! - self.cam.timing_mode.set(self.triggermode) + # self.cam.acquire_time.set(self.scaninfo.exp_time) + # Set acquisition parameters slightly shorter then cycle + # self.cam.acquire_period.set( + # self.scaninfo.exp_time + (self.scaninfo.readout_time - self.reduce_readout) + # ) + self.cam.num_cycles.set(self.scaninfo.num_frames) + self.cam.num_frames.set(1) + + def _set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector, either directly to value or TriggerSource.* with + AUTO = 0 + TRIGGER = 1 + GATING = 2 + BURST_TRIGGER = 3 + """ + value = int(trigger_source) + self.cam.timing_mode.set(value) def _prep_file_writer(self) -> None: self.filepath = self.filewriter.compile_full_filename( - self.scaninfo.scan_number, "eiger", 1000, 5, True + self.scaninfo.scan_number, "eiger.h5", 1000, 5, True ) + logger.info(f" std_daq output filepath {self.filepath}") self.std_client.start_writer_async( {"output_file": self.filepath, "n_images": self.scaninfo.num_frames} ) + logger.info("Waiting for std daq to be armed") + while True: + det_ctrl = self.std_client.get_status()["acquisition"]["state"] + if det_ctrl == "WAITING_IMAGES": + break + time.sleep(0.005) def _close_file_writer(self) -> None: + self.std_client.stop_writer() pass def stage(self) -> List[object]: """stage the detector and file writer""" - # TODO remove once running from BEC - # self.scaninfo.load_scan_metadata() - self.scaninfo.scan_number = 10 - self.scaninfo.exp_time = 0.5 - self.scaninfo.num_frames = 3 - self.mokev = 12 - self.triggermode = 0 + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.read()[ + self.device_manager.devices.mokev.name + ]["value"] self._prep_det() self._prep_file_writer() @@ -172,32 +252,43 @@ class Eiger9mCsaxs(DetectorBase): MessageEndpoints.public_file(self.scaninfo.scanID, "eiger9m"), msg.dumps(), ) + self.arm_acquisition() + logger.info("Waiting for detector to be armed") + while True: + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] + if det_ctrl == int(DetectorState.RUNNING): + break + time.sleep(0.005) + logger.info("Detector is armed") return super().stage() def unstage(self) -> List[object]: """unstage the detector and file writer""" - self.timing_mode = 0 - self._close_file_writer() - std_status = self.std_client.get_status() - if ( - not std_status["acquisition"]["message"] == "Completed" - or std_status["acquisition"]["state"] == "FINISHED" - ): - logger.info(std_status) - # TODO add BEC error message - # raise - # TODO check detector stateX12SA-ES-EIGER9M:cam1:DetectorState_RBV + logger.info("Waiting for eiger9M to return from acquisition") + while True: + det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] + if det_ctrl == 0: + break + time.sleep(0.005) - state = True - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), - msg.dumps(), - ) + logger.info("Waiting for std daq to receive images") + while True: + det_ctrl = self.std_client.get_status()["acquisition"]["state"] + if det_ctrl == "FINISHED": + break + time.sleep(0.005) + # Message to BEC + # state = True + + # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + # self._producer.set_and_publish( + # MessageEndpoints.public_file(self.metadata["scanID"], self.name), + # msg.dumps(), + # ) return super().unstage() - def acquire(self) -> None: + def arm_acquisition(self) -> None: """Start acquisition in software trigger mode, or arm the detector in hardware of the detector """ @@ -206,7 +297,13 @@ class Eiger9mCsaxs(DetectorBase): def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" self.cam.acquire.set(0) - self.std_client.stop_writer() + self._close_file_writer() self.unstage() super().stop(success=success) self._stopped = True + + +# Automatically connect to test environmenr if directly invoked +if __name__ == "__main__": + eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) + eiger.stage() From 96a131d3743c8d62aaac309868ac1309d83fe9aa Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:31:09 +0200 Subject: [PATCH 26/53] feat: adding mcs card to repository --- ophyd_devices/epics/devices/mcs_csaxs.py | 229 ++++++++++++++++++++++- 1 file changed, 226 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index a34a140..eaf5afa 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -1,7 +1,59 @@ +import enum +import time +from typing import Any, List +import numpy as np + +from ophyd import EpicsSignal, EpicsSignalRO from ophyd import EpicsSignal, EpicsSignalRO, Component as Cpt, Device -from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDXPMultiElementSystem +from ophyd.mca import EpicsMCARecord from ophyd.scaler import ScalerCH +from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core import bec_loggers +from ophyd_devices.utils import bec_utils as bec_utils + +from std_daq_client import StdDaqClient + +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin + +from ophyd_devices.utils import bec_utils + +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin + + +class MCSError(Exception): + pass + + +class TriggerSource(int, enum.Enum): + MODE0 = 0 + MODE1 = 1 + MODE2 = 2 + MODE3 = 3 + MODE4 = 4 + MODE5 = 5 + MODE6 = 6 + + +class ChannelAdvance(int, enum.Enum): + INTERNAL = 0 + EXTERNAL = 1 + + +class ReadoutMode(int, enum.Enum): + PASSIVE = 0 + EVENT = 1 + IO_INTR = 2 + FREQ_0_1HZ = 3 + FREQ_0_2HZ = 4 + FREQ_0_5HZ = 5 + FREQ_1HZ = 6 + FREQ_2HZ = 7 + FREQ_5HZ = 8 + FREQ_10HZ = 9 + FREQ_100HZ = 10 + class SIS38XX(Device): """SIS38XX control""" @@ -17,7 +69,8 @@ class SIS38XX(Device): preset_real = Cpt(EpicsSignal, "PresetReal") elapsed_real = Cpt(EpicsSignal, "ElapsedReal") - read_all = Cpt(EpicsSignal, "ReadAll") + read_mode = Cpt(EpicsSignal, "ReadAll.SCAN") + read_all = Cpt(EpicsSignal, "DoReadAll.VAL") num_use_all = Cpt(EpicsSignal, "NuseAll") current_channel = Cpt(EpicsSignal, "CurrentChannel") dwell = Cpt(EpicsSignal, "Dwell") @@ -40,7 +93,7 @@ class SIS38XX(Device): max_channels = Cpt(EpicsSignalRO, "MaxChannels") -class SIS3820(SIS38XX): +class McsCsaxs(SIS38XX): scaler = Cpt(ScalerCH, "scaler1") mca1 = Cpt(EpicsMCARecord, "mca1") @@ -75,3 +128,173 @@ class SIS3820(SIS38XX): # mca30 = Cpt(EpicsMCARecord, "mca30") # mca31 = Cpt(EpicsMCARecord, "mca31") # mca32 = Cpt(EpicsMCARecord, "mca32") + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + sim_mode=False, + **kwargs, + ): + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + if device_manager is None and not sim_mode: + raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") + + self.name = name + self.wait_for_connection() # Make sure to be connected before talking to PVs + if not sim_mode: + self._producer = self.device_manager.producer + self.device_manager = device_manager + else: + self._producer = bec_utils.MockProducer() + self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + # TODO + self.scaninfo.username = "e21206" + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.filewriter = FileWriterMixin(self.service_cfg) + self._init_mcs() + self._init_standard_daq() + + def _init_mcs(self) -> None: + """Init parameters for mcs card 9m + channel_advance: 0/1 -> internal / external + channel1_source: 0/1 -> int clock / external source + user_led: 0/1 -> off/on + max_output : num of channels 0...32, uncomment top for more than 5 + input_mode: operation mode -> Mode 3 for external trigger, check manual for more info + input_polarity: triggered between falling and falling edge -> use inverted signal from ddg + """ + self.channel_advance.set(ChannelAdvance.EXTERNAL) + self.channel1_source.set(ChannelAdvance.INTERNAL) + self.user_led.set(0) + self.mux_output.set(5) + self._set_trigger(TriggerSource.MODE3) + self.input_polarity.set(0) + + def _prep_det(self) -> None: + self._set_acquisition_params() + self._set_trigger(TriggerSource.MODE3) + + def _set_acquisition_params(self) -> None: + # max number of readings is limited to 10000, but device can be reseted.. needs to be included on scan level + self.num_use_all.set(self.scaninfo.num_frames) + self.preset_real.set(0) + + self.count_on_start.set(0) + + self.cam.num_frames.set(1) + + def _set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector, either directly to value or TriggerSource.* with + AUTO = 0 + TRIGGER = 1 + GATING = 2 + BURST_TRIGGER = 3 + """ + value = int(trigger_source) + self.cam.timing_mode.set(value) + + def _prep_readout(self) -> None: + """Set readout mode of mcs card + Check ReadoutMode class for more information about options + """ + self.read_mode.set(ReadoutMode.PASSIVE) + + def _readout(self) -> List: + self.read_all.set(1) + readback = [] + readback.append(self.scaler.read()[self.scaler.name]["value"]) + for ii in range(1, self.mux_output.get() + 1): + readback.append(self._readout_mca(ii)) + return readback + + def _readout_mca(self, num: int) -> List[List]: + signal = f"mca{num}" + if signal in self.component_names: + return getattr(self, signal).read()[getattr(self, signal).name]["value"] + + def stage(self) -> List[object]: + """stage the detector and file writer""" + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.read()[ + self.device_manager.devices.mokev.name + ]["value"] + + self._prep_det() + self._prep_file_writer() + + msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, "eiger9m"), + msg.dumps(), + ) + self.arm_acquisition() + logger.info("Waiting for detector to be armed") + while True: + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] + if det_ctrl == int(DetectorState.RUNNING): + break + time.sleep(0.005) + logger.info("Detector is armed") + + return super().stage() + + def unstage(self) -> List[object]: + """unstage the detector and file writer""" + logger.info("Waiting for eiger9M to return from acquisition") + while True: + det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] + if det_ctrl == 0: + break + time.sleep(0.005) + + logger.info("Waiting for std daq to receive images") + while True: + det_ctrl = self.std_client.get_status()["acquisition"]["state"] + if det_ctrl == "FINISHED": + break + time.sleep(0.005) + # Message to BEC + # state = True + + # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + # self._producer.set_and_publish( + # MessageEndpoints.public_file(self.metadata["scanID"], self.name), + # msg.dumps(), + # ) + return super().unstage() + + def arm_acquisition(self) -> None: + """Start acquisition in software trigger mode, + or arm the detector in hardware of the detector + """ + self.cam.acquire.set(1) + + def stop(self, *, success=False) -> None: + """Stop the scan, with camera and file writer""" + self.cam.acquire.set(0) + self._close_file_writer() + self.unstage() + super().stop(success=success) + self._stopped = True + + +# Automatically connect to test environmenr if directly invoked +if __name__ == "__main__": + eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) + eiger.stage() From b91f8dbc6854cf46d1d504610855d50563a8df36 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:31:40 +0200 Subject: [PATCH 27/53] fix: pil300k device, pending readout --- ophyd_devices/epics/devices/pilatus_csaxs.py | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 06eec96..782e828 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -84,16 +84,20 @@ class PilatusCsaxs(DetectorBase): self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() self.device_manager.devices.mokev.read()["mokev"]["value"] # self.triggermode = scan_msg.content["info"]["trigger_mode"] - self.filename = self.filewriter.compile_full_filename( - self.scan_number, "pilatus_2", 1000, 5, True - ) + # self.filename = self.filewriter.compile_full_filename( + # self.scan_number, "pilatus_2", 1000, 5, True + # ) + # TODO fix with BEC running + # self.filename = '/sls/X12SA/Data10/e21206/data/test.h5' def _prep_det(self) -> None: + # TODO slow reaction, seemed to have timeout. self._set_det_threshold() self._set_acquisition_params() def _set_det_threshold(self) -> None: threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + # threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]['value'] if not np.isclose(self.mokev / 2, threshold, rtol=0.05): self.cam.threshold_energy.set(self.mokev / 2) @@ -210,11 +214,11 @@ class PilatusCsaxs(DetectorBase): self._prep_det() self._prep_file_writer() - msg = BECMessage.FileMessage(file_path=self.filename, done=False) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scanID, "pilatus_2"), - msg.dumps(), - ) + # msg = BECMessage.FileMessage(file_path=self.filename, done=False) + # self._producer.set_and_publish( + # MessageEndpoints.public_file(self.scanID, "pilatus_2"), + # msg.dumps(), + # ) return super().stage() @@ -225,11 +229,11 @@ class PilatusCsaxs(DetectorBase): self._close_file_writer() # TODO check if acquisition is done and successful! state = True - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), - msg.dumps(), - ) + # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + # self.producer.set_and_publish( + # MessageEndpoints.public_file(self.metadata["scanID"], self.name), + # msg.dumps(), + # ) return super().unstage() def acquire(self) -> None: From cc6c8cb41bc6e3388a580adeee0af8a1c7dbca27 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:32:20 +0200 Subject: [PATCH 28/53] fix: sgalil scan --- ophyd_devices/galil/sgalil_ophyd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index 7caf301..9c1601f 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -284,7 +284,7 @@ class GalilController(Controller): n_samples = int(interval_y * interval_x) # Hard coded to maximum offset of 0.1mm to avoid long motions. - self.socket_put_and_receive(f"off={(0*0.1/2*1000):f}") + self.socket_put_and_receive(f"off={(0):f}") self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}") self.socket_put_and_receive( f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}" From ed0ef338eb606977993d45c98421ebde0f477927 Mon Sep 17 00:00:00 2001 From: e21206 Date: Mon, 28 Aug 2023 22:33:30 +0200 Subject: [PATCH 29/53] fix: bec_utils mixin --- ophyd_devices/utils/bec_utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 6a944d0..d30200d 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -1,8 +1,29 @@ +import time + from bec_lib.core import bec_logger + logger = bec_logger.logger class MockProducer: - def set_and_publish(endpoint: str, msgdump: str): + def set_and_publish(self, endpoint: str, msgdump: str): logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") + + +class MockDeviceManager: + def __init__(self) -> None: + self.devices = devices() + + +class devices: + def __init__(self): + self.mokev = mokev() + + +class mokev: + def __init__(self): + self.name = "mock_mokev" + + def read(self): + return {self.name: {"value": 12.4, "timestamp": time.time()}} From 7c45682367c363207257fff7b6ce53ffee1449df Mon Sep 17 00:00:00 2001 From: e21206 Date: Tue, 29 Aug 2023 13:21:02 +0200 Subject: [PATCH 30/53] fix: running ophyd for mcs card, pending fix mcs_read_all epics channel --- ophyd_devices/epics/devices/mcs_csaxs.py | 108 ++++++++++------------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index eaf5afa..9a59302 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -8,18 +8,15 @@ from ophyd import EpicsSignal, EpicsSignalRO, Component as Cpt, Device from ophyd.mca import EpicsMCARecord from ophyd.scaler import ScalerCH -from bec_lib.core import BECMessage, MessageEndpoints -from bec_lib.core.file_utils import FileWriterMixin -from bec_lib.core import bec_loggers -from ophyd_devices.utils import bec_utils as bec_utils - -from std_daq_client import StdDaqClient - from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin - from ophyd_devices.utils import bec_utils -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin +from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core.file_utils import FileWriterMixin + +from bec_lib.core import bec_logger + +logger = bec_logger.logger class MCSError(Exception): @@ -70,7 +67,7 @@ class SIS38XX(Device): elapsed_real = Cpt(EpicsSignal, "ElapsedReal") read_mode = Cpt(EpicsSignal, "ReadAll.SCAN") - read_all = Cpt(EpicsSignal, "DoReadAll.VAL") + read_all = Cpt(EpicsSignal, "DoReadAll.VAL", trigger_value=1) num_use_all = Cpt(EpicsSignal, "NuseAll") current_channel = Cpt(EpicsSignal, "CurrentChannel") dwell = Cpt(EpicsSignal, "Dwell") @@ -168,7 +165,6 @@ class McsCsaxs(SIS38XX): self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self._init_mcs() - self._init_standard_daq() def _init_mcs(self) -> None: """Init parameters for mcs card 9m @@ -185,6 +181,7 @@ class McsCsaxs(SIS38XX): self.mux_output.set(5) self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) + self.count_on_start.set(0) def _prep_det(self) -> None: self._set_acquisition_params() @@ -195,19 +192,11 @@ class McsCsaxs(SIS38XX): self.num_use_all.set(self.scaninfo.num_frames) self.preset_real.set(0) - self.count_on_start.set(0) - - self.cam.num_frames.set(1) - def _set_trigger(self, trigger_source: TriggerSource) -> None: - """Set trigger source for the detector, either directly to value or TriggerSource.* with - AUTO = 0 - TRIGGER = 1 - GATING = 2 - BURST_TRIGGER = 3 - """ + """7 Modes, see TriggerSource + Mode3 for cSAXS""" value = int(trigger_source) - self.cam.timing_mode.set(value) + self.input_mode.set(value) def _prep_readout(self) -> None: """Set readout mode of mcs card @@ -215,60 +204,54 @@ class McsCsaxs(SIS38XX): """ self.read_mode.set(ReadoutMode.PASSIVE) - def _readout(self) -> List: + def _read_mcs_card(self) -> None: + # TODO how to properly trigger the readout!!! self.read_all.set(1) + + def readout_data(self) -> List: + self._read_mcs_card() readback = [] - readback.append(self.scaler.read()[self.scaler.name]["value"]) - for ii in range(1, self.mux_output.get() + 1): - readback.append(self._readout_mca(ii)) + for ii in range(1, int(self.mux_output.read()[self.mux_output.name]["value"]) + 1): + readback.append(self._readout_mca_channels(ii)) return readback - def _readout_mca(self, num: int) -> List[List]: + def _readout_mca_channels(self, num: int) -> List[List]: signal = f"mca{num}" if signal in self.component_names: - return getattr(self, signal).read()[getattr(self, signal).name]["value"] + readback = f"{getattr(self, signal).name}_spectrum" + return getattr(self, signal).read()[readback]["value"] def stage(self) -> List[object]: """stage the detector and file writer""" self.scaninfo.load_scan_metadata() - self.mokev = self.device_manager.devices.mokev.read()[ - self.device_manager.devices.mokev.name - ]["value"] - self._prep_det() - self._prep_file_writer() + self._prep_readout() - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, "eiger9m"), - msg.dumps(), - ) + # msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + # self._producer.set_and_publish( + # MessageEndpoints.public_file(self.scaninfo.scanID, "mcs_csaxs"), + # msg.dumps(), + # ) self.arm_acquisition() - logger.info("Waiting for detector to be armed") + logger.info("Waiting for mcs to be armed") while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] - if det_ctrl == int(DetectorState.RUNNING): + det_ctrl = self.acquiring.read()[self.acquiring.name]["value"] + if det_ctrl == 1: break time.sleep(0.005) - logger.info("Detector is armed") + logger.info("mcs is ready and running") return super().stage() def unstage(self) -> List[object]: - """unstage the detector and file writer""" - logger.info("Waiting for eiger9M to return from acquisition") + """unstage""" + logger.info("Waiting for mcs to finish acquisition") while True: - det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] + det_ctrl = self.acquiring.read()[self.acquiring.name]["value"] if det_ctrl == 0: break time.sleep(0.005) - - logger.info("Waiting for std daq to receive images") - while True: - det_ctrl = self.std_client.get_status()["acquisition"]["state"] - if det_ctrl == "FINISHED": - break - time.sleep(0.005) + self._read_mcs_card() # Message to BEC # state = True @@ -280,15 +263,20 @@ class McsCsaxs(SIS38XX): return super().unstage() def arm_acquisition(self) -> None: - """Start acquisition in software trigger mode, - or arm the detector in hardware of the detector + """Arm acquisition + Options: + Start: start_all + Erase/Start: erase_start """ - self.cam.acquire.set(1) + self.erase_start.set(1) + # self.start_all.set(1) def stop(self, *, success=False) -> None: - """Stop the scan, with camera and file writer""" - self.cam.acquire.set(0) - self._close_file_writer() + """Stop acquisition + Stop or Stop and Erase + """ + self.stop_all.set(1) + # self.erase_all.set(1) self.unstage() super().stop(success=success) self._stopped = True @@ -296,5 +284,5 @@ class McsCsaxs(SIS38XX): # Automatically connect to test environmenr if directly invoked if __name__ == "__main__": - eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) - eiger.stage() + mcs = McsCsaxs(name="mcs", prefix="X12SA-MCS:", sim_mode=True) + mcs.stage() From d694f6594d0bd81fd62be570142bc2f6b19cf6f4 Mon Sep 17 00:00:00 2001 From: e21206 Date: Tue, 29 Aug 2023 13:21:35 +0200 Subject: [PATCH 31/53] fix: fixed stop command --- ophyd_devices/galil/sgalil_ophyd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index 9c1601f..e1a35a4 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -150,7 +150,9 @@ class GalilController(Controller): return var def stop_all_axes(self) -> str: - return self.socket_put_and_receive(f"XQ#STOP,1") + # return self.socket_put_and_receive(f"XQ#STOP,1") + # Command stops all threads and motors! + return self.socket_put_and_receive(f"AB") def axis_is_referenced(self) -> bool: return bool(float(self.socket_put_and_receive(f"MG allaxref").strip())) @@ -298,6 +300,7 @@ class GalilController(Controller): # threading.Thread(target=_while_in_motion(3, n_samples), daemon=True).start() # self._while_in_motion(3, n_samples) + # TODO this is for reading out positions, readout is limited by stage triggering def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple: last_readout = 0 val_axis2 = [] # y axis From fbfa562713adaf374dfaf67ebf30cbd1895dd428 Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 30 Aug 2023 22:27:47 +0200 Subject: [PATCH 32/53] fix: online changes to integrate devices in BEC --- .../epics/devices/DelayGeneratorDG645.py | 17 ++++++- ophyd_devices/epics/devices/__init__.py | 5 ++ .../epics/devices/bec_scaninfo_mixin.py | 25 ++++++---- ophyd_devices/epics/devices/eiger9m_csaxs.py | 33 +++++++------ ophyd_devices/epics/devices/falcon_csaxs.py | 47 ++++++++++++++++--- ophyd_devices/epics/devices/mcs_csaxs.py | 14 ++++-- ophyd_devices/epics/devices/pilatus_csaxs.py | 6 +++ ophyd_devices/epics/devices/specMotors.py | 2 +- ophyd_devices/galil/sgalil_ophyd.py | 6 +-- ophyd_devices/utils/bec_utils.py | 10 +++- 10 files changed, 122 insertions(+), 43 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index ab3a316..54e6002 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -334,7 +334,8 @@ class DelayGeneratorDG645(Device): self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] + self._all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] + self._all_delay_pairs = ["AB", "CD", "EF", "GH"] self.wait_for_connection() # Make sure to be connected before talking to PVs self._init_ddg() self._ddg_is_okay() @@ -386,7 +387,7 @@ class DelayGeneratorDG645(Device): def _set_channels(self, signal: str, value: Any, channels: List = None) -> None: if not channels: - channels = self.all_channels + channels = self._all_channels for chname in channels: channel = getattr(self, chname, None) if not channel: @@ -404,6 +405,17 @@ class DelayGeneratorDG645(Device): self._init_ddg_pol_allchannels(self.polarity.get()) self._init_ddg_amp_allchannels(self.amplitude.get()) self._init_ddg_offset_allchannels(self.offset.get()) + self._set_channels( + "reference", + 0, + [f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))], + ) + for ii in range(len(self._all_delay_pairs)): + self._set_channels( + "reference", + 2 * ii + 1, + [f"channel{self._all_delay_pairs[ii]}.ch2"], + ) self._set_trigger(TriggerSource.SINGLE_SHOT) self.level.set(self.thres_trig_level.get()) @@ -474,6 +486,7 @@ class DelayGeneratorDG645(Device): # if self.scaninfo.scan_type == "step": if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() + super().trigger() def burstEnable(self, count, delay, period, config="all"): """Enable the burst mode""" diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index c65ce0b..d1e599e 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -21,3 +21,8 @@ from .specMotors import ( from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor from ophyd.sim import SynAxis, SynSignal, SynPeriodicSignal from ophyd.quadem import QuadEM + +# cSAXS +from .mcs_csaxs import McsCsaxs +from .eiger9m_csaxs import Eiger9mCsaxs +from .pilatus_csaxs import PilatusCsaxs diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index b9b3ac3..d09c66a 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -7,6 +7,21 @@ class BecScaninfoMixin: def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: self.device_manager = device_manager self.sim_mode = sim_mode + self.bec_info_msg = { + "RID": "mockrid", + "queueID": "mockqueuid", + "scan_number": 1, + "exp_time": 26e-3, + "num_points": 10000, + "readout_time": 2e-3, + "scan_type": "fly", + } + + def get_bec_info_msg(self) -> None: + return self.bec_info_msg + + def change_config(self, bec_info_msg: dict) -> None: + self.bec_info_msg = bec_info_msg def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: if not self.sim_mode: @@ -16,15 +31,7 @@ class BecScaninfoMixin: return BECMessage.ScanStatusMessage( scanID="1", status={}, - info={ - "RID": "mockrid", - "queueID": "mockqueuid", - "scan_number": 1, - "exp_time": 26e-3, - "num_points": 10000, - "readout_time": 2e-3, - "scan_type": "fly", - }, + info=self.bec_info_msg, ) def _get_username(self) -> str: diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 99f8d89..0f4d042 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -4,7 +4,7 @@ from typing import Any, List import numpy as np from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV -from ophyd import CamBase, DetectorBase +from ophyd import CamBase, DetectorBase, Device from ophyd import ADComponent as ADCpt from ophyd.areadetector.plugins import FileBase @@ -25,7 +25,7 @@ class EigerError(Exception): pass -class SlsDetectorCam(CamBase, FileBase): +class SlsDetectorCam(Device): # CamBase, FileBase): detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") setting = ADCpt(EpicsSignalWithRBV, "Setting") delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") @@ -63,6 +63,10 @@ class SlsDetectorCam(CamBase, FileBase): json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode") json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") + # fixes due to missing PVs from CamBase + acquire = ADCpt(EpicsSignal, "Acquire") + detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") + class TriggerSource(int, enum.Enum): AUTO = 0 @@ -127,8 +131,8 @@ class Eiger9mCsaxs(DetectorBase): self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs if not sim_mode: - self._producer = self.device_manager.producer self.device_manager = device_manager + self._producer = self.device_manager.producer else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() @@ -139,7 +143,6 @@ class Eiger9mCsaxs(DetectorBase): self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered - self.mokev = 12 self._init_eiger9m() self._init_standard_daq() @@ -149,7 +152,6 @@ class Eiger9mCsaxs(DetectorBase): def _init_eiger9m(self) -> None: """Init parameters for Eiger 9m""" - self._set_det_threshold() self._set_trigger(TriggerSource.GATING) self.cam.acquire.set(0) @@ -184,7 +186,7 @@ class Eiger9mCsaxs(DetectorBase): def _prep_det(self) -> None: self._set_det_threshold() self._set_acquisition_params() - self._set_trigger(TriggerSource.TRIGGER) + self._set_trigger(TriggerSource.GATING) def _set_det_threshold(self) -> None: # threshold_energy PV exists on Eiger 9M? @@ -226,7 +228,6 @@ class Eiger9mCsaxs(DetectorBase): self.std_client.start_writer_async( {"output_file": self.filepath, "n_images": self.scaninfo.num_frames} ) - logger.info("Waiting for std daq to be armed") while True: det_ctrl = self.std_client.get_status()["acquisition"]["state"] if det_ctrl == "WAITING_IMAGES": @@ -240,16 +241,18 @@ class Eiger9mCsaxs(DetectorBase): def stage(self) -> List[object]: """stage the detector and file writer""" self.scaninfo.load_scan_metadata() - self.mokev = self.device_manager.devices.mokev.read()[ + self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] self._prep_det() + logger.info("Waiting for std daq to be armed") self._prep_file_writer() + logger.info("std_daq is ready") msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, "eiger9m"), + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) self.arm_acquisition() @@ -279,13 +282,13 @@ class Eiger9mCsaxs(DetectorBase): break time.sleep(0.005) # Message to BEC - # state = True + state = True - # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - # self._producer.set_and_publish( - # MessageEndpoints.public_file(self.metadata["scanID"], self.name), - # msg.dumps(), - # ) + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), + msg.dumps(), + ) return super().unstage() def arm_acquisition(self) -> None: diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 372ad68..508d072 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -9,6 +9,9 @@ from ophyd.areadetector.plugins import HDF5Plugin, HDF5Plugin_V21, FilePlugin_V2 from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import MessageEndpoints, BECMessage, RedisConnector from bec_lib.core import bec_logger +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin + +from ophyd_devices.utils import bec_utils logger = bec_logger.logger @@ -34,6 +37,10 @@ class EpicsDXPFalcon(Device): current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") +class FalconError(Exception): + pass + + class FalconHDF5Plugins(HDF5Plugin_V21, FilePlugin_V22): pass @@ -80,6 +87,7 @@ class FalconCsaxs(Device): configuration_attrs=None, parent=None, device_manager=None, + sim_mode=False, **kwargs, ): super().__init__( @@ -91,15 +99,23 @@ class FalconCsaxs(Device): parent=parent, **kwargs, ) - self.device_manager = device_manager - self.name = name - self.username = "e21206" - # TODO once running from BEC - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + if device_manager is None and not sim_mode: + raise FalconError("Add DeviceManager to initialization or init with sim_mode=True") - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} + self.name = name + self.wait_for_connection() # Make sure to be connected before talking to PVs + if not sim_mode: + self.device_manager = device_manager + self._producer = self.device_manager.producer + else: + self._producer = bec_utils.MockProducer() + self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + # TODO + self.scaninfo.username = "e21206" + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) - self._producer = RedisConnector(["localhost:6379"]).producer() + self.readout = 0.003 # 3 ms self._value_pixel_per_buffer = 16 # TODO create file template from filewriter compile filename @@ -218,3 +234,20 @@ class FalconCsaxs(Device): # TODO raise error logger.warning("Returned in unknown state") return state + + def stop(self, *, success=False) -> None: + """Stop acquisition + Stop or Stop and Erase + """ + self._clean_up.set(1) + # self.erase_all.set(1) + self.unstage() + super().stop(success=success) + self._stopped = True + + # Automatically connect to test environmenr if directly invoked + + +if __name__ == "__main__": + falcon = FalconCsaxs(name="falcon", prefix="X12SA-SITORO::", sim_mode=True) + falcon.stage() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 9a59302..af8bd21 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -154,8 +154,8 @@ class McsCsaxs(SIS38XX): self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs if not sim_mode: - self._producer = self.device_manager.producer self.device_manager = device_manager + self._producer = self.device_manager.producer else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() @@ -164,6 +164,7 @@ class McsCsaxs(SIS38XX): self.scaninfo.username = "e21206" self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) + self._stopped = False self._init_mcs() def _init_mcs(self) -> None: @@ -202,11 +203,12 @@ class McsCsaxs(SIS38XX): """Set readout mode of mcs card Check ReadoutMode class for more information about options """ + # self.read_mode.set(ReadoutMode.EVENT) self.read_mode.set(ReadoutMode.PASSIVE) def _read_mcs_card(self) -> None: # TODO how to properly trigger the readout!!! - self.read_all.set(1) + self.read_all.put(1, use_complete=False) def readout_data(self) -> List: self._read_mcs_card() @@ -250,8 +252,11 @@ class McsCsaxs(SIS38XX): det_ctrl = self.acquiring.read()[self.acquiring.name]["value"] if det_ctrl == 0: break + if self._stopped: + break time.sleep(0.005) - self._read_mcs_card() + if not self._stopped: + self._read_mcs_card() # Message to BEC # state = True @@ -260,6 +265,7 @@ class McsCsaxs(SIS38XX): # MessageEndpoints.public_file(self.metadata["scanID"], self.name), # msg.dumps(), # ) + self._stopped = False return super().unstage() def arm_acquisition(self) -> None: @@ -277,7 +283,7 @@ class McsCsaxs(SIS38XX): """ self.stop_all.set(1) # self.erase_all.set(1) - self.unstage() + # self.unstage() super().stop(success=success) self._stopped = True diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 782e828..74b5557 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -248,3 +248,9 @@ class PilatusCsaxs(DetectorBase): self.unstage() super().stop(success=success) self._stopped = True + + +# Automatically connect to test environmenr if directly invoked +if __name__ == "__main__": + pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True) + pilatus_2.stage() diff --git a/ophyd_devices/epics/devices/specMotors.py b/ophyd_devices/epics/devices/specMotors.py index dde88fe..81f8270 100644 --- a/ophyd_devices/epics/devices/specMotors.py +++ b/ophyd_devices/epics/devices/specMotors.py @@ -181,7 +181,7 @@ class MonoTheta2(VirtualEpicsSignalRO): MONO_THETA2_OFFSETS_FILENAME = ( - "/import/work/sls/spec/local/X12SA/macros/spec_data/mono_th2_offsets.txt" + "/sls/X12SA/data/gac-x12saop/spec/macros/spec_data/mono_th2_offsets.txt" ) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index e1a35a4..9400ae4 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -247,6 +247,7 @@ class GalilController(Controller): interval_x: int, exp_time: float, readtime: float, + **kwargs, ) -> tuple: """_summary_ @@ -266,9 +267,8 @@ class GalilController(Controller): LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s """ - - # time.sleep(0.2) - + # + axes_referenced = self.controller.axis_is_referenced() # Check limits # TODO check sign of stage, or not necessary check_values = [start_y, end_y, start_x, end_x] diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index d30200d..02a8b4f 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -16,9 +16,15 @@ class MockDeviceManager: self.devices = devices() +class OphydObject: + def __init__(self) -> None: + self.name = "mock_mokev" + self.obj = mokev() + + class devices: def __init__(self): - self.mokev = mokev() + self.mokev = OphydObject() class mokev: @@ -26,4 +32,4 @@ class mokev: self.name = "mock_mokev" def read(self): - return {self.name: {"value": 12.4, "timestamp": time.time()}} + return {self.name: {"value": 16.0, "timestamp": time.time()}} From 2dc3290787a2c2cc79141b9d1da3a805b2c67ccd Mon Sep 17 00:00:00 2001 From: e21206 Date: Wed, 30 Aug 2023 22:30:32 +0200 Subject: [PATCH 33/53] fix: test function --- ophyd_devices/epics/devices/Test.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ophyd_devices/epics/devices/Test.py diff --git a/ophyd_devices/epics/devices/Test.py b/ophyd_devices/epics/devices/Test.py new file mode 100644 index 0000000..6987c50 --- /dev/null +++ b/ophyd_devices/epics/devices/Test.py @@ -0,0 +1,9 @@ +import os + +from ophyd_devices.epics.devices.pilatus_csaxs import PilatusCsaxs + +os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO" +os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5824" +# pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K") + +# pilatus_2.stage() From fe404bff9c960ff8a3f56686b24310d056ad4eda Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 30 Aug 2023 23:29:24 +0200 Subject: [PATCH 34/53] fix: bugfix for polarity --- ophyd_devices/epics/devices/DelayGeneratorDG645.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 54e6002..62d68a8 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -418,6 +418,8 @@ class DelayGeneratorDG645(Device): ) self._set_trigger(TriggerSource.SINGLE_SHOT) self.level.set(self.thres_trig_level.get()) + # invert trigger signal for eiger9m + self._set_channels("polarity", 0, channels=["channelAB"]) # TODO add delta_delay, delta_width, delta triggers! @@ -434,7 +436,7 @@ class DelayGeneratorDG645(Device): self.burstEnable(num_burst_cycle, delay_burst, exp_time, config="first") self._set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time / 2) + self._set_channels("width", exp_time) elif self.scaninfo.scan_type == "fly": # Prepare FSH DDG if self.set_high_on_exposure.get(): @@ -453,7 +455,7 @@ class DelayGeneratorDG645(Device): self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") self._set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time / 2) + self._set_channels("width", exp_time) else: # define parameters self._set_trigger(TriggerSource.SINGLE_SHOT) @@ -465,7 +467,7 @@ class DelayGeneratorDG645(Device): self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") self._set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time / 2) + self._set_channels("width", exp_time) else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") From 5d86382d80c033fb442afef74e95a19952cd5937 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 31 Aug 2023 17:09:43 +0200 Subject: [PATCH 35/53] fix: add std_daq_client and pyepics to setup --- setup.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ae287a9..bd6397e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,16 @@ __version__ = "0.4.0" if __name__ == "__main__": setup( - install_requires=["ophyd", "typeguard", "prettytable", "bec_lib", "numpy", "pyyaml"], + install_requires=[ + "ophyd", + "typeguard", + "prettytable", + "bec_lib", + "numpy", + "pyyaml", + "std_daq_client", + "pyepics", + ], extras_require={"dev": ["pytest", "pytest-random-order", "black"]}, version=__version__, ) From ac6de9da54444dda21820591dd8e3ad098d3f0ac Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 31 Aug 2023 17:11:31 +0200 Subject: [PATCH 36/53] feat: add ConfigSignal to bec_utils --- ophyd_devices/utils/bec_utils.py | 109 +++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 02a8b4f..304a2ed 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -2,8 +2,13 @@ import time from bec_lib.core import bec_logger +from ophyd import Signal, Kind + +from ophyd_devices.utils.socket import data_shape, data_type + logger = bec_logger.logger +DEFAULT_EPICSSIGNAL_VALUE = object() class MockProducer: @@ -33,3 +38,107 @@ class mokev: def read(self): return {self.name: {"value": 16.0, "timestamp": time.time()}} + + +class ConfigSignal(Signal): + def __init__( + self, + *, + name, + value=0, + timestamp=None, + parent=None, + labels=None, + kind=Kind.hinted, + tolerance=None, + rtolerance=None, + metadata=None, + cl=None, + attr_name="", + config_storage_name: str = "config_storage", + ): + super().__init__( + name=name, + value=value, + timestamp=timestamp, + parent=parent, + labels=labels, + kind=kind, + tolerance=tolerance, + rtolerance=rtolerance, + metadata=metadata, + cl=cl, + attr_name=attr_name, + ) + + self.storage_name = config_storage_name + + def get(self): + self._readback = getattr(self.parent, self.storage_name)[self.name] + return self._readback + + def put( + self, + value, + connection_timeout=1, + callback=None, + timeout=1, + **kwargs, + ): + """Using channel access, set the write PV to `value`. + + Keyword arguments are passed on to callbacks + + Parameters + ---------- + value : any + The value to set + connection_timeout : float, optional + If not already connected, allow up to `connection_timeout` seconds + for the connection to complete. + use_complete : bool, optional + Override put completion settings + callback : callable + Callback for when the put has completed + timeout : float, optional + Timeout before assuming that put has failed. (Only relevant if + put completion is used.) + """ + + old_value = self.get() + timestamp = time.time() + getattr(self.parent, self.storage_name)[self.name] = value + super().put(value, timestamp=timestamp, force=True) + self._run_subs( + sub_type=self.SUB_VALUE, + old_value=old_value, + value=value, + timestamp=timestamp, + ) + + def describe(self): + """Provide schema and meta-data for :meth:`~BlueskyInterface.read` + + This keys in the `OrderedDict` this method returns must match the + keys in the `OrderedDict` return by :meth:`~BlueskyInterface.read`. + + This provides schema related information, (ex shape, dtype), the + source (ex PV name), and if available, units, limits, precision etc. + + Returns + ------- + data_keys : OrderedDict + The keys must be strings and the values must be dict-like + with the ``event_model.event_descriptor.data_key`` schema. + """ + if self._readback is DEFAULT_EPICSSIGNAL_VALUE: + val = self.get() + else: + val = self._readback + return { + self.name: { + "source": f"{self.parent.prefix}:{self.name}", + "dtype": data_type(val), + "shape": data_shape(val), + } + } From ab220562fc1eac3bcffff01fd92085445dd774e7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 31 Aug 2023 17:12:08 +0200 Subject: [PATCH 37/53] feat: add mcs_readout_monitor and stream --- ophyd_devices/epics/devices/mcs_csaxs.py | 130 +++++++++++++++++------ 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index af8bd21..dc702b4 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -13,6 +13,7 @@ from ophyd_devices.utils import bec_utils from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin +from collections import defaultdict from bec_lib.core import bec_logger @@ -90,14 +91,22 @@ class SIS38XX(Device): max_channels = Cpt(EpicsSignalRO, "MaxChannels") -class McsCsaxs(SIS38XX): - scaler = Cpt(ScalerCH, "scaler1") +num_lines = Cpt( + bec_utils.ConfigSignal, + name="num_lines", + kind="config", + config_storage_name="mcs_configs", +) - mca1 = Cpt(EpicsMCARecord, "mca1") - mca2 = Cpt(EpicsMCARecord, "mca2") - mca3 = Cpt(EpicsMCARecord, "mca3") - mca4 = Cpt(EpicsMCARecord, "mca4") - mca5 = Cpt(EpicsMCARecord, "mca5") + +class McsCsaxs(SIS38XX): + # scaler = Cpt(ScalerCH, "scaler1") + + # mca2 = Cpt(EpicsMCARecord, "mca2") + mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True) + mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True) + mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True) + # mca5 = Cpt(EpicsMCARecord, "mca5") # mca6 = Cpt(EpicsMCARecord, "mca6") # mca7 = Cpt(EpicsMCARecord, "mca7") # mca8 = Cpt(EpicsMCARecord, "mca8") @@ -139,6 +148,10 @@ class McsCsaxs(SIS38XX): sim_mode=False, **kwargs, ): + self.mcs_configs = { + f"{name}_num_lines": 1, + } + super().__init__( prefix=prefix, name=name, @@ -148,23 +161,29 @@ class McsCsaxs(SIS38XX): parent=parent, **kwargs, ) + if device_manager is None and not sim_mode: raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name + self._stream_ttl = 1800 self.wait_for_connection() # Make sure to be connected before talking to PVs + if not sim_mode: self.device_manager = device_manager self._producer = self.device_manager.producer else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() + # TODO mack mock connector class + # self._consumer = self.device_manager.connector.consumer self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.scaninfo.username = "e21206" self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False + self._acquisition_done = False self._init_mcs() def _init_mcs(self) -> None: @@ -183,14 +202,50 @@ class McsCsaxs(SIS38XX): self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) self.count_on_start.set(0) + self.mca_names = [signal for signal in self.signal_names if signal.startswith("mca")] + for mca in self.mca_names: + signal = getattr(self, mca) + signal.subscribe(self._on_mca_data) + self.mca_data = defaultdict(lambda: []) + self._counter = 0 + + def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None: + self.mca_data[obj.attr_name] = value + if len(self.mca_names) == len(self.mca_data) and len(self.mca_data[self.mca_names]) != 0: + self._updated = True + self.erase_start.set(1) + self._send_data_to_bec() + self.mca_data = defaultdict(lambda: []) + self.counter += 1 + if self.counter == self.num_lines: + self._acquisition_done = True + + def _send_data_to_bec(self) -> None: + metadata = self.scaninfo.scan_msg.metadata + metadata.update( + { + "async_update": "append", + "num_lines": self.num_lines, + } + ) + msg = BECMessage.DeviceMessage( + signals=dict(self.mca_data), metadata=self.scaninfo.scan_msg.metadata + ).dumps() + self._producer.xadd( + topics=MessageEndpoints._device_async_readback(self.name), + msg=msg, + expire=self._stream_ttl, + ) def _prep_det(self) -> None: self._set_acquisition_params() self._set_trigger(TriggerSource.MODE3) def _set_acquisition_params(self) -> None: - # max number of readings is limited to 10000, but device can be reseted.. needs to be included on scan level - self.num_use_all.set(self.scaninfo.num_frames) + n_points = self.scaninfo.num_frames / int(self.num_lines.get()) + if n_points > 10000: + raise MCSError(f"Requested number of points {n_points} exceeds hardware limit of 10000") + self.num_use_all.set(n_points) self.preset_real.set(0) def _set_trigger(self, trigger_source: TriggerSource) -> None: @@ -204,24 +259,35 @@ class McsCsaxs(SIS38XX): Check ReadoutMode class for more information about options """ # self.read_mode.set(ReadoutMode.EVENT) - self.read_mode.set(ReadoutMode.PASSIVE) + self.erase_all.set(1) + self.read_mode.set(ReadoutMode.EVENT) - def _read_mcs_card(self) -> None: - # TODO how to properly trigger the readout!!! + def _force_readout_mcs_card(self) -> None: self.read_all.put(1, use_complete=False) - def readout_data(self) -> List: - self._read_mcs_card() - readback = [] - for ii in range(1, int(self.mux_output.read()[self.mux_output.name]["value"]) + 1): - readback.append(self._readout_mca_channels(ii)) - return readback + # TODO does not work anymore with new mca signals + # def readout_data(self) -> List[List]: + # """Manual readout of mca slots, returns list of lists""" + # self._force_readout_mcs_card() + # readback = [] + # for ii in range(1, int(self.mux_output.read()[self.mux_output.name]["value"]) + 1): + # readback.append(self._readout_mca_channels(ii)) + # return readback - def _readout_mca_channels(self, num: int) -> List[List]: - signal = f"mca{num}" - if signal in self.component_names: - readback = f"{getattr(self, signal).name}_spectrum" - return getattr(self, signal).read()[readback]["value"] + # def _readout_mca_channels(self, num: int) -> List: + # """readout of single mca channel""" + # signal = f"mca{num}" + # if signal in self.component_names: + # readback = f"{getattr(self, signal).name}_spectrum" + # return getattr(self, signal).read()[readback]["value"] + + def _start_readout_loop(self) -> None: + self._readout_lines() + # stop acquisition and clean up data + self.stop_all.set(1) + self.erase_all.set(1) + self._acquisition_done = True + self._updated = False def stage(self) -> List[object]: """stage the detector and file writer""" @@ -248,15 +314,14 @@ class McsCsaxs(SIS38XX): def unstage(self) -> List[object]: """unstage""" logger.info("Waiting for mcs to finish acquisition") - while True: - det_ctrl = self.acquiring.read()[self.acquiring.name]["value"] - if det_ctrl == 0: - break + # start readout in thread? + self._start_readout_loop() + while not self._acquisition_done: + # monitor signal instead? if self._stopped: break time.sleep(0.005) - if not self._stopped: - self._read_mcs_card() + # Message to BEC # state = True @@ -265,7 +330,10 @@ class McsCsaxs(SIS38XX): # MessageEndpoints.public_file(self.metadata["scanID"], self.name), # msg.dumps(), # ) + + self._acquisition_done = False self._stopped = False + logger.info("mcs done") return super().unstage() def arm_acquisition(self) -> None: @@ -274,6 +342,7 @@ class McsCsaxs(SIS38XX): Start: start_all Erase/Start: erase_start """ + self.counter = 0 self.erase_start.set(1) # self.start_all.set(1) @@ -283,9 +352,8 @@ class McsCsaxs(SIS38XX): """ self.stop_all.set(1) # self.erase_all.set(1) - # self.unstage() - super().stop(success=success) self._stopped = True + super().stop(success=success) # Automatically connect to test environmenr if directly invoked From 278679125ee50bd1de4daf3c4aa08d2afaa43c20 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 31 Aug 2023 17:13:22 +0200 Subject: [PATCH 38/53] refactor: online changes --- .../epics/devices/DelayGeneratorDG645.py | 160 ++++++------------ ophyd_devices/epics/devices/__init__.py | 1 + .../epics/devices/bec_scaninfo_mixin.py | 3 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 1 + ophyd_devices/epics/devices/falcon_csaxs.py | 2 +- 5 files changed, 55 insertions(+), 112 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 62d68a8..f5f9a28 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -18,85 +18,12 @@ from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger -DEFAULT_EPICSSIGNAL_VALUE = object() class DDGError(Exception): pass -class DDGConfigSignal(Signal): - def get(self): - self._readback = self.parent.ddg_configs[self.name] - return self._readback - - def put( - self, - value, - connection_timeout=1, - callback=None, - timeout=1, - **kwargs, - ): - """Using channel access, set the write PV to `value`. - - Keyword arguments are passed on to callbacks - - Parameters - ---------- - value : any - The value to set - connection_timeout : float, optional - If not already connected, allow up to `connection_timeout` seconds - for the connection to complete. - use_complete : bool, optional - Override put completion settings - callback : callable - Callback for when the put has completed - timeout : float, optional - Timeout before assuming that put has failed. (Only relevant if - put completion is used.) - """ - - old_value = self.get() - timestamp = time.time() - self.parent.ddg_configs[self.name] = value - super().put(value, timestamp=timestamp, force=True) - self._run_subs( - sub_type=self.SUB_VALUE, - old_value=old_value, - value=value, - timestamp=timestamp, - ) - - def describe(self): - """Provide schema and meta-data for :meth:`~BlueskyInterface.read` - - This keys in the `OrderedDict` this method returns must match the - keys in the `OrderedDict` return by :meth:`~BlueskyInterface.read`. - - This provides schema related information, (ex shape, dtype), the - source (ex PV name), and if available, units, limits, precision etc. - - Returns - ------- - data_keys : OrderedDict - The keys must be strings and the values must be dict-like - with the ``event_model.event_descriptor.data_key`` schema. - """ - if self._readback is DEFAULT_EPICSSIGNAL_VALUE: - val = self.get() - else: - val = self._readback - return { - self.name: { - "source": f"{self.parent.prefix}:{self.name}", - "dtype": data_type(val), - "shape": data_shape(val), - } - } - - class DelayStatic(Device): """Static axis for the T0 output channel It allows setting the logic levels, but the timing is fixed. @@ -263,15 +190,45 @@ class DelayGeneratorDG645(Device): EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config ) - delay_burst = Component(DDGConfigSignal, name="delay_burst", kind="config") - delta_width = Component(DDGConfigSignal, name="delta_width", kind="config") - additional_triggers = Component(DDGConfigSignal, name="additional_triggers", kind="config") - polarity = Component(DDGConfigSignal, name="polarity", kind="config") - amplitude = Component(DDGConfigSignal, name="amplitude", kind="config") - offset = Component(DDGConfigSignal, name="offset", kind="config") - thres_trig_level = Component(DDGConfigSignal, name="thres_trig_level", kind="config") - set_high_on_exposure = Component(DDGConfigSignal, name="set_high_on_exposure", kind="config") - set_high_on_stage = Component(DDGConfigSignal, name="set_high_on_stage", kind="config") + delay_burst = Component( + bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_configs" + ) + delta_width = Component( + bec_utils.ConfigSignal, name="delta_width", kind="config", config_storage_name="ddg_configs" + ) + additional_triggers = Component( + bec_utils.ConfigSignal, + name="additional_triggers", + kind="config", + config_storage_name="ddg_configs", + ) + polarity = Component( + bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_configs" + ) + amplitude = Component( + bec_utils.ConfigSignal, name="amplitude", kind="config", config_storage_name="ddg_configs" + ) + offset = Component( + bec_utils.ConfigSignal, name="offset", kind="config", config_storage_name="ddg_configs" + ) + thres_trig_level = Component( + bec_utils.ConfigSignal, + name="thres_trig_level", + kind="config", + config_storage_name="ddg_configs", + ) + set_high_on_exposure = Component( + bec_utils.ConfigSignal, + name="set_high_on_exposure", + kind="config", + config_storage_name="ddg_configs", + ) + set_high_on_stage = Component( + bec_utils.ConfigSignal, + name="set_high_on_stage", + kind="config", + config_storage_name="ddg_configs", + ) def __init__( self, @@ -364,27 +321,6 @@ class DelayGeneratorDG645(Device): elif status != "STATUS OK": raise DDGError(f"DDG failed to start with status: {status}") - def _init_ddg_pol_allchannels(self, polarity: int = 1) -> None: - """Set Polarity for all channels (including T0) upon init - Args: - polarity: int | 0 negative, 1 positive defaults to 1 - """ - self._set_channels("polarity", polarity) - - def _init_ddg_amp_allchannels(self, amplitude: float = 5) -> None: - """Set amplitude for all channels (including T0) upon init - Args: - amplitude: float | defaults to 5 - """ - self._set_channels("amplitude", amplitude) - - def _init_ddg_offset_allchannels(self, offset: float = 0) -> None: - """Set offset for all channels (including T0) upon init - Args: - offset: float | defaults to 0 - """ - self._set_channels("offset", offset) - def _set_channels(self, signal: str, value: Any, channels: List = None) -> None: if not channels: channels = self._all_channels @@ -402,9 +338,16 @@ class DelayGeneratorDG645(Device): self._set_trigger(TriggerSource.SINGLE_SHOT) def _init_ddg(self) -> None: - self._init_ddg_pol_allchannels(self.polarity.get()) - self._init_ddg_amp_allchannels(self.amplitude.get()) - self._init_ddg_offset_allchannels(self.offset.get()) + self._set_channels( + "polarity", + self.polarity.get(), + channels=["channelT0", "channelCD", "channelEF", "channelGH"], + ) + # Set polarity for eiger inverted! + self._set_channels("polarity", 0, channels=["channelAB"]) + self._set_channels("amplitude", self.amplitude.get()) + self._set_channels("offset", self.offset.get()) + # Setup reference self._set_channels( "reference", 0, @@ -417,11 +360,8 @@ class DelayGeneratorDG645(Device): [f"channel{self._all_delay_pairs[ii]}.ch2"], ) self._set_trigger(TriggerSource.SINGLE_SHOT) + # Set threshold level for ext. pulses self.level.set(self.thres_trig_level.get()) - # invert trigger signal for eiger9m - self._set_channels("polarity", 0, channels=["channelAB"]) - - # TODO add delta_delay, delta_width, delta triggers! def stage(self): """Trigger the generator by arming to accept triggers""" diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index d1e599e..4915e40 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -26,3 +26,4 @@ from ophyd.quadem import QuadEM from .mcs_csaxs import McsCsaxs from .eiger9m_csaxs import Eiger9mCsaxs from .pilatus_csaxs import PilatusCsaxs +from .falcon_csaxs import FalconCsaxs diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index d09c66a..63feab0 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -15,6 +15,7 @@ class BecScaninfoMixin: "num_points": 10000, "readout_time": 2e-3, "scan_type": "fly", + "num_lines": 10, } def get_bec_info_msg(self) -> None: @@ -40,7 +41,7 @@ class BecScaninfoMixin: return os.getlogin() def load_scan_metadata(self) -> None: - scan_msg = self._get_current_scan_msg() + self.scan_msg = scan_msg = self._get_current_scan_msg() self.metadata = { "scanID": scan_msg.content["scanID"], "RID": scan_msg.content["info"]["RID"], diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 0f4d042..b59e27d 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -289,6 +289,7 @@ class Eiger9mCsaxs(DetectorBase): MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) + logger.info("Eiger done") return super().unstage() def arm_acquisition(self) -> None: diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 508d072..a172572 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -249,5 +249,5 @@ class FalconCsaxs(Device): if __name__ == "__main__": - falcon = FalconCsaxs(name="falcon", prefix="X12SA-SITORO::", sim_mode=True) + falcon = FalconCsaxs(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) falcon.stage() From 08efb6405bc615b40855288067c1e811f1471423 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 31 Aug 2023 18:24:19 +0200 Subject: [PATCH 39/53] fix: mcs working --- .../epics/devices/bec_scaninfo_mixin.py | 1 + ophyd_devices/epics/devices/mcs_csaxs.py | 72 ++++++++++--------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 63feab0..955bb0b 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -7,6 +7,7 @@ class BecScaninfoMixin: def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: self.device_manager = device_manager self.sim_mode = sim_mode + self.scan_msg = None self.bec_info_msg = { "RID": "mockrid", "queueID": "mockqueuid", diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index dc702b4..2d0886a 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -91,14 +91,6 @@ class SIS38XX(Device): max_channels = Cpt(EpicsSignalRO, "MaxChannels") -num_lines = Cpt( - bec_utils.ConfigSignal, - name="num_lines", - kind="config", - config_storage_name="mcs_configs", -) - - class McsCsaxs(SIS38XX): # scaler = Cpt(ScalerCH, "scaler1") @@ -135,6 +127,13 @@ class McsCsaxs(SIS38XX): # mca31 = Cpt(EpicsMCARecord, "mca31") # mca32 = Cpt(EpicsMCARecord, "mca32") + num_lines = Cpt( + bec_utils.ConfigSignal, + name="num_lines", + kind="config", + config_storage_name="mcs_configs", + ) + def __init__( self, prefix="", @@ -202,38 +201,50 @@ class McsCsaxs(SIS38XX): self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) self.count_on_start.set(0) - self.mca_names = [signal for signal in self.signal_names if signal.startswith("mca")] + self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")] + self.mca_data = defaultdict(lambda: []) for mca in self.mca_names: signal = getattr(self, mca) - signal.subscribe(self._on_mca_data) - self.mca_data = defaultdict(lambda: []) + signal.subscribe(self._on_mca_data, run=False) self._counter = 0 - def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None: - self.mca_data[obj.attr_name] = value - if len(self.mca_names) == len(self.mca_data) and len(self.mca_data[self.mca_names]) != 0: - self._updated = True - self.erase_start.set(1) - self._send_data_to_bec() + def _on_mca_data(self, *args, obj=None, **kwargs) -> None: + self.mca_data[obj.attr_name] = kwargs["value"] + if len(self.mca_names) != len(self.mca_data): + return + ref_entry = self.mca_data[self.mca_names[0]] + if not ref_entry: self.mca_data = defaultdict(lambda: []) - self.counter += 1 - if self.counter == self.num_lines: - self._acquisition_done = True + return + if isinstance(ref_entry, list) and not ref_entry: + return + + self._updated = True + self.erase_start.set(1) + self._send_data_to_bec() + self.mca_data = defaultdict(lambda: []) + self.counter += 1 + if self.counter == self.num_lines.get(): + self._acquisition_done = True def _send_data_to_bec(self) -> None: + if self.scaninfo.scan_msg is None: + return metadata = self.scaninfo.scan_msg.metadata metadata.update( { "async_update": "append", - "num_lines": self.num_lines, + "num_lines": self.num_lines.get(), } ) msg = BECMessage.DeviceMessage( signals=dict(self.mca_data), metadata=self.scaninfo.scan_msg.metadata ).dumps() self._producer.xadd( - topics=MessageEndpoints._device_async_readback(self.name), - msg=msg, + topic=MessageEndpoints.device_async_readback( + scanID=self.scaninfo.scanID, device=self.name + ), + msg={"data": msg}, expire=self._stream_ttl, ) @@ -281,13 +292,12 @@ class McsCsaxs(SIS38XX): # readback = f"{getattr(self, signal).name}_spectrum" # return getattr(self, signal).read()[readback]["value"] - def _start_readout_loop(self) -> None: - self._readout_lines() - # stop acquisition and clean up data - self.stop_all.set(1) - self.erase_all.set(1) - self._acquisition_done = True - self._updated = False + # def _start_readout_loop(self) -> None: + # # stop acquisition and clean up data + # self.stop_all.set(1) + # self.erase_all.set(1) + # self._acquisition_done = True + # self._updated = False def stage(self) -> List[object]: """stage the detector and file writer""" @@ -314,8 +324,6 @@ class McsCsaxs(SIS38XX): def unstage(self) -> List[object]: """unstage""" logger.info("Waiting for mcs to finish acquisition") - # start readout in thread? - self._start_readout_loop() while not self._acquisition_done: # monitor signal instead? if self._stopped: From b6101cced24b8b37a3363efa5554a627fdc875b1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Sep 2023 09:33:35 +0200 Subject: [PATCH 40/53] fix: online changes --- .../epics/devices/DelayGeneratorDG645.py | 1 + .../epics/devices/bec_scaninfo_mixin.py | 2 ++ ophyd_devices/epics/devices/eiger9m_csaxs.py | 1 + ophyd_devices/epics/devices/mcs_csaxs.py | 34 +++++++++---------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index f5f9a28..5c96d34 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -190,6 +190,7 @@ class DelayGeneratorDG645(Device): EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config ) + # bec_utils device ConfigSignal delay_burst = Component( bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_configs" ) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 955bb0b..ca0026e 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -8,6 +8,7 @@ class BecScaninfoMixin: self.device_manager = device_manager self.sim_mode = sim_mode self.scan_msg = None + self.scanID = None self.bec_info_msg = { "RID": "mockrid", "queueID": "mockqueuid", @@ -27,6 +28,7 @@ class BecScaninfoMixin: def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: if not self.sim_mode: + # TODO what if no scan info is there yet! msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) return BECMessage.ScanStatusMessage.loads(msg) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index b59e27d..83c60ac 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -138,6 +138,7 @@ class Eiger9mCsaxs(DetectorBase): self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO + self.filepath = "" self.scaninfo.username = "e21206" self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 2d0886a..12944f4 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -162,7 +162,9 @@ class McsCsaxs(SIS38XX): ) if device_manager is None and not sim_mode: - raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") + raise MCSError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.name = name self._stream_ttl = 1800 @@ -179,7 +181,9 @@ class McsCsaxs(SIS38XX): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = { + "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" + } self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False self._acquisition_done = False @@ -201,7 +205,9 @@ class McsCsaxs(SIS38XX): self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) self.count_on_start.set(0) - self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")] + self.mca_names = [ + signal for signal in self.component_names if signal.startswith("mca") + ] self.mca_data = defaultdict(lambda: []) for mca in self.mca_names: signal = getattr(self, mca) @@ -220,12 +226,14 @@ class McsCsaxs(SIS38XX): return self._updated = True - self.erase_start.set(1) - self._send_data_to_bec() - self.mca_data = defaultdict(lambda: []) self.counter += 1 if self.counter == self.num_lines.get(): self._acquisition_done = True + self._send_data_to_bec() + self.stop_all.put(1, use_complete=False) + self.erase_start.set(1) + self._send_data_to_bec() + self.mca_data = defaultdict(lambda: []) def _send_data_to_bec(self) -> None: if self.scaninfo.scan_msg is None: @@ -255,7 +263,9 @@ class McsCsaxs(SIS38XX): def _set_acquisition_params(self) -> None: n_points = self.scaninfo.num_frames / int(self.num_lines.get()) if n_points > 10000: - raise MCSError(f"Requested number of points {n_points} exceeds hardware limit of 10000") + raise MCSError( + f"Requested number of points {n_points} exceeds hardware limit of 10000" + ) self.num_use_all.set(n_points) self.preset_real.set(0) @@ -329,16 +339,6 @@ class McsCsaxs(SIS38XX): if self._stopped: break time.sleep(0.005) - - # Message to BEC - # state = True - - # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - # self._producer.set_and_publish( - # MessageEndpoints.public_file(self.metadata["scanID"], self.name), - # msg.dumps(), - # ) - self._acquisition_done = False self._stopped = False logger.info("mcs done") From c0b34183661b11c39d65eb117c3670a714f9eb5c Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 4 Sep 2023 16:23:14 +0200 Subject: [PATCH 41/53] fix: online changes to all devices in preparation for beamtime --- .../epics/devices/DelayGeneratorDG645.py | 194 +++++++++++++----- ophyd_devices/epics/devices/__init__.py | 1 + ophyd_devices/epics/devices/eiger9m_csaxs.py | 25 ++- ophyd_devices/epics/devices/mcs_csaxs.py | 14 +- ophyd_devices/galil/sgalil_ophyd.py | 78 +++++-- 5 files changed, 234 insertions(+), 78 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 5c96d34..f7048c4 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -42,10 +42,18 @@ class DelayStatic(Device): kind=Kind.config, ) amplitude = Component( - EpicsSignal, "OutputAmpAI", write_pv="OutputAmpAO", name="amplitude", kind=Kind.config + EpicsSignal, + "OutputAmpAI", + write_pv="OutputAmpAO", + name="amplitude", + kind=Kind.config, ) offset = Component( - EpicsSignal, "OutputOffsetAI", write_pv="OutputOffsetAO", name="offset", kind=Kind.config + EpicsSignal, + "OutputOffsetAI", + write_pv="OutputOffsetAO", + name="offset", + kind=Kind.config, ) @@ -53,7 +61,9 @@ class DummyPositioner(PVPositioner): setpoint = Component(EpicsSignal, "DelayAO", put_complete=True, kind=Kind.config) readback = Component(EpicsSignalRO, "DelayAI", kind=Kind.config) done = Component(Signal, value=1) - reference = Component(EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config) + reference = Component( + EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config + ) class DelayPair(PseudoPositioner): @@ -83,12 +93,16 @@ class DelayPair(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): """Run a forward (pseudo -> real) calculation""" - return self.RealPosition(ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width) + return self.RealPosition( + ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width + ) @real_position_argument def inverse(self, real_pos): """Run an inverse (real -> pseudo) calculation""" - return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) + return self.PseudoPosition( + delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1 + ) class TriggerSource(int, enum.Enum): @@ -125,6 +139,14 @@ class DelayGeneratorDG645(Device): current device """ + USER_ACCESS = [ + "set_channels", + "_set_trigger", + "burst_enable", + "burst_disable", + "reload_config", + ] + state = Component(EpicsSignalRO, "EventStatusLI", name="status_register") status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -172,63 +194,107 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) - trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") + trigger_shot = Component( + EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config" + ) # Burst mode burstMode = Component( - EpicsSignal, "BurstModeBI", write_pv="BurstModeBO", name="burstmode", kind=Kind.config + EpicsSignal, + "BurstModeBI", + write_pv="BurstModeBO", + name="burstmode", + kind=Kind.config, ) burstConfig = Component( - EpicsSignal, "BurstConfigBI", write_pv="BurstConfigBO", name="burstconfig", kind=Kind.config + EpicsSignal, + "BurstConfigBI", + write_pv="BurstConfigBO", + name="burstconfig", + kind=Kind.config, ) burstCount = Component( - EpicsSignal, "BurstCountLI", write_pv="BurstCountLO", name="burstcount", kind=Kind.config + EpicsSignal, + "BurstCountLI", + write_pv="BurstCountLO", + name="burstcount", + kind=Kind.config, ) burstDelay = Component( - EpicsSignal, "BurstDelayAI", write_pv="BurstDelayAO", name="burstdelay", kind=Kind.config + EpicsSignal, + "BurstDelayAI", + write_pv="BurstDelayAO", + name="burstdelay", + kind=Kind.config, ) burstPeriod = Component( - EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config + EpicsSignal, + "BurstPeriodAI", + write_pv="BurstPeriodAO", + name="burstperiod", + kind=Kind.config, ) - # bec_utils device ConfigSignal delay_burst = Component( - bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_configs" + bec_utils.ConfigSignal, + name="delay_burst", + kind="config", + config_storage_name="ddg_config", ) + delta_width = Component( - bec_utils.ConfigSignal, name="delta_width", kind="config", config_storage_name="ddg_configs" + bec_utils.ConfigSignal, + name="delta_width", + kind="config", + config_storage_name="ddg_config", ) + additional_triggers = Component( bec_utils.ConfigSignal, name="additional_triggers", kind="config", - config_storage_name="ddg_configs", + config_storage_name="ddg_config", ) + polarity = Component( - bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_configs" + bec_utils.ConfigSignal, + name="polarity", + kind="config", + config_storage_name="ddg_config", ) + amplitude = Component( - bec_utils.ConfigSignal, name="amplitude", kind="config", config_storage_name="ddg_configs" + bec_utils.ConfigSignal, + name="amplitude", + kind="config", + config_storage_name="ddg_config", ) + offset = Component( - bec_utils.ConfigSignal, name="offset", kind="config", config_storage_name="ddg_configs" + bec_utils.ConfigSignal, + name="offset", + kind="config", + config_storage_name="ddg_config", ) + thres_trig_level = Component( bec_utils.ConfigSignal, name="thres_trig_level", kind="config", - config_storage_name="ddg_configs", + config_storage_name="ddg_config", ) + set_high_on_exposure = Component( bec_utils.ConfigSignal, name="set_high_on_exposure", kind="config", - config_storage_name="ddg_configs", + config_storage_name="ddg_config", ) + set_high_on_stage = Component( bec_utils.ConfigSignal, name="set_high_on_stage", kind="config", - config_storage_name="ddg_configs", + config_storage_name="ddg_config", ) def __init__( @@ -242,6 +308,7 @@ class DelayGeneratorDG645(Device): parent=None, device_manager=None, sim_mode=False, + ddg_config = None, **kwargs, ): """_summary_ @@ -259,11 +326,11 @@ class DelayGeneratorDG645(Device): amplitude (_type_, optional): _description_. Defaults to None. offset (_type_, optional): _description_. Defaults to None. thres_trig_level (_type_, optional): _description_. Defaults to None. - delta_delay (_type_, float): Add delay for triggering in software trigger mode to allow fast shutter to open. Defaults to 0. + delay_burst (_type_, float): Add delay for triggering in software trigger mode to allow fast shutter to open. Defaults to 0. delta_width (_type_, float): Add width to fast shutter signal to make sure its open during acquisition. Defaults to 0. delta_triggers (_type_, int): Add additional triggers to burst mode (mcs card needs +1 triggers per line). Defaults to 0. """ - self.ddg_configs = { + self.ddg_config = { f"{name}_delay_burst": 0, f"{name}_delta_width": 0, f"{name}_additional_triggers": 0, @@ -274,6 +341,8 @@ class DelayGeneratorDG645(Device): f"{name}_set_high_on_exposure": False, f"{name}_set_high_on_stage": False, } + if ddg_config is not None: + [self.ddg_config.update({f'{name}_{key}' : value}) for key, value in ddg_config.items()] super().__init__( prefix=prefix, name=name, @@ -284,7 +353,9 @@ class DelayGeneratorDG645(Device): **kwargs, ) if device_manager is None and not sim_mode: - raise DDGError("Add DeviceManager to initialization or init with sim_mode=True") + raise DDGError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.device_manager = device_manager if not sim_mode: self._producer = self.device_manager.producer @@ -292,10 +363,17 @@ class DelayGeneratorDG645(Device): self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self._all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] + self._all_channels = [ + "channelT0", + "channelAB", + "channelCD", + "channelEF", + "channelGH", + ] self._all_delay_pairs = ["AB", "CD", "EF", "GH"] self.wait_for_connection() # Make sure to be connected before talking to PVs - self._init_ddg() + logger.info(f'Current polarity value {self.polarity.get()}') + self.reload_config() self._ddg_is_okay() def _set_trigger(self, trigger_source: TriggerSource) -> None: @@ -322,7 +400,7 @@ class DelayGeneratorDG645(Device): elif status != "STATUS OK": raise DDGError(f"DDG failed to start with status: {status}") - def _set_channels(self, signal: str, value: Any, channels: List = None) -> None: + def set_channels(self, signal: str, value: Any, channels: List = None) -> None: if not channels: channels = self._all_channels for chname in channels: @@ -338,26 +416,29 @@ class DelayGeneratorDG645(Device): def _cleanup_ddg(self) -> None: self._set_trigger(TriggerSource.SINGLE_SHOT) - def _init_ddg(self) -> None: - self._set_channels( + def reload_config(self) -> None: + self.set_channels( "polarity", self.polarity.get(), - channels=["channelT0", "channelCD", "channelEF", "channelGH"], + channels=["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"], ) # Set polarity for eiger inverted! - self._set_channels("polarity", 0, channels=["channelAB"]) - self._set_channels("amplitude", self.amplitude.get()) - self._set_channels("offset", self.offset.get()) + # self.set_channels("polarity", 0, channels=["channelAB"]) + self.set_channels("amplitude", self.amplitude.get()) + self.set_channels("offset", self.offset.get()) # Setup reference - self._set_channels( + self.set_channels( "reference", 0, - [f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))], + [ + f"channel{self._all_delay_pairs[ii]}.ch1" + for ii in range(len(self._all_delay_pairs)) + ], ) for ii in range(len(self._all_delay_pairs)): - self._set_channels( + self.set_channels( "reference", - 2 * ii + 1, + 0, [f"channel{self._all_delay_pairs[ii]}.ch2"], ) self._set_trigger(TriggerSource.SINGLE_SHOT) @@ -374,10 +455,10 @@ class DelayGeneratorDG645(Device): delay_burst = self.delay_burst.get() num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG - self.burstEnable(num_burst_cycle, delay_burst, exp_time, config="first") - self._set_channels("delay", 0) + self.burst_enable(num_burst_cycle, delay_burst, exp_time, config="first") + self.set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time) + self.set_channels("width", exp_time) elif self.scaninfo.scan_type == "fly": # Prepare FSH DDG if self.set_high_on_exposure.get(): @@ -393,22 +474,28 @@ class DelayGeneratorDG645(Device): # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG - self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") - self._set_channels("delay", 0) + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) + self.set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time) + self.set_channels("width", exp_time) else: # define parameters self._set_trigger(TriggerSource.SINGLE_SHOT) exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + num_burst_cycle = ( + self.scaninfo.num_frames + self.additional_triggers.get() + ) # set parameters in DDG - self.burstEnable(num_burst_cycle, delay_burst, total_exposure, config="first") - self._set_channels("delay", 0) + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) + self.set_channels("delay", 0) # Set burst length to half of the experimental time! - self._set_channels("width", exp_time) + self.set_channels("width", exp_time) else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") @@ -427,18 +514,23 @@ class DelayGeneratorDG645(Device): def trigger(self) -> None: # if self.scaninfo.scan_type == "step": - if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): + if self.source.read()[self.source.name]["value"] == int( + TriggerSource.SINGLE_SHOT + ): self.trigger_shot.set(1).wait() super().trigger() - def burstEnable(self, count, delay, period, config="all"): + def burst_enable(self, count, delay, period, config="all"): """Enable the burst mode""" # Validate inputs count = int(count) assert count > 0, "Number of bursts must be positive" assert delay >= 0, "Burst delay must be larger than 0" assert period > 0, "Burst period must be positive" - assert config in ["all", "first"], "Supported bust configs are 'all' and 'first'" + assert config in [ + "all", + "first", + ], "Supported bust configs are 'all' and 'first'" self.burstMode.set(1).wait() self.burstCount.set(count).wait() @@ -450,7 +542,7 @@ class DelayGeneratorDG645(Device): elif config == "first": self.burstConfig.set(1).wait() - def burstDisable(self): + def burst_disable(self): """Disable the burst mode""" self.burstMode.set(0).wait() diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 4915e40..2dd859f 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -27,3 +27,4 @@ from .mcs_csaxs import McsCsaxs from .eiger9m_csaxs import Eiger9mCsaxs from .pilatus_csaxs import PilatusCsaxs from .falcon_csaxs import FalconCsaxs +from .DelayGeneratorDG645 import DelayGeneratorDG645 diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 83c60ac..6cadde1 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -126,7 +126,9 @@ class Eiger9mCsaxs(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") + raise EigerError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs @@ -140,7 +142,9 @@ class Eiger9mCsaxs(DetectorBase): # TODO self.filepath = "" self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = { + "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" + } self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -168,7 +172,9 @@ class Eiger9mCsaxs(DetectorBase): f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}" ) cfg.update({cfg_key: value}) - logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") + logger.info( + f"Updated std_daq config for key {cfg_key} from {old_value} to {value}" + ) def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" @@ -198,7 +204,9 @@ class Eiger9mCsaxs(DetectorBase): energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] if setp_energy != energy: self.cam.beam_energy.set(setp_energy) # .wait() - threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name][ + "value" + ] if not np.isclose(setp_energy / 2, threshold, rtol=0.05): self.cam.threshold_energy.set(setp_energy / 2) # .wait() @@ -259,7 +267,9 @@ class Eiger9mCsaxs(DetectorBase): self.arm_acquisition() logger.info("Waiting for detector to be armed") while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name][ + "value" + ] if det_ctrl == int(DetectorState.RUNNING): break time.sleep(0.005) @@ -279,13 +289,16 @@ class Eiger9mCsaxs(DetectorBase): logger.info("Waiting for std daq to receive images") while True: det_ctrl = self.std_client.get_status()["acquisition"]["state"] + # TODO if no writing was performed before if det_ctrl == "FINISHED": break time.sleep(0.005) # Message to BEC state = True - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + msg = BECMessage.FileMessage( + file_path=self.filepath, done=True, successful=state + ) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 12944f4..1174706 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -131,7 +131,7 @@ class McsCsaxs(SIS38XX): bec_utils.ConfigSignal, name="num_lines", kind="config", - config_storage_name="mcs_configs", + config_storage_name="mcs_config", ) def __init__( @@ -145,11 +145,15 @@ class McsCsaxs(SIS38XX): parent=None, device_manager=None, sim_mode=False, + mcs_config = None, **kwargs, ): - self.mcs_configs = { + + self.mcs_config = { f"{name}_num_lines": 1, } + if mcs_config is not None: + [self.mcs_config.update({f'{name}_{key}' : value}) for key, value in mcs_config.items()] super().__init__( prefix=prefix, @@ -231,6 +235,9 @@ class McsCsaxs(SIS38XX): self._acquisition_done = True self._send_data_to_bec() self.stop_all.put(1, use_complete=False) + self._send_data_to_bec() + self.erase_all.set(1) + return self.erase_start.set(1) self._send_data_to_bec() self.mca_data = defaultdict(lambda: []) @@ -328,7 +335,7 @@ class McsCsaxs(SIS38XX): break time.sleep(0.005) logger.info("mcs is ready and running") - + time.sleep(5) return super().stage() def unstage(self) -> List[object]: @@ -361,6 +368,7 @@ class McsCsaxs(SIS38XX): self.stop_all.set(1) # self.erase_all.set(1) self._stopped = True + self._acquisition_done = True super().stop(success=success) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index 9400ae4..61d9f00 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -49,6 +49,9 @@ class GalilController(Controller): "galil_show_all", "socket_put_and_receive", "socket_put_confirmed", + "sgalil_reference", + "fly_grid_scan", + "read_encoder_position", ] def __init__( @@ -152,7 +155,7 @@ class GalilController(Controller): def stop_all_axes(self) -> str: # return self.socket_put_and_receive(f"XQ#STOP,1") # Command stops all threads and motors! - return self.socket_put_and_receive(f"AB") + return self.socket_put_and_receive(f"ST") def axis_is_referenced(self) -> bool: return bool(float(self.socket_put_and_receive(f"MG allaxref").strip())) @@ -268,14 +271,23 @@ class GalilController(Controller): """ # - axes_referenced = self.controller.axis_is_referenced() + axes_referenced = self.axis_is_referenced() + sign_y = self._axis[ord("c") - 97].sign + sign_x = self._axis[ord("e") - 97].sign # Check limits # TODO check sign of stage, or not necessary check_values = [start_y, end_y, start_x, end_x] for val in check_values: self.check_value(val) - speed = np.abs(end_y - start_y) / ((interval_y) * exp_time + (interval_y - 1) * readtime) + start_x *= sign_x + end_x *= sign_x + start_y *= sign_y + end_y *= sign_y + + speed = np.abs(end_y - start_y) / ( + (interval_y) * exp_time + (interval_y - 1) * readtime + ) if speed > 2.00 or speed < 0.02: raise LimitError( f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s" @@ -287,7 +299,9 @@ class GalilController(Controller): # Hard coded to maximum offset of 0.1mm to avoid long motions. self.socket_put_and_receive(f"off={(0):f}") - self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}") + self.socket_put_and_receive( + f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}" + ) self.socket_put_and_receive( f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}" ) @@ -307,7 +321,9 @@ class GalilController(Controller): val_axis4 = [] # x axis while self.is_thread_active(thread_id): posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) - logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}") + logger.info( + f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}" + ) time.sleep(1) if posct > last_readout: positions = self.read_encoder_position(last_readout, posct) @@ -318,7 +334,9 @@ class GalilController(Controller): time.sleep(1) # Readout of last positions after scan finished posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) - logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}") + logger.info( + f"SGalil is scanning - latest enconder position {posct} from {n_samples}" + ) if posct > last_readout: positions = self.read_encoder_position(last_readout, posct) val_axis4.extend(positions[0]) @@ -330,7 +348,9 @@ class GalilController(Controller): val_axis2 = [] # y axis val_axis4 = [] # x axis for ii in range(fromval, toval + 1): - rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10") + rts = self.socket_put_and_receive( + f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10" + ) if rts == ":": val_axis4.append(rts) val_axis2.append(rts) @@ -369,11 +389,15 @@ class GalilReadbackSignal(GalilSignalRO): """ if self.parent.axis_Id_numeric == 2: current_pos = float( - self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm") + self.controller.socket_put_and_receive( + f"MG _TP{self.parent.axis_Id}/mm" + ) ) elif self.parent.axis_Id_numeric == 4: # hardware controller readback from axis 4 is on axis 0, A instead of E - current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm")) + current_pos = float( + self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm") + ) current_pos *= self.parent.sign return current_pos @@ -419,11 +443,17 @@ class GalilSetpointSignal(GalilSignalBase): time.sleep(0.1) if self.parent.axis_Id_numeric == 2: - self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm") + self.controller.socket_put_confirmed( + f"PA{self.parent.axis_Id}={target_val:.4f}*mm" + ) self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}") elif self.parent.axis_Id_numeric == 4: - self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}") - self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}") + self.controller.socket_put_confirmed( + f"targ{self.parent.axis_Id}={target_val:.4f}" + ) + self.controller.socket_put_and_receive( + f"XQ#POSE,{self.parent.axis_Id_numeric}" + ) while self.controller.is_thread_active(0): time.sleep(0.005) @@ -432,7 +462,9 @@ class GalilMotorIsMoving(GalilSignalRO): @threadlocked def _socket_get(self): if self.parent.axis_Id_numeric == 2: - ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric) + ret = self.controller.is_axis_moving( + self.parent.axis_Id, self.parent.axis_Id_numeric + ) return ret if self.parent.axis_Id_numeric == 4: # Motion signal from axis 4 is mapped to axis 5 @@ -470,8 +502,12 @@ class SGalilMotor(Device, PositionerBase): kind="hinted", ) user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint") - motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") - all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config") + motor_is_moving = Cpt( + GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal" + ) + all_axes_referenced = Cpt( + GalilAxesReferenced, signal_name="all_axes_referenced", kind="config" + ) high_limit_travel = Cpt(Signal, value=0, kind="omitted") low_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -652,7 +688,9 @@ class SGalilMotor(Device, PositionerBase): def axis_Id_numeric(self, val): if isinstance(val, int): if val not in [2, 4]: - raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.") + raise ValueError( + f"Numeric value {val} is not supported, it must be either 2 or 4." + ) self._axis_Id_alpha = val self._axis_Id_numeric = (chr(val + 97)).capitalize() else: @@ -676,7 +714,11 @@ if __name__ == "__main__": else: from ophyd_devices.utils.socket import SocketMock - samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock) - samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock) + samx = SGalilMotor( + "E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock + ) + samy = SGalilMotor( + "C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock + ) samx.controller.galil_show_all() From 2dd8f25c8727759a8cf98a0abee87e379c9307d7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 4 Sep 2023 20:50:14 +0200 Subject: [PATCH 42/53] fix: bugfix in delaygenerators --- .../epics/devices/DelayGeneratorDG645.py | 131 +++++++++++++----- 1 file changed, 96 insertions(+), 35 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index f7048c4..1338c19 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -147,7 +147,12 @@ class DelayGeneratorDG645(Device): "reload_config", ] - state = Component(EpicsSignalRO, "EventStatusLI", name="status_register") + trigger_burst_readout = Component( + EpicsSignal, "EventStatusLI.PROC", name="read_burst_state" + ) + burst_cycle_finished = Component( + EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state" + ) status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -297,6 +302,13 @@ class DelayGeneratorDG645(Device): config_storage_name="ddg_config", ) + set_trigger_source = Component( + bec_utils.ConfigSignal, + name="set_trigger_source", + kind="config", + config_storage_name="ddg_config", + ) + def __init__( self, prefix="", @@ -308,7 +320,7 @@ class DelayGeneratorDG645(Device): parent=None, device_manager=None, sim_mode=False, - ddg_config = None, + ddg_config=None, **kwargs, ): """_summary_ @@ -329,6 +341,9 @@ class DelayGeneratorDG645(Device): delay_burst (_type_, float): Add delay for triggering in software trigger mode to allow fast shutter to open. Defaults to 0. delta_width (_type_, float): Add width to fast shutter signal to make sure its open during acquisition. Defaults to 0. delta_triggers (_type_, int): Add additional triggers to burst mode (mcs card needs +1 triggers per line). Defaults to 0. + set_high_on_exposure + set_high_on_stage + set_trigger_source """ self.ddg_config = { f"{name}_delay_burst": 0, @@ -340,9 +355,13 @@ class DelayGeneratorDG645(Device): f"{name}_thres_trig_level": 2.5, f"{name}_set_high_on_exposure": False, f"{name}_set_high_on_stage": False, + f"{name}_set_trigger_source": "SINGLE_SHOT", } if ddg_config is not None: - [self.ddg_config.update({f'{name}_{key}' : value}) for key, value in ddg_config.items()] + [ + self.ddg_config.update({f"{name}_{key}": value}) + for key, value in ddg_config.items() + ] super().__init__( prefix=prefix, name=name, @@ -372,9 +391,10 @@ class DelayGeneratorDG645(Device): ] self._all_delay_pairs = ["AB", "CD", "EF", "GH"] self.wait_for_connection() # Make sure to be connected before talking to PVs - logger.info(f'Current polarity value {self.polarity.get()}') + logger.info(f"Current polarity values {self.polarity.get()}") self.reload_config() self._ddg_is_okay() + self._stopped = False def _set_trigger(self, trigger_source: TriggerSource) -> None: """Set trigger source to value of list below, or string @@ -414,14 +434,11 @@ class DelayGeneratorDG645(Device): getattr(channel.io, signal).set(value) def _cleanup_ddg(self) -> None: - self._set_trigger(TriggerSource.SINGLE_SHOT) + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) def reload_config(self) -> None: - self.set_channels( - "polarity", - self.polarity.get(), - channels=["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"], - ) + for ii, channel in enumerate(self._all_channels): + self.set_channels("polarity", self.polarity.get()[ii], channels=[channel]) # Set polarity for eiger inverted! # self.set_channels("polarity", 0, channels=["channelAB"]) self.set_channels("amplitude", self.amplitude.get()) @@ -441,39 +458,45 @@ class DelayGeneratorDG645(Device): 0, [f"channel{self._all_delay_pairs[ii]}.ch2"], ) - self._set_trigger(TriggerSource.SINGLE_SHOT) + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) # Set threshold level for ext. pulses self.level.set(self.thres_trig_level.get()) + def _check_burst_cycle(self) -> None: + "''Checks burst cycle of delay generator''" + while True: + self.trigger_burst_readout.set(1) + if ( + self.burst_cycle_finished.read()[self.burst_cycle_finished.name][ + "value" + ] + == 1 + ): + self._acquisition_done = True + return + if self._stopped == True: + return + time.sleep(0.01) + + def stop(self, success=False): + """Stops the DDG""" + self._stopped = True + self._acquisition_done = True + super().stop(success=success) + def stage(self): """Trigger the generator by arming to accept triggers""" self.scaninfo.load_scan_metadata() if self.scaninfo.scan_type == "step": # define parameters - self._set_trigger(TriggerSource.SINGLE_SHOT) - exp_time = self.delta_width.get() + self.scaninfo.exp_time - delay_burst = self.delay_burst.get() - num_burst_cycle = 1 + self.additional_triggers.get() - # set parameters in DDG - self.burst_enable(num_burst_cycle, delay_burst, exp_time, config="first") - self.set_channels("delay", 0) - # Set burst length to half of the experimental time! - self.set_channels("width", exp_time) - elif self.scaninfo.scan_type == "fly": - # Prepare FSH DDG + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) if self.set_high_on_exposure.get(): - # define parameters - self._set_trigger(TriggerSource.SINGLE_SHOT) - exp_time = ( - self.delta_width.get() - + self.scaninfo.exp_time * self.scaninfo.num_frames - + self.scaninfo.readout_time * (self.scaninfo.num_frames - 1) + num_burst_cycle = 1 + exp_time = self.delta_width.get() + self.scaninfo.num_frames * ( + self.scaninfo.exp_time + self.scaninfo.readout_time ) total_exposure = exp_time delay_burst = self.delay_burst.get() - # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. - num_burst_cycle = 1 + self.additional_triggers.get() - # set parameters in DDG self.burst_enable( num_burst_cycle, delay_burst, total_exposure, config="first" ) @@ -481,8 +504,6 @@ class DelayGeneratorDG645(Device): # Set burst length to half of the experimental time! self.set_channels("width", exp_time) else: - # define parameters - self._set_trigger(TriggerSource.SINGLE_SHOT) exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() @@ -496,20 +517,60 @@ class DelayGeneratorDG645(Device): self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) + elif self.scaninfo.scan_type == "fly": + # Prepare FSH DDG + if self.set_high_on_exposure.get(): + # define parameters + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) + exp_time = ( + self.delta_width.get() + + self.scaninfo.exp_time * self.scaninfo.num_frames + + self.scaninfo.readout_time * (self.scaninfo.num_frames - 1) + ) + total_exposure = exp_time + delay_burst = self.delay_burst.get() + # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. + num_burst_cycle = 1 + self.additional_triggers.get() + # set parameters in DDG + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) + self.set_channels("delay", 0.0) + # Set burst length to half of the experimental time! + self.set_channels("width", exp_time) + else: + # define parameters + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) + exp_time = self.delta_width.get() + self.scaninfo.exp_time + total_exposure = exp_time + self.scaninfo.readout_time + delay_burst = self.delay_burst.get() + num_burst_cycle = ( + self.scaninfo.num_frames + self.additional_triggers.get() + ) + # set parameters in DDG + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) + self.set_channels("delay", 0.0) + # Set burst length to half of the experimental time! + self.set_channels("width", exp_time) else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") # Check status self._ddg_is_okay() - + logger.info("DDG staged") super().stage() def unstage(self): """Stop the trigger generator from accepting triggers""" - self._set_trigger(TriggerSource.SINGLE_SHOT) + # self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) + self._check_burst_cycle() # Check status self._ddg_is_okay() + self._stopped = False + self._acquisition_done = False super().unstage() def trigger(self) -> None: From ba9cb77ed9b0d1a7e0f744558360c90f393b6f08 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 4 Sep 2023 20:50:42 +0200 Subject: [PATCH 43/53] fix: bugfix online fixes --- .../epics/devices/bec_scaninfo_mixin.py | 4 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 24 ++----- ophyd_devices/epics/devices/mcs_csaxs.py | 21 ++---- ophyd_devices/galil/sgalil_ophyd.py | 64 +++++-------------- 4 files changed, 31 insertions(+), 82 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index ca0026e..c5f8b65 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -53,7 +53,9 @@ class BecScaninfoMixin: self.scanID = scan_msg.content["scanID"] self.scan_number = scan_msg.content["info"]["scan_number"] self.exp_time = scan_msg.content["info"]["exp_time"] - self.num_frames = scan_msg.content["info"]["num_points"] + self.num_frames = ( + scan_msg.content["info"]["num_points"] * scan_msg.content["info"]["frames_per_trigger"] + ) self.scan_type = scan_msg.content["info"].get("scan_type", "step") self.readout_time = scan_msg.content["info"]["readout_time"] self.username = self._get_username() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 6cadde1..c283665 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -126,9 +126,7 @@ class Eiger9mCsaxs(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise EigerError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs @@ -142,9 +140,7 @@ class Eiger9mCsaxs(DetectorBase): # TODO self.filepath = "" self.scaninfo.username = "e21206" - self.service_cfg = { - "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" - } + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -172,9 +168,7 @@ class Eiger9mCsaxs(DetectorBase): f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}" ) cfg.update({cfg_key: value}) - logger.info( - f"Updated std_daq config for key {cfg_key} from {old_value} to {value}" - ) + logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" @@ -204,9 +198,7 @@ class Eiger9mCsaxs(DetectorBase): energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] if setp_energy != energy: self.cam.beam_energy.set(setp_energy) # .wait() - threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name][ - "value" - ] + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] if not np.isclose(setp_energy / 2, threshold, rtol=0.05): self.cam.threshold_energy.set(setp_energy / 2) # .wait() @@ -267,9 +259,7 @@ class Eiger9mCsaxs(DetectorBase): self.arm_acquisition() logger.info("Waiting for detector to be armed") while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name][ - "value" - ] + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): break time.sleep(0.005) @@ -296,9 +286,7 @@ class Eiger9mCsaxs(DetectorBase): # Message to BEC state = True - msg = BECMessage.FileMessage( - file_path=self.filepath, done=True, successful=state - ) + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 1174706..9b8bc43 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -145,15 +145,14 @@ class McsCsaxs(SIS38XX): parent=None, device_manager=None, sim_mode=False, - mcs_config = None, + mcs_config=None, **kwargs, ): - self.mcs_config = { f"{name}_num_lines": 1, } if mcs_config is not None: - [self.mcs_config.update({f'{name}_{key}' : value}) for key, value in mcs_config.items()] + [self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()] super().__init__( prefix=prefix, @@ -166,9 +165,7 @@ class McsCsaxs(SIS38XX): ) if device_manager is None and not sim_mode: - raise MCSError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name self._stream_ttl = 1800 @@ -185,9 +182,7 @@ class McsCsaxs(SIS38XX): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.scaninfo.username = "e21206" - self.service_cfg = { - "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" - } + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False self._acquisition_done = False @@ -209,9 +204,7 @@ class McsCsaxs(SIS38XX): self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) self.count_on_start.set(0) - self.mca_names = [ - signal for signal in self.component_names if signal.startswith("mca") - ] + self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")] self.mca_data = defaultdict(lambda: []) for mca in self.mca_names: signal = getattr(self, mca) @@ -270,9 +263,7 @@ class McsCsaxs(SIS38XX): def _set_acquisition_params(self) -> None: n_points = self.scaninfo.num_frames / int(self.num_lines.get()) if n_points > 10000: - raise MCSError( - f"Requested number of points {n_points} exceeds hardware limit of 10000" - ) + raise MCSError(f"Requested number of points {n_points} exceeds hardware limit of 10000") self.num_use_all.set(n_points) self.preset_real.set(0) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index 61d9f00..7bf1b44 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -285,9 +285,7 @@ class GalilController(Controller): start_y *= sign_y end_y *= sign_y - speed = np.abs(end_y - start_y) / ( - (interval_y) * exp_time + (interval_y - 1) * readtime - ) + speed = np.abs(end_y - start_y) / ((interval_y) * exp_time + (interval_y - 1) * readtime) if speed > 2.00 or speed < 0.02: raise LimitError( f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s" @@ -299,9 +297,7 @@ class GalilController(Controller): # Hard coded to maximum offset of 0.1mm to avoid long motions. self.socket_put_and_receive(f"off={(0):f}") - self.socket_put_and_receive( - f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}" - ) + self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}") self.socket_put_and_receive( f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}" ) @@ -321,9 +317,7 @@ class GalilController(Controller): val_axis4 = [] # x axis while self.is_thread_active(thread_id): posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) - logger.info( - f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}" - ) + logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}") time.sleep(1) if posct > last_readout: positions = self.read_encoder_position(last_readout, posct) @@ -334,9 +328,7 @@ class GalilController(Controller): time.sleep(1) # Readout of last positions after scan finished posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0]) - logger.info( - f"SGalil is scanning - latest enconder position {posct} from {n_samples}" - ) + logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}") if posct > last_readout: positions = self.read_encoder_position(last_readout, posct) val_axis4.extend(positions[0]) @@ -348,9 +340,7 @@ class GalilController(Controller): val_axis2 = [] # y axis val_axis4 = [] # x axis for ii in range(fromval, toval + 1): - rts = self.socket_put_and_receive( - f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10" - ) + rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10") if rts == ":": val_axis4.append(rts) val_axis2.append(rts) @@ -389,15 +379,11 @@ class GalilReadbackSignal(GalilSignalRO): """ if self.parent.axis_Id_numeric == 2: current_pos = float( - self.controller.socket_put_and_receive( - f"MG _TP{self.parent.axis_Id}/mm" - ) + self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm") ) elif self.parent.axis_Id_numeric == 4: # hardware controller readback from axis 4 is on axis 0, A instead of E - current_pos = float( - self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm") - ) + current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm")) current_pos *= self.parent.sign return current_pos @@ -443,17 +429,11 @@ class GalilSetpointSignal(GalilSignalBase): time.sleep(0.1) if self.parent.axis_Id_numeric == 2: - self.controller.socket_put_confirmed( - f"PA{self.parent.axis_Id}={target_val:.4f}*mm" - ) + self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm") self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}") elif self.parent.axis_Id_numeric == 4: - self.controller.socket_put_confirmed( - f"targ{self.parent.axis_Id}={target_val:.4f}" - ) - self.controller.socket_put_and_receive( - f"XQ#POSE,{self.parent.axis_Id_numeric}" - ) + self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}") + self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}") while self.controller.is_thread_active(0): time.sleep(0.005) @@ -462,9 +442,7 @@ class GalilMotorIsMoving(GalilSignalRO): @threadlocked def _socket_get(self): if self.parent.axis_Id_numeric == 2: - ret = self.controller.is_axis_moving( - self.parent.axis_Id, self.parent.axis_Id_numeric - ) + ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric) return ret if self.parent.axis_Id_numeric == 4: # Motion signal from axis 4 is mapped to axis 5 @@ -502,12 +480,8 @@ class SGalilMotor(Device, PositionerBase): kind="hinted", ) user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint") - motor_is_moving = Cpt( - GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal" - ) - all_axes_referenced = Cpt( - GalilAxesReferenced, signal_name="all_axes_referenced", kind="config" - ) + motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") + all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config") high_limit_travel = Cpt(Signal, value=0, kind="omitted") low_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -688,9 +662,7 @@ class SGalilMotor(Device, PositionerBase): def axis_Id_numeric(self, val): if isinstance(val, int): if val not in [2, 4]: - raise ValueError( - f"Numeric value {val} is not supported, it must be either 2 or 4." - ) + raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.") self._axis_Id_alpha = val self._axis_Id_numeric = (chr(val + 97)).capitalize() else: @@ -714,11 +686,7 @@ if __name__ == "__main__": else: from ophyd_devices.utils.socket import SocketMock - samx = SGalilMotor( - "E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock - ) - samy = SGalilMotor( - "C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock - ) + samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock) + samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock) samx.controller.galil_show_all() From b3237ceda5468058e294da4a3e608c4344e582dc Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 4 Sep 2023 20:52:05 +0200 Subject: [PATCH 44/53] fix: fix ddg code --- .../epics/devices/DelayGeneratorDG645.py | 77 +++++-------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 1338c19..27bd113 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -61,9 +61,7 @@ class DummyPositioner(PVPositioner): setpoint = Component(EpicsSignal, "DelayAO", put_complete=True, kind=Kind.config) readback = Component(EpicsSignalRO, "DelayAI", kind=Kind.config) done = Component(Signal, value=1) - reference = Component( - EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config - ) + reference = Component(EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config) class DelayPair(PseudoPositioner): @@ -93,16 +91,12 @@ class DelayPair(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): """Run a forward (pseudo -> real) calculation""" - return self.RealPosition( - ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width - ) + return self.RealPosition(ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width) @real_position_argument def inverse(self, real_pos): """Run an inverse (real -> pseudo) calculation""" - return self.PseudoPosition( - delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1 - ) + return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) class TriggerSource(int, enum.Enum): @@ -147,12 +141,8 @@ class DelayGeneratorDG645(Device): "reload_config", ] - trigger_burst_readout = Component( - EpicsSignal, "EventStatusLI.PROC", name="read_burst_state" - ) - burst_cycle_finished = Component( - EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state" - ) + trigger_burst_readout = Component(EpicsSignal, "EventStatusLI.PROC", name="read_burst_state") + burst_cycle_finished = Component(EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state") status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -199,9 +189,7 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) - trigger_shot = Component( - EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config" - ) + trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") # Burst mode burstMode = Component( EpicsSignal, @@ -358,10 +346,7 @@ class DelayGeneratorDG645(Device): f"{name}_set_trigger_source": "SINGLE_SHOT", } if ddg_config is not None: - [ - self.ddg_config.update({f"{name}_{key}": value}) - for key, value in ddg_config.items() - ] + [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] super().__init__( prefix=prefix, name=name, @@ -372,9 +357,7 @@ class DelayGeneratorDG645(Device): **kwargs, ) if device_manager is None and not sim_mode: - raise DDGError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise DDGError("Add DeviceManager to initialization or init with sim_mode=True") self.device_manager = device_manager if not sim_mode: self._producer = self.device_manager.producer @@ -447,10 +430,7 @@ class DelayGeneratorDG645(Device): self.set_channels( "reference", 0, - [ - f"channel{self._all_delay_pairs[ii]}.ch1" - for ii in range(len(self._all_delay_pairs)) - ], + [f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))], ) for ii in range(len(self._all_delay_pairs)): self.set_channels( @@ -463,15 +443,12 @@ class DelayGeneratorDG645(Device): self.level.set(self.thres_trig_level.get()) def _check_burst_cycle(self) -> None: - "''Checks burst cycle of delay generator''" + """Checks burst cycle of delay generator + Force readout, return value from end of burst cycle + """ while True: self.trigger_burst_readout.set(1) - if ( - self.burst_cycle_finished.read()[self.burst_cycle_finished.name][ - "value" - ] - == 1 - ): + if self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1: self._acquisition_done = True return if self._stopped == True: @@ -497,9 +474,7 @@ class DelayGeneratorDG645(Device): ) total_exposure = exp_time delay_burst = self.delay_burst.get() - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -507,13 +482,9 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = ( - self.scaninfo.num_frames + self.additional_triggers.get() - ) + num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -532,9 +503,7 @@ class DelayGeneratorDG645(Device): # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -544,13 +513,9 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = ( - self.scaninfo.num_frames + self.additional_triggers.get() - ) + num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -575,9 +540,7 @@ class DelayGeneratorDG645(Device): def trigger(self) -> None: # if self.scaninfo.scan_type == "step": - if self.source.read()[self.source.name]["value"] == int( - TriggerSource.SINGLE_SHOT - ): + if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() super().trigger() From 8ad3eb29b79a0a8a742d1bc319cfedf60fcc150f Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 5 Sep 2023 00:24:04 +0200 Subject: [PATCH 45/53] fix: working mcs readout --- .../epics/devices/DelayGeneratorDG645.py | 75 +++++++++++++----- .../epics/devices/bec_scaninfo_mixin.py | 8 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 34 ++++++--- ophyd_devices/epics/devices/mcs_csaxs.py | 76 +++++++++---------- 4 files changed, 124 insertions(+), 69 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 27bd113..224f773 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -61,7 +61,9 @@ class DummyPositioner(PVPositioner): setpoint = Component(EpicsSignal, "DelayAO", put_complete=True, kind=Kind.config) readback = Component(EpicsSignalRO, "DelayAI", kind=Kind.config) done = Component(Signal, value=1) - reference = Component(EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config) + reference = Component( + EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config + ) class DelayPair(PseudoPositioner): @@ -91,12 +93,16 @@ class DelayPair(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): """Run a forward (pseudo -> real) calculation""" - return self.RealPosition(ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width) + return self.RealPosition( + ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width + ) @real_position_argument def inverse(self, real_pos): """Run an inverse (real -> pseudo) calculation""" - return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) + return self.PseudoPosition( + delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1 + ) class TriggerSource(int, enum.Enum): @@ -141,8 +147,12 @@ class DelayGeneratorDG645(Device): "reload_config", ] - trigger_burst_readout = Component(EpicsSignal, "EventStatusLI.PROC", name="read_burst_state") - burst_cycle_finished = Component(EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state") + trigger_burst_readout = Component( + EpicsSignal, "EventStatusLI.PROC", name="read_burst_state" + ) + burst_cycle_finished = Component( + EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state" + ) status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -189,7 +199,9 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) - trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") + trigger_shot = Component( + EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config" + ) # Burst mode burstMode = Component( EpicsSignal, @@ -337,7 +349,7 @@ class DelayGeneratorDG645(Device): f"{name}_delay_burst": 0, f"{name}_delta_width": 0, f"{name}_additional_triggers": 0, - f"{name}_polarity": 1, + f"{name}_polarity": [1, 1, 1, 1, 1], f"{name}_amplitude": 4.5, f"{name}_offset": 0, f"{name}_thres_trig_level": 2.5, @@ -346,7 +358,10 @@ class DelayGeneratorDG645(Device): f"{name}_set_trigger_source": "SINGLE_SHOT", } if ddg_config is not None: - [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] + [ + self.ddg_config.update({f"{name}_{key}": value}) + for key, value in ddg_config.items() + ] super().__init__( prefix=prefix, name=name, @@ -357,7 +372,9 @@ class DelayGeneratorDG645(Device): **kwargs, ) if device_manager is None and not sim_mode: - raise DDGError("Add DeviceManager to initialization or init with sim_mode=True") + raise DDGError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.device_manager = device_manager if not sim_mode: self._producer = self.device_manager.producer @@ -430,7 +447,10 @@ class DelayGeneratorDG645(Device): self.set_channels( "reference", 0, - [f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))], + [ + f"channel{self._all_delay_pairs[ii]}.ch1" + for ii in range(len(self._all_delay_pairs)) + ], ) for ii in range(len(self._all_delay_pairs)): self.set_channels( @@ -448,7 +468,12 @@ class DelayGeneratorDG645(Device): """ while True: self.trigger_burst_readout.set(1) - if self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1: + if ( + self.burst_cycle_finished.read()[self.burst_cycle_finished.name][ + "value" + ] + == 1 + ): self._acquisition_done = True return if self._stopped == True: @@ -474,7 +499,9 @@ class DelayGeneratorDG645(Device): ) total_exposure = exp_time delay_burst = self.delay_burst.get() - self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -482,9 +509,13 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + num_burst_cycle = ( + self.scaninfo.num_frames + self.additional_triggers.get() + ) # set parameters in DDG - self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -503,7 +534,9 @@ class DelayGeneratorDG645(Device): # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG - self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -513,9 +546,13 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + num_burst_cycle = ( + self.scaninfo.num_frames + self.additional_triggers.get() + ) # set parameters in DDG - self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.burst_enable( + num_burst_cycle, delay_burst, total_exposure, config="first" + ) self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -540,7 +577,9 @@ class DelayGeneratorDG645(Device): def trigger(self) -> None: # if self.scaninfo.scan_type == "step": - if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): + if self.source.read()[self.source.name]["value"] == int( + TriggerSource.SINGLE_SHOT + ): self.trigger_shot.set(1).wait() super().trigger() diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index c5f8b65..179da3d 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -4,7 +4,9 @@ from bec_lib.core import DeviceManagerBase, BECMessage, MessageEndpoints class BecScaninfoMixin: - def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: + def __init__( + self, device_manager: DeviceManagerBase = None, sim_mode=False + ) -> None: self.device_manager = device_manager self.sim_mode = sim_mode self.scan_msg = None @@ -18,6 +20,7 @@ class BecScaninfoMixin: "readout_time": 2e-3, "scan_type": "fly", "num_lines": 10, + "frames_per_trigger": 1, } def get_bec_info_msg(self) -> None: @@ -54,7 +57,8 @@ class BecScaninfoMixin: self.scan_number = scan_msg.content["info"]["scan_number"] self.exp_time = scan_msg.content["info"]["exp_time"] self.num_frames = ( - scan_msg.content["info"]["num_points"] * scan_msg.content["info"]["frames_per_trigger"] + scan_msg.content["info"]["num_points"] + * scan_msg.content["info"]["frames_per_trigger"] ) self.scan_type = scan_msg.content["info"].get("scan_type", "step") self.readout_time = scan_msg.content["info"]["readout_time"] diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index c283665..f5a6293 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -126,7 +126,9 @@ class Eiger9mCsaxs(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") + raise EigerError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs @@ -140,7 +142,9 @@ class Eiger9mCsaxs(DetectorBase): # TODO self.filepath = "" self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = { + "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" + } self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -168,7 +172,9 @@ class Eiger9mCsaxs(DetectorBase): f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}" ) cfg.update({cfg_key: value}) - logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") + logger.info( + f"Updated std_daq config for key {cfg_key} from {old_value} to {value}" + ) def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" @@ -180,9 +186,12 @@ class Eiger9mCsaxs(DetectorBase): time.sleep(0.1) timeout = timeout + 0.1 if timeout > 2: - raise EigerError( - f"Std client not in READY state, returns: {self.std_client.get_status()}" - ) + if not self.std_client.get_status()["state"]: + raise EigerError( + f"Std client not in READY state, returns: {self.std_client.get_status()}" + ) + else: + return def _prep_det(self) -> None: self._set_det_threshold() @@ -198,7 +207,9 @@ class Eiger9mCsaxs(DetectorBase): energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] if setp_energy != energy: self.cam.beam_energy.set(setp_energy) # .wait() - threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name][ + "value" + ] if not np.isclose(setp_energy / 2, threshold, rtol=0.05): self.cam.threshold_energy.set(setp_energy / 2) # .wait() @@ -225,6 +236,7 @@ class Eiger9mCsaxs(DetectorBase): self.filepath = self.filewriter.compile_full_filename( self.scaninfo.scan_number, "eiger.h5", 1000, 5, True ) + # self._close_file_writer() logger.info(f" std_daq output filepath {self.filepath}") self.std_client.start_writer_async( {"output_file": self.filepath, "n_images": self.scaninfo.num_frames} @@ -259,7 +271,9 @@ class Eiger9mCsaxs(DetectorBase): self.arm_acquisition() logger.info("Waiting for detector to be armed") while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name][ + "value" + ] if det_ctrl == int(DetectorState.RUNNING): break time.sleep(0.005) @@ -286,7 +300,9 @@ class Eiger9mCsaxs(DetectorBase): # Message to BEC state = True - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) + msg = BECMessage.FileMessage( + file_path=self.filepath, done=True, successful=state + ) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 9b8bc43..59f1147 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -152,7 +152,10 @@ class McsCsaxs(SIS38XX): f"{name}_num_lines": 1, } if mcs_config is not None: - [self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()] + [ + self.mcs_config.update({f"{name}_{key}": value}) + for key, value in mcs_config.items() + ] super().__init__( prefix=prefix, @@ -165,7 +168,9 @@ class McsCsaxs(SIS38XX): ) if device_manager is None and not sim_mode: - raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") + raise MCSError( + "Add DeviceManager to initialization or init with sim_mode=True" + ) self.name = name self._stream_ttl = 1800 @@ -182,7 +187,9 @@ class McsCsaxs(SIS38XX): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = { + "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" + } self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False self._acquisition_done = False @@ -203,8 +210,11 @@ class McsCsaxs(SIS38XX): self.mux_output.set(5) self._set_trigger(TriggerSource.MODE3) self.input_polarity.set(0) + self.output_polarity.set(1) self.count_on_start.set(0) - self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")] + self.mca_names = [ + signal for signal in self.component_names if signal.startswith("mca") + ] self.mca_data = defaultdict(lambda: []) for mca in self.mca_names: signal = getattr(self, mca) @@ -212,24 +222,28 @@ class McsCsaxs(SIS38XX): self._counter = 0 def _on_mca_data(self, *args, obj=None, **kwargs) -> None: - self.mca_data[obj.attr_name] = kwargs["value"] + if not isinstance(kwargs["value"], (list, np.ndarray)): + return + self.mca_data[obj.attr_name] = kwargs["value"][1:] if len(self.mca_names) != len(self.mca_data): return - ref_entry = self.mca_data[self.mca_names[0]] - if not ref_entry: - self.mca_data = defaultdict(lambda: []) - return - if isinstance(ref_entry, list) and not ref_entry: - return + # ref_entry = self.mca_data[self.mca_names[0]] + # if not ref_entry: + # self.mca_data = defaultdict(lambda: []) + # return + # if isinstance(ref_entry, list) and (ref_entry > 0): + # return self._updated = True - self.counter += 1 - if self.counter == self.num_lines.get(): + self._counter += 1 + if self._counter == self.num_lines.get(): self._acquisition_done = True self._send_data_to_bec() self.stop_all.put(1, use_complete=False) self._send_data_to_bec() self.erase_all.set(1) + # TODO how to make card wait for sure! + self._counter = 0 return self.erase_start.set(1) self._send_data_to_bec() @@ -245,6 +259,7 @@ class McsCsaxs(SIS38XX): "num_lines": self.num_lines.get(), } ) + logger.info(f"{self.mca_data}") msg = BECMessage.DeviceMessage( signals=dict(self.mca_data), metadata=self.scaninfo.scan_msg.metadata ).dumps() @@ -262,9 +277,11 @@ class McsCsaxs(SIS38XX): def _set_acquisition_params(self) -> None: n_points = self.scaninfo.num_frames / int(self.num_lines.get()) - if n_points > 10000: - raise MCSError(f"Requested number of points {n_points} exceeds hardware limit of 10000") - self.num_use_all.set(n_points) + if n_points > 9999: + raise MCSError( + f"Requested number of points {n_points+1} exceeds hardware limit of 10000 (n+1)" + ) + self.num_use_all.set(n_points + 1) self.preset_real.set(0) def _set_trigger(self, trigger_source: TriggerSource) -> None: @@ -284,29 +301,6 @@ class McsCsaxs(SIS38XX): def _force_readout_mcs_card(self) -> None: self.read_all.put(1, use_complete=False) - # TODO does not work anymore with new mca signals - # def readout_data(self) -> List[List]: - # """Manual readout of mca slots, returns list of lists""" - # self._force_readout_mcs_card() - # readback = [] - # for ii in range(1, int(self.mux_output.read()[self.mux_output.name]["value"]) + 1): - # readback.append(self._readout_mca_channels(ii)) - # return readback - - # def _readout_mca_channels(self, num: int) -> List: - # """readout of single mca channel""" - # signal = f"mca{num}" - # if signal in self.component_names: - # readback = f"{getattr(self, signal).name}_spectrum" - # return getattr(self, signal).read()[readback]["value"] - - # def _start_readout_loop(self) -> None: - # # stop acquisition and clean up data - # self.stop_all.set(1) - # self.erase_all.set(1) - # self._acquisition_done = True - # self._updated = False - def stage(self) -> List[object]: """stage the detector and file writer""" self.scaninfo.load_scan_metadata() @@ -348,7 +342,7 @@ class McsCsaxs(SIS38XX): Start: start_all Erase/Start: erase_start """ - self.counter = 0 + self._counter = 0 self.erase_start.set(1) # self.start_all.set(1) @@ -360,6 +354,7 @@ class McsCsaxs(SIS38XX): # self.erase_all.set(1) self._stopped = True self._acquisition_done = True + self._counter = 0 super().stop(success=success) @@ -367,3 +362,4 @@ class McsCsaxs(SIS38XX): if __name__ == "__main__": mcs = McsCsaxs(name="mcs", prefix="X12SA-MCS:", sim_mode=True) mcs.stage() + mcs.unstage() From 911c8a2438c9cdf1ca2a9685e1dbbbf4a1913f5c Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 5 Sep 2023 09:26:27 +0200 Subject: [PATCH 46/53] fix: online changes SAXS --- .../epics/devices/DelayGeneratorDG645.py | 73 +++++-------------- .../epics/devices/bec_scaninfo_mixin.py | 7 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 24 ++---- ophyd_devices/epics/devices/mcs_csaxs.py | 17 +---- 4 files changed, 29 insertions(+), 92 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 224f773..40cc546 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -61,9 +61,7 @@ class DummyPositioner(PVPositioner): setpoint = Component(EpicsSignal, "DelayAO", put_complete=True, kind=Kind.config) readback = Component(EpicsSignalRO, "DelayAI", kind=Kind.config) done = Component(Signal, value=1) - reference = Component( - EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config - ) + reference = Component(EpicsSignal, "ReferenceMO", put_complete=True, kind=Kind.config) class DelayPair(PseudoPositioner): @@ -93,16 +91,12 @@ class DelayPair(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): """Run a forward (pseudo -> real) calculation""" - return self.RealPosition( - ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width - ) + return self.RealPosition(ch1=pseudo_pos.delay, ch2=pseudo_pos.delay + pseudo_pos.width) @real_position_argument def inverse(self, real_pos): """Run an inverse (real -> pseudo) calculation""" - return self.PseudoPosition( - delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1 - ) + return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) class TriggerSource(int, enum.Enum): @@ -147,12 +141,8 @@ class DelayGeneratorDG645(Device): "reload_config", ] - trigger_burst_readout = Component( - EpicsSignal, "EventStatusLI.PROC", name="read_burst_state" - ) - burst_cycle_finished = Component( - EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state" - ) + trigger_burst_readout = Component(EpicsSignal, "EventStatusLI.PROC", name="read_burst_state") + burst_cycle_finished = Component(EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state") status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -199,9 +189,7 @@ class DelayGeneratorDG645(Device): name="trigger_rate", kind=Kind.config, ) - trigger_shot = Component( - EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config" - ) + trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") # Burst mode burstMode = Component( EpicsSignal, @@ -358,10 +346,7 @@ class DelayGeneratorDG645(Device): f"{name}_set_trigger_source": "SINGLE_SHOT", } if ddg_config is not None: - [ - self.ddg_config.update({f"{name}_{key}": value}) - for key, value in ddg_config.items() - ] + [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] super().__init__( prefix=prefix, name=name, @@ -372,9 +357,7 @@ class DelayGeneratorDG645(Device): **kwargs, ) if device_manager is None and not sim_mode: - raise DDGError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise DDGError("Add DeviceManager to initialization or init with sim_mode=True") self.device_manager = device_manager if not sim_mode: self._producer = self.device_manager.producer @@ -447,10 +430,7 @@ class DelayGeneratorDG645(Device): self.set_channels( "reference", 0, - [ - f"channel{self._all_delay_pairs[ii]}.ch1" - for ii in range(len(self._all_delay_pairs)) - ], + [f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))], ) for ii in range(len(self._all_delay_pairs)): self.set_channels( @@ -468,12 +448,7 @@ class DelayGeneratorDG645(Device): """ while True: self.trigger_burst_readout.set(1) - if ( - self.burst_cycle_finished.read()[self.burst_cycle_finished.name][ - "value" - ] - == 1 - ): + if self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1: self._acquisition_done = True return if self._stopped == True: @@ -499,9 +474,7 @@ class DelayGeneratorDG645(Device): ) total_exposure = exp_time delay_burst = self.delay_burst.get() - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -509,13 +482,9 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = ( - self.scaninfo.num_frames + self.additional_triggers.get() - ) + num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -534,9 +503,7 @@ class DelayGeneratorDG645(Device): # self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully.. num_burst_cycle = 1 + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -546,13 +513,9 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = ( - self.scaninfo.num_frames + self.additional_triggers.get() - ) + num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() # set parameters in DDG - self.burst_enable( - num_burst_cycle, delay_burst, total_exposure, config="first" - ) + self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) @@ -577,9 +540,7 @@ class DelayGeneratorDG645(Device): def trigger(self) -> None: # if self.scaninfo.scan_type == "step": - if self.source.read()[self.source.name]["value"] == int( - TriggerSource.SINGLE_SHOT - ): + if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() super().trigger() diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 179da3d..5c79e48 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -4,9 +4,7 @@ from bec_lib.core import DeviceManagerBase, BECMessage, MessageEndpoints class BecScaninfoMixin: - def __init__( - self, device_manager: DeviceManagerBase = None, sim_mode=False - ) -> None: + def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: self.device_manager = device_manager self.sim_mode = sim_mode self.scan_msg = None @@ -57,8 +55,7 @@ class BecScaninfoMixin: self.scan_number = scan_msg.content["info"]["scan_number"] self.exp_time = scan_msg.content["info"]["exp_time"] self.num_frames = ( - scan_msg.content["info"]["num_points"] - * scan_msg.content["info"]["frames_per_trigger"] + scan_msg.content["info"]["num_points"] * scan_msg.content["info"]["frames_per_trigger"] ) self.scan_type = scan_msg.content["info"].get("scan_type", "step") self.readout_time = scan_msg.content["info"]["readout_time"] diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index f5a6293..261d398 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -126,9 +126,7 @@ class Eiger9mCsaxs(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise EigerError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs @@ -142,9 +140,7 @@ class Eiger9mCsaxs(DetectorBase): # TODO self.filepath = "" self.scaninfo.username = "e21206" - self.service_cfg = { - "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" - } + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -172,9 +168,7 @@ class Eiger9mCsaxs(DetectorBase): f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}" ) cfg.update({cfg_key: value}) - logger.info( - f"Updated std_daq config for key {cfg_key} from {old_value} to {value}" - ) + logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") def _init_standard_daq(self) -> None: self.std_rest_server_url = "http://xbl-daq-29:5000" @@ -207,9 +201,7 @@ class Eiger9mCsaxs(DetectorBase): energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] if setp_energy != energy: self.cam.beam_energy.set(setp_energy) # .wait() - threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name][ - "value" - ] + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] if not np.isclose(setp_energy / 2, threshold, rtol=0.05): self.cam.threshold_energy.set(setp_energy / 2) # .wait() @@ -271,9 +263,7 @@ class Eiger9mCsaxs(DetectorBase): self.arm_acquisition() logger.info("Waiting for detector to be armed") while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name][ - "value" - ] + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): break time.sleep(0.005) @@ -300,9 +290,7 @@ class Eiger9mCsaxs(DetectorBase): # Message to BEC state = True - msg = BECMessage.FileMessage( - file_path=self.filepath, done=True, successful=state - ) + msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 59f1147..f00095d 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -152,10 +152,7 @@ class McsCsaxs(SIS38XX): f"{name}_num_lines": 1, } if mcs_config is not None: - [ - self.mcs_config.update({f"{name}_{key}": value}) - for key, value in mcs_config.items() - ] + [self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()] super().__init__( prefix=prefix, @@ -168,9 +165,7 @@ class McsCsaxs(SIS38XX): ) if device_manager is None and not sim_mode: - raise MCSError( - "Add DeviceManager to initialization or init with sim_mode=True" - ) + raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name self._stream_ttl = 1800 @@ -187,9 +182,7 @@ class McsCsaxs(SIS38XX): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.scaninfo.username = "e21206" - self.service_cfg = { - "base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" - } + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False self._acquisition_done = False @@ -212,9 +205,7 @@ class McsCsaxs(SIS38XX): self.input_polarity.set(0) self.output_polarity.set(1) self.count_on_start.set(0) - self.mca_names = [ - signal for signal in self.component_names if signal.startswith("mca") - ] + self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")] self.mca_data = defaultdict(lambda: []) for mca in self.mca_names: signal = getattr(self, mca) From 5ce6fbcbb92d2fafb6cfcb4bb7b1f5ee616140b8 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 5 Sep 2023 12:28:38 +0200 Subject: [PATCH 47/53] fix: DDG logic to wait for burst in trigger --- .../epics/devices/DelayGeneratorDG645.py | 49 ++++++++++++------- .../epics/devices/bec_scaninfo_mixin.py | 7 ++- ophyd_devices/epics/devices/eiger9m_csaxs.py | 11 ++++- ophyd_devices/epics/devices/mcs_csaxs.py | 16 ++++-- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 40cc546..4a897c2 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -1,8 +1,9 @@ import enum +import threading import time from typing import Any, List from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind -from ophyd import PVPositioner, Signal +from ophyd import PVPositioner, Signal, DeviceStatus from ophyd.pseudopos import ( pseudo_position_argument, real_position_argument, @@ -141,8 +142,11 @@ class DelayGeneratorDG645(Device): "reload_config", ] - trigger_burst_readout = Component(EpicsSignal, "EventStatusLI.PROC", name="read_burst_state") + trigger_burst_readout = Component( + EpicsSignal, "EventStatusLI.PROC", name="trigger_burst_readout" + ) burst_cycle_finished = Component(EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state") + delay_finished = Component(EpicsSignalRO, "EventStatusMBBID.B2", name="delay_finished") status = Component(EpicsSignalRO, "StatusSI", name="status") clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error") @@ -442,17 +446,23 @@ class DelayGeneratorDG645(Device): # Set threshold level for ext. pulses self.level.set(self.thres_trig_level.get()) - def _check_burst_cycle(self) -> None: + def _check_burst_cycle(self, status) -> None: """Checks burst cycle of delay generator Force readout, return value from end of burst cycle """ while True: self.trigger_burst_readout.set(1) - if self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1: + if ( + self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1 + and self.delay_finished.read()[self.delay_finished.name]["value"] == 1 + ): self._acquisition_done = True + status.set_finished() return if self._stopped == True: - return + status.set_finished() + break + time.sleep(0.01) def stop(self, success=False): @@ -466,11 +476,14 @@ class DelayGeneratorDG645(Device): self.scaninfo.load_scan_metadata() if self.scaninfo.scan_type == "step": # define parameters - self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) if self.set_high_on_exposure.get(): - num_burst_cycle = 1 - exp_time = self.delta_width.get() + self.scaninfo.num_frames * ( - self.scaninfo.exp_time + self.scaninfo.readout_time + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) + num_burst_cycle = 1 + self.additional_triggers.get() + exp_time = ( + self.delta_width.get() + + self.scaninfo.num_points + * self.scaninfo.frames_per_trigger + * (self.scaninfo.exp_time + self.scaninfo.readout_time) ) total_exposure = exp_time delay_burst = self.delay_burst.get() @@ -479,24 +492,24 @@ class DelayGeneratorDG645(Device): # Set burst length to half of the experimental time! self.set_channels("width", exp_time) else: + self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + num_burst_cycle = self.scaninfo.frames_per_trigger + self.additional_triggers.get() # set parameters in DDG self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! self.set_channels("width", exp_time) elif self.scaninfo.scan_type == "fly": - # Prepare FSH DDG if self.set_high_on_exposure.get(): # define parameters self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) exp_time = ( self.delta_width.get() - + self.scaninfo.exp_time * self.scaninfo.num_frames - + self.scaninfo.readout_time * (self.scaninfo.num_frames - 1) + + self.scaninfo.exp_time * self.scaninfo.num_points + + self.scaninfo.readout_time * (self.scaninfo.num_points - 1) ) total_exposure = exp_time delay_burst = self.delay_burst.get() @@ -513,7 +526,7 @@ class DelayGeneratorDG645(Device): exp_time = self.delta_width.get() + self.scaninfo.exp_time total_exposure = exp_time + self.scaninfo.readout_time delay_burst = self.delay_burst.get() - num_burst_cycle = self.scaninfo.num_frames + self.additional_triggers.get() + num_burst_cycle = self.scaninfo.num_points + self.additional_triggers.get() # set parameters in DDG self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) @@ -531,18 +544,20 @@ class DelayGeneratorDG645(Device): def unstage(self): """Stop the trigger generator from accepting triggers""" # self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) - self._check_burst_cycle() # Check status self._ddg_is_okay() self._stopped = False self._acquisition_done = False super().unstage() - def trigger(self) -> None: + def trigger(self) -> DeviceStatus: # if self.scaninfo.scan_type == "step": if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() - super().trigger() + status = super().trigger() + burst_state = threading.Thread(target=self._check_burst_cycle, args=(status,), daemon=True) + burst_state.start() + return status def burst_enable(self, count, delay, period, config="all"): """Enable the burst mode""" diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 5c79e48..09e1a0c 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -14,7 +14,7 @@ class BecScaninfoMixin: "queueID": "mockqueuid", "scan_number": 1, "exp_time": 26e-3, - "num_points": 10000, + "num_points": 9999, "readout_time": 2e-3, "scan_type": "fly", "num_lines": 10, @@ -54,9 +54,8 @@ class BecScaninfoMixin: self.scanID = scan_msg.content["scanID"] self.scan_number = scan_msg.content["info"]["scan_number"] self.exp_time = scan_msg.content["info"]["exp_time"] - self.num_frames = ( - scan_msg.content["info"]["num_points"] * scan_msg.content["info"]["frames_per_trigger"] - ) + self.frames_per_trigger = scan_msg.content["info"]["frames_per_trigger"] + self.num_points = scan_msg.content["info"]["num_points"] self.scan_type = scan_msg.content["info"].get("scan_type", "step") self.readout_time = scan_msg.content["info"]["readout_time"] self.username = self._get_username() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 261d398..2cd6d80 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -131,16 +131,20 @@ class Eiger9mCsaxs(DetectorBase): self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs if not sim_mode: + from bec_lib.core.bec_service import SERVICE_CONFIG + self.device_manager = device_manager self._producer = self.device_manager.producer + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) # TODO self.filepath = "" self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered @@ -176,9 +180,11 @@ class Eiger9mCsaxs(DetectorBase): self.std_client.stop_writer() timeout = 0 self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) + time.sleep(1) while not self.std_client.get_status()["state"] == "READY": time.sleep(0.1) timeout = timeout + 0.1 + logger.info("Waiting for std_daq init.") if timeout > 2: if not self.std_client.get_status()["state"]: raise EigerError( @@ -188,8 +194,11 @@ class Eiger9mCsaxs(DetectorBase): return def _prep_det(self) -> None: + logger.info("prepping thresholds") self._set_det_threshold() + logger.info("prepping detector parameter") self._set_acquisition_params() + logger.info("setting trigger") self._set_trigger(TriggerSource.GATING) def _set_det_threshold(self) -> None: diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index f00095d..49a46ad 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -1,4 +1,5 @@ import enum +import threading import time from typing import Any, List import numpy as np @@ -15,7 +16,7 @@ from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin from collections import defaultdict -from bec_lib.core import bec_logger +from bec_lib.core import bec_logger, threadlocked logger = bec_logger.logger @@ -186,6 +187,7 @@ class McsCsaxs(SIS38XX): self.filewriter = FileWriterMixin(self.service_cfg) self._stopped = False self._acquisition_done = False + self._lock = threading.RLock() self._init_mcs() def _init_mcs(self) -> None: @@ -212,6 +214,7 @@ class McsCsaxs(SIS38XX): signal.subscribe(self._on_mca_data, run=False) self._counter = 0 + @threadlocked def _on_mca_data(self, *args, obj=None, **kwargs) -> None: if not isinstance(kwargs["value"], (list, np.ndarray)): return @@ -229,11 +232,12 @@ class McsCsaxs(SIS38XX): self._counter += 1 if self._counter == self.num_lines.get(): self._acquisition_done = True - self._send_data_to_bec() self.stop_all.put(1, use_complete=False) self._send_data_to_bec() self.erase_all.set(1) - # TODO how to make card wait for sure! + # Require wait for + # time.sleep(0.01) + self.mca_data = defaultdict(lambda: []) self._counter = 0 return self.erase_start.set(1) @@ -252,7 +256,8 @@ class McsCsaxs(SIS38XX): ) logger.info(f"{self.mca_data}") msg = BECMessage.DeviceMessage( - signals=dict(self.mca_data), metadata=self.scaninfo.scan_msg.metadata + signals=dict(self.mca_data), + metadata=self.scaninfo.scan_msg.metadata, ).dumps() self._producer.xadd( topic=MessageEndpoints.device_async_readback( @@ -294,6 +299,7 @@ class McsCsaxs(SIS38XX): def stage(self) -> List[object]: """stage the detector and file writer""" + logger.info("Stage Eiger") self.scaninfo.load_scan_metadata() self._prep_det() self._prep_readout() @@ -311,7 +317,7 @@ class McsCsaxs(SIS38XX): break time.sleep(0.005) logger.info("mcs is ready and running") - time.sleep(5) + # time.sleep(5) return super().stage() def unstage(self) -> List[object]: From 58caf2ddd3416deaace82b6e321fc0753771b282 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 5 Sep 2023 16:16:17 +0200 Subject: [PATCH 48/53] fix: working acquire, line and grid scan using mcs, ddg and eiger9m --- .../epics/devices/DelayGeneratorDG645.py | 39 ++++++++++++++----- ophyd_devices/epics/devices/eiger9m_csaxs.py | 12 +++++- ophyd_devices/epics/devices/mcs_csaxs.py | 18 ++++++--- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 4a897c2..2b04981 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -301,6 +301,13 @@ class DelayGeneratorDG645(Device): config_storage_name="ddg_config", ) + trigger_width = Component( + bec_utils.ConfigSignal, + name="trigger_width", + kind="config", + config_storage_name="ddg_config", + ) + def __init__( self, prefix="", @@ -348,6 +355,7 @@ class DelayGeneratorDG645(Device): f"{name}_set_high_on_exposure": False, f"{name}_set_high_on_stage": False, f"{name}_set_trigger_source": "SINGLE_SHOT", + f"{name}_trigger_width": None, } if ddg_config is not None: [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] @@ -479,18 +487,19 @@ class DelayGeneratorDG645(Device): if self.set_high_on_exposure.get(): self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) num_burst_cycle = 1 + self.additional_triggers.get() - exp_time = ( - self.delta_width.get() - + self.scaninfo.num_points - * self.scaninfo.frames_per_trigger - * (self.scaninfo.exp_time + self.scaninfo.readout_time) + + exp_time = self.delta_width.get() + self.scaninfo.frames_per_trigger * ( + self.scaninfo.exp_time + self.scaninfo.readout_time ) total_exposure = exp_time delay_burst = self.delay_burst.get() self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! - self.set_channels("width", exp_time) + if not self.trigger_width.get(): + self.set_channels("width", exp_time) + else: + self.set_channels("width", self.trigger_width.get()) else: self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) exp_time = self.delta_width.get() + self.scaninfo.exp_time @@ -501,7 +510,10 @@ class DelayGeneratorDG645(Device): self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0) # Set burst length to half of the experimental time! - self.set_channels("width", exp_time) + if not self.trigger_width.get(): + self.set_channels("width", exp_time) + else: + self.set_channels("width", self.trigger_width.get()) elif self.scaninfo.scan_type == "fly": if self.set_high_on_exposure.get(): # define parameters @@ -519,7 +531,10 @@ class DelayGeneratorDG645(Device): self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! - self.set_channels("width", exp_time) + if not self.trigger_width.get(): + self.set_channels("width", exp_time) + else: + self.set_channels("width", self.trigger_width.get()) else: # define parameters self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) @@ -531,7 +546,10 @@ class DelayGeneratorDG645(Device): self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") self.set_channels("delay", 0.0) # Set burst length to half of the experimental time! - self.set_channels("width", exp_time) + if not self.trigger_width.get(): + self.set_channels("width", exp_time) + else: + self.set_channels("width", self.trigger_width.get()) else: raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}") @@ -554,7 +572,8 @@ class DelayGeneratorDG645(Device): # if self.scaninfo.scan_type == "step": if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): self.trigger_shot.set(1).wait() - status = super().trigger() + # status = super().trigger(status=) + status = DeviceStatus(self) burst_state = threading.Thread(target=self._check_burst_cycle, args=(status,), daemon=True) burst_state.start() return status diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 2cd6d80..45dded0 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -220,7 +220,7 @@ class Eiger9mCsaxs(DetectorBase): # self.cam.acquire_period.set( # self.scaninfo.exp_time + (self.scaninfo.readout_time - self.reduce_readout) # ) - self.cam.num_cycles.set(self.scaninfo.num_frames) + self.cam.num_cycles.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_frames.set(1) def _set_trigger(self, trigger_source: TriggerSource) -> None: @@ -240,7 +240,10 @@ class Eiger9mCsaxs(DetectorBase): # self._close_file_writer() logger.info(f" std_daq output filepath {self.filepath}") self.std_client.start_writer_async( - {"output_file": self.filepath, "n_images": self.scaninfo.num_frames} + { + "output_file": self.filepath, + "n_images": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger), + } ) while True: det_ctrl = self.std_client.get_status()["acquisition"]["state"] @@ -264,6 +267,11 @@ class Eiger9mCsaxs(DetectorBase): self._prep_file_writer() logger.info("std_daq is ready") + msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), + msg.dumps(), + ) msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 49a46ad..c2806cb 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -230,7 +230,10 @@ class McsCsaxs(SIS38XX): self._updated = True self._counter += 1 - if self._counter == self.num_lines.get(): + logger.info(f"counter {self._counter}") + if (self.scaninfo.scan_type == "fly" and self._counter == self.num_lines.get()) or ( + self.scaninfo.scan_type == "step" and self._counter == self.scaninfo.num_points + ): self._acquisition_done = True self.stop_all.put(1, use_complete=False) self._send_data_to_bec() @@ -272,12 +275,17 @@ class McsCsaxs(SIS38XX): self._set_trigger(TriggerSource.MODE3) def _set_acquisition_params(self) -> None: - n_points = self.scaninfo.num_frames / int(self.num_lines.get()) - if n_points > 9999: + if self.scaninfo.scan_type == "step": + n_points = self.scaninfo.frames_per_trigger + 1 + elif self.scaninfo.scan_type == "fly": + n_points = self.scaninfo.num_points / int(self.num_lines.get()) + 1 + else: + raise MCSError(f"Scantype {self.scaninfo} not implemented for MCS card") + if n_points > 1000: raise MCSError( - f"Requested number of points {n_points+1} exceeds hardware limit of 10000 (n+1)" + f"Requested number of points N={n_points} exceeds hardware limit of mcs card 10000 (N-1)" ) - self.num_use_all.set(n_points + 1) + self.num_use_all.set(n_points) self.preset_real.set(0) def _set_trigger(self, trigger_source: TriggerSource) -> None: From 3e594b5e0461d43431e0103cb713bcd9fd22ca1c Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 6 Sep 2023 07:59:12 +0200 Subject: [PATCH 49/53] fix: changes for sgalil grid scan from BEC --- ophyd_devices/galil/sgalil_ophyd.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index 7bf1b44..ae71a3f 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -249,7 +249,7 @@ class GalilController(Controller): end_x: float, interval_x: int, exp_time: float, - readtime: float, + readout_time: float, **kwargs, ) -> tuple: """_summary_ @@ -262,7 +262,7 @@ class GalilController(Controller): end_x (float): end position of x axis (slow axis) interval_x (int): number of points in x axis exp_time (float): exposure time in seconds - readtime (float): readout time in seconds, minimum of .5e-3s (0.5ms) + readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms) Raises: @@ -285,7 +285,9 @@ class GalilController(Controller): start_y *= sign_y end_y *= sign_y - speed = np.abs(end_y - start_y) / ((interval_y) * exp_time + (interval_y - 1) * readtime) + speed = np.abs(end_y - start_y) / ( + (interval_y) * exp_time + (interval_y - 1) * readout_time + ) if speed > 2.00 or speed < 0.02: raise LimitError( f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s" @@ -677,6 +679,22 @@ class SGalilMotor(Device, PositionerBase): self.controller.stop_all_axes() return super().stop(success=success) + def kickoff( + self, + metadata: dict, + **kwargs, + ) -> None: + self.controller.fly_grid_scan( + kwargs.get("start_y"), + kwargs.get("end_y"), + kwargs.get("interval_y"), + kwargs.get("start_x"), + kwargs.get("end_x"), + kwargs.get("interval_x"), + kwargs.get("exp_time"), + kwargs.get("readout_time"), + ) + if __name__ == "__main__": mock = False From 057d93ab60d2872b2b029bdc7b6dcab35a6a21a5 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 6 Sep 2023 07:59:51 +0200 Subject: [PATCH 50/53] feat: extension for epics motors from xiaoqiang --- ophyd_devices/epics/devices/epics_motor_ex.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 ophyd_devices/epics/devices/epics_motor_ex.py diff --git a/ophyd_devices/epics/devices/epics_motor_ex.py b/ophyd_devices/epics/devices/epics_motor_ex.py new file mode 100644 index 0000000..a2b79b9 --- /dev/null +++ b/ophyd_devices/epics/devices/epics_motor_ex.py @@ -0,0 +1,44 @@ +from ophyd import Component as Cpt, EpicsSignal, EpicsMotor + + +class EpicsMotorEx(EpicsMotor): + """Extend EpicsMotor with extra configuration fields.""" + + # configuration + motor_resolution = Cpt(EpicsSignal, ".MRES", kind="config", auto_monitor=True) + base_velocity = Cpt(EpicsSignal, ".VBAS", kind="config", auto_monitor=True) + backlash_distance = Cpt(EpicsSignal, ".BDST", kind="config", auto_monitor=True) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs + ): + # get configuration attributes from kwargs and then remove them + attrs = {} + for key, value in kwargs.items(): + if hasattr(EpicsMotorEx, key) and isinstance(getattr(EpicsMotorEx, key), Cpt): + attrs[key] = value + for key in attrs: + kwargs.pop(key) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs + ) + + # set configuration attributes + for key, value in attrs.items(): + print("setting ", key, "=", value) + getattr(self, key).put(value) From ac8b96b9ba76ba52920aeca7486ca9046e07326c Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 6 Sep 2023 08:00:31 +0200 Subject: [PATCH 51/53] fix: adjusted __init__ for epics motor extension --- ophyd_devices/epics/devices/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 2dd859f..285483a 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -23,6 +23,7 @@ from ophyd.sim import SynAxis, SynSignal, SynPeriodicSignal from ophyd.quadem import QuadEM # cSAXS +from .epics_motor_ex import EpicsMotorEx from .mcs_csaxs import McsCsaxs from .eiger9m_csaxs import Eiger9mCsaxs from .pilatus_csaxs import PilatusCsaxs From 3a126976cd7cfb3f294556110d77249da6fbc99d Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 6 Sep 2023 08:00:42 +0200 Subject: [PATCH 52/53] fix: online changes --- .../epics/devices/bec_scaninfo_mixin.py | 8 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 9 +- ophyd_devices/epics/devices/mcs_csaxs.py | 6 +- ophyd_devices/epics/devices/pilatus_csaxs.py | 241 ++++++++++++------ 4 files changed, 168 insertions(+), 96 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 09e1a0c..2a10b3b 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -13,11 +13,11 @@ class BecScaninfoMixin: "RID": "mockrid", "queueID": "mockqueuid", "scan_number": 1, - "exp_time": 26e-3, - "num_points": 9999, - "readout_time": 2e-3, + "exp_time": 12e-3, + "num_points": 500, + "readout_time": 3e-3, "scan_type": "fly", - "num_lines": 10, + "num_lines": 1, "frames_per_trigger": 1, } diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 45dded0..15f208b 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -139,11 +139,13 @@ class Eiger9mCsaxs(DetectorBase): else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.scaninfo.load_scan_metadata() self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.scaninfo.load_scan_metadata() # TODO self.filepath = "" - self.scaninfo.username = "e21206" self.filewriter = FileWriterMixin(self.service_cfg) self.reduce_readout = 1e-3 # 3 ms @@ -194,11 +196,8 @@ class Eiger9mCsaxs(DetectorBase): return def _prep_det(self) -> None: - logger.info("prepping thresholds") self._set_det_threshold() - logger.info("prepping detector parameter") self._set_acquisition_params() - logger.info("setting trigger") self._set_trigger(TriggerSource.GATING) def _set_det_threshold(self) -> None: @@ -274,7 +273,7 @@ class Eiger9mCsaxs(DetectorBase): ) msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), + MessageEndpoints.file_event(self.name), msg.dumps(), ) self.arm_acquisition() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index c2806cb..a041b01 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -276,12 +276,12 @@ class McsCsaxs(SIS38XX): def _set_acquisition_params(self) -> None: if self.scaninfo.scan_type == "step": - n_points = self.scaninfo.frames_per_trigger + 1 + n_points = int(self.scaninfo.frames_per_trigger + 1) elif self.scaninfo.scan_type == "fly": - n_points = self.scaninfo.num_points / int(self.num_lines.get()) + 1 + n_points = int(self.scaninfo.num_points / int(self.num_lines.get()) + 1) else: raise MCSError(f"Scantype {self.scaninfo} not implemented for MCS card") - if n_points > 1000: + if n_points > 10000: raise MCSError( f"Requested number of points N={n_points} exceeds hardware limit of mcs card 10000 (N-1)" ) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 74b5557..db97798 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -1,24 +1,41 @@ +import enum import json import os +import time from typing import List import requests import numpy as np from ophyd.areadetector import ADComponent as ADCpt, PilatusDetectorCam, DetectorBase from ophyd.areadetector.plugins import FileBase +from ophyd_devices.utils import bec_utils as bec_utils from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin + logger = bec_logger.logger +class PilatusError(Exception): + pass + + class PilatusDetectorCamEx(PilatusDetectorCam, FileBase): pass +class TriggerSource(int, enum.Enum): + INTERNAL = 0 + EXT_ENABLE = 1 + EXT_TRIGGER = 2 + MULTI_TRIGGER = 3 + ALGINMENT = 4 + + class PilatusCsaxs(DetectorBase): """Pilatus_2 300k detector for CSAXS @@ -43,6 +60,7 @@ class PilatusCsaxs(DetectorBase): configuration_attrs=None, parent=None, device_manager=None, + sim_mode=False, **kwargs, ): super().__init__( @@ -54,41 +72,55 @@ class PilatusCsaxs(DetectorBase): parent=parent, **kwargs, ) - self.device_manager = device_manager - self.name = name - self.username = "e21206" - # TODO once running from BEC - # self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() + if device_manager is None and not sim_mode: + raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True") + + self.name = name + self.wait_for_connection() # Make sure to be connected before talking to PVs + if not sim_mode: + from bec_lib.core.bec_service import SERVICE_CONFIG + + self.device_manager = device_manager + self._producer = self.device_manager.producer + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] + else: + self._producer = bec_utils.MockProducer() + self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.scaninfo.load_scan_metadata() + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.filepath = "" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/data/"} self.filewriter = FileWriterMixin(self.service_cfg) - self._producer = RedisConnector(["localhost:6379"]).producer() - self.readout = 0.003 # 3 ms - self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + self.readout = 1e-3 # 3 ms def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) return BECMessage.ScanStatusMessage.loads(msg) - def _load_scan_metadata(self) -> None: - scan_msg = self._get_current_scan_msg() - self.metadata = { - "scanID": scan_msg.content["scanID"], - "RID": scan_msg.content["info"]["RID"], - "queueID": scan_msg.content["info"]["queueID"], - } - self.scanID = scan_msg.content["scanID"] - self.scan_number = scan_msg.content["info"]["scan_number"] - self.exp_time = scan_msg.content["info"]["exp_time"] - self.num_frames = scan_msg.content["info"]["num_points"] - self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - self.device_manager.devices.mokev.read()["mokev"]["value"] - # self.triggermode = scan_msg.content["info"]["trigger_mode"] - # self.filename = self.filewriter.compile_full_filename( - # self.scan_number, "pilatus_2", 1000, 5, True - # ) - # TODO fix with BEC running - # self.filename = '/sls/X12SA/Data10/e21206/data/test.h5' + # def _load_scan_metadata(self) -> None: + # scan_msg = self._get_current_scan_msg() + # self.metadata = { + # "scanID": scan_msg.content["scanID"], + # "RID": scan_msg.content["info"]["RID"], + # "queueID": scan_msg.content["info"]["queueID"], + # } + # self.scanID = scan_msg.content["scanID"] + # self.scan_number = scan_msg.content["info"]["scan_number"] + # self.exp_time = scan_msg.content["info"]["exp_time"] + # self.num_frames = scan_msg.content["info"]["num_points"] + # self.username = self.device_manager.producer.get( + # MessageEndpoints.account() + # ).decode() + # self.device_manager.devices.mokev.read()["mokev"]["value"] + # # self.triggermode = scan_msg.content["info"]["trigger_mode"] + # # self.filename = self.filewriter.compile_full_filename( + # # self.scan_number, "pilatus_2", 1000, 5, True + # # ) + # # TODO fix with BEC running + # # self.filename = '/sls/X12SA/Data10/e21206/data/test.h5' def _prep_det(self) -> None: # TODO slow reaction, seemed to have timeout. @@ -96,37 +128,57 @@ class PilatusCsaxs(DetectorBase): self._set_acquisition_params() def _set_det_threshold(self) -> None: + # threshold_energy PV exists on Eiger 9M? + factor = 1 + if self.cam.threshold_energy._metadata["units"] == "eV": + factor = 1000 + setp_energy = int(self.mokev * factor) threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] - # threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]['value'] - if not np.isclose(self.mokev / 2, threshold, rtol=0.05): - self.cam.threshold_energy.set(self.mokev / 2) + if not np.isclose(setp_energy / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(setp_energy / 2) def _set_acquisition_params(self) -> None: """set acquisition parameters on the detector""" - self.cam.acquire_time.set(self.exp_time) - self.cam.acquire_period.set(self.exp_time + self.readout) - self.cam.num_images.set(self.num_frames) + # self.cam.acquire_time.set(self.exp_time) + # self.cam.acquire_period.set(self.exp_time + self.readout) + self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_exposures.set(1) - self.cam.trigger_mode.set(self.triggermode) + self._set_trigger(TriggerSource.INTERNAL) # EXT_TRIGGER) + + def _set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector, either directly to value or TriggerSource.* with + INTERNAL = 0 + EXT_ENABLE = 1 + EXT_TRIGGER = 2 + MULTI_TRIGGER = 3 + ALGINMENT = 4 + """ + value = int(trigger_source) + self.cam.trigger_mode.set(value) def _prep_file_writer(self) -> None: """Prepare the file writer for pilatus_2 a zmq service is running on xbl-daq-34 that is waiting for a zmq message to start the writer for the pilatus_2 x12sa-pd-2 """ + self.filepath_h5 = self.filewriter.compile_full_filename( + self.scaninfo.scan_number, "pilatus_2.h5", 1000, 5, True + ) self.cam.file_path.set(f"/dev/shm/zmq/") - self.cam.file_name.set(f"{self.username}_2_{self.scan_number:05d}") + self.cam.file_name.set(f"{self.scaninfo.username}_2_{self.scaninfo.scan_number:05d}") self.cam.auto_increment.set(1) # auto increment self.cam.file_number.set(0) # first iter self.cam.file_format.set(0) # 0: TIFF self.cam.file_template.set("%s%s_%5.5d.cbf") - # TODO Filewriter Plugin to write cbfs to h5! - # Pilatus_2 writes cbf files -> where do we like to write those! - # scan_dir = self.filewriter._get_scan_directory( - # scan_bundle=1000, scan_number=self.scan_number, leading_zeros=5 - # ) # os.path.join(self.service_cfg["base_path"], scan_dir) - self.destination_path = "/sls/X12SA/data/{self.username}/Data10/pilatus_2/" + # compile filename + basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/" + self.destination_path = os.path.join( + basepath, + self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5), + ) + # Make directory if needed + os.makedirs(os.path.dirname(self.destination_path), exist_ok=True) data_msg = { "source": [ @@ -146,6 +198,7 @@ class PilatusCsaxs(DetectorBase): data=json.dumps(data_msg), headers=headers, ) + logger.info(f"{res.status_code} - {res.text} - {res.content}") if not res.ok: res.raise_for_status() @@ -153,14 +206,14 @@ class PilatusCsaxs(DetectorBase): # prepare writer data_msg = [ "zmqWriter", - self.username, + self.scaninfo.username, { "addr": "tcp://x12sa-pd-2:8888", "dst": ["file"], - "numFrm": self.num_frames, + "numFrm": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger), "timeout": 2000, "ifType": "PULL", - "user": self.username, + "user": self.scaninfo.username, }, ] @@ -170,55 +223,69 @@ class PilatusCsaxs(DetectorBase): headers=headers, ) + logger.info(f"{res.status_code} - {res.text} - {res.content}") + if not res.ok: res.raise_for_status() + # Wait for server to become available again + time.sleep(0.1) + + headers = {"Content-Type": "application/json", "Accept": "application/json"} + data_msg = [ + "zmqWriter", + self.scaninfo.username, + { + "frmCnt": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger), + "timeout": 2000, + }, + ] + logger.info(f"{res.status_code} -{res.text} - {res.content}") + + try: + res = requests.put( + url="http://xbl-daq-34:8091/pilatus_2/wait", + data=json.dumps(data_msg), + # headers=headers, + ) + + logger.info(f"{res}") + + if not res.ok: + res.raise_for_status() + except Exception as exc: + logger.info("exc") + def _close_file_writer(self) -> None: """Close the file writer for pilatus_2 a zmq service is running on xbl-daq-34 that is waiting for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2 """ - headers = {"Content-Type": "application/json", "Accept": "application/json"} - data_msg = [ - "zmqWriter", - self.username, - { - "frmCnt": self.num_frames, - "timeout": 2000, - }, - ] - logger.info(data_msg) - - res = requests.put( - url="http://xbl-daq-34:8091/pilatus_2/wait", - data=json.dumps(data_msg), - headers=headers, - ) - - if not res.ok: - res.raise_for_status() res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") if not res.ok: res.raise_for_status() + def _stop_file_writer(self) -> None: + res = requests.put( + url="http://xbl-daq-34:8091/pilatus_2/stop", + # data=json.dumps(data_msg), + # headers=headers, + ) + + if not res.ok: + res.raise_for_status() + def stage(self) -> List[object]: """stage the detector and file writer""" - # TODO remove once running from BEC - # self._load_scan_metadata() - self.scan_number = 10 - self.exp_time = 0.5 - self.num_frames = 3 - self.mokev = 12 + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.obj.read()[ + self.device_manager.devices.mokev.name + ]["value"] self._prep_det() self._prep_file_writer() - - # msg = BECMessage.FileMessage(file_path=self.filename, done=False) - # self._producer.set_and_publish( - # MessageEndpoints.public_file(self.scanID, "pilatus_2"), - # msg.dumps(), - # ) + self.acquire() return super().stage() @@ -227,13 +294,18 @@ class PilatusCsaxs(DetectorBase): # Reset to software trigger self.triggermode = 0 self._close_file_writer() - # TODO check if acquisition is done and successful! - state = True - # msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - # self.producer.set_and_publish( - # MessageEndpoints.public_file(self.metadata["scanID"], self.name), - # msg.dumps(), - # ) + self._stop_file_writer() + # Only sent this out once data is written to disk since cbf to hdf5 converter will be triggered + msg = BECMessage.FileMessage(file_path=self.filepath, done=True) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), + msg.dumps(), + ) + msg = BECMessage.FileMessage(file_path=self.filepath, done=True) + self._producer.set_and_publish( + MessageEndpoints.file_event(self.name), + msg.dumps(), + ) return super().unstage() def acquire(self) -> None: @@ -245,7 +317,8 @@ class PilatusCsaxs(DetectorBase): def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" self.cam.acquire.set(0) - self.unstage() + self._stop_file_writer() + # self.unstage() super().stop(success=success) self._stopped = True From 3bab432a2f01e3a62811e13b9143d67da495fbb8 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 7 Sep 2023 11:07:59 +0200 Subject: [PATCH 53/53] feat: add falcon and progress bar option to devices --- .../epics/devices/DelayGeneratorDG645.py | 26 ++- .../epics/devices/bec_scaninfo_mixin.py | 36 +-- ophyd_devices/epics/devices/eiger9m_csaxs.py | 47 ++-- ophyd_devices/epics/devices/epics_motor_ex.py | 1 + ophyd_devices/epics/devices/falcon_csaxs.py | 213 +++++++++--------- ophyd_devices/epics/devices/mcs_csaxs.py | 49 ++-- ophyd_devices/epics/devices/pilatus_csaxs.py | 151 +++++++++---- ophyd_devices/galil/sgalil_ophyd.py | 5 +- 8 files changed, 316 insertions(+), 212 deletions(-) diff --git a/ophyd_devices/epics/devices/DelayGeneratorDG645.py b/ophyd_devices/epics/devices/DelayGeneratorDG645.py index 2b04981..d0aa10d 100644 --- a/ophyd_devices/epics/devices/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/devices/DelayGeneratorDG645.py @@ -134,6 +134,10 @@ class DelayGeneratorDG645(Device): current device """ + SUB_PROGRESS = "progress" + SUB_VALUE = "value" + _default_sub = SUB_VALUE + USER_ACCESS = [ "set_channels", "_set_trigger", @@ -403,7 +407,7 @@ class DelayGeneratorDG645(Device): LINE = 6 """ value = int(trigger_source) - self.source.set(value) + self.source.put(value) def _ddg_is_okay(self, raise_on_error=False) -> None: status = self.status.read()[self.status.name]["value"] @@ -452,14 +456,14 @@ class DelayGeneratorDG645(Device): ) self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get())) # Set threshold level for ext. pulses - self.level.set(self.thres_trig_level.get()) + self.level.put(self.thres_trig_level.get()) def _check_burst_cycle(self, status) -> None: """Checks burst cycle of delay generator Force readout, return value from end of burst cycle """ while True: - self.trigger_burst_readout.set(1) + self.trigger_burst_readout.put(1) if ( self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1 and self.delay_finished.read()[self.delay_finished.name]["value"] == 1 @@ -571,7 +575,7 @@ class DelayGeneratorDG645(Device): def trigger(self) -> DeviceStatus: # if self.scaninfo.scan_type == "step": if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT): - self.trigger_shot.set(1).wait() + self.trigger_shot.put(1) # status = super().trigger(status=) status = DeviceStatus(self) burst_state = threading.Thread(target=self._check_burst_cycle, args=(status,), daemon=True) @@ -590,19 +594,19 @@ class DelayGeneratorDG645(Device): "first", ], "Supported bust configs are 'all' and 'first'" - self.burstMode.set(1).wait() - self.burstCount.set(count).wait() - self.burstDelay.set(delay).wait() - self.burstPeriod.set(period).wait() + self.burstMode.put(1) + self.burstCount.put(count) + self.burstDelay.put(delay) + self.burstPeriod.put(period) if config == "all": - self.burstConfig.set(0).wait() + self.burstConfig.put(0) elif config == "first": - self.burstConfig.set(1).wait() + self.burstConfig.put(1) def burst_disable(self): """Disable the burst mode""" - self.burstMode.set(0).wait() + self.burstMode.put(0) # Automatically connect to test environmenr if directly invoked diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 2a10b3b..5adf6ef 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -1,6 +1,9 @@ import os from bec_lib.core import DeviceManagerBase, BECMessage, MessageEndpoints +from bec_lib.core import bec_logger + +logger = bec_logger.logger class BecScaninfoMixin: @@ -39,23 +42,28 @@ class BecScaninfoMixin: info=self.bec_info_msg, ) - def _get_username(self) -> str: + def get_username(self) -> str: if not self.sim_mode: return self.device_manager.producer.get(MessageEndpoints.account()).decode() return os.getlogin() def load_scan_metadata(self) -> None: self.scan_msg = scan_msg = self._get_current_scan_msg() - self.metadata = { - "scanID": scan_msg.content["scanID"], - "RID": scan_msg.content["info"]["RID"], - "queueID": scan_msg.content["info"]["queueID"], - } - self.scanID = scan_msg.content["scanID"] - self.scan_number = scan_msg.content["info"]["scan_number"] - self.exp_time = scan_msg.content["info"]["exp_time"] - self.frames_per_trigger = scan_msg.content["info"]["frames_per_trigger"] - self.num_points = scan_msg.content["info"]["num_points"] - self.scan_type = scan_msg.content["info"].get("scan_type", "step") - self.readout_time = scan_msg.content["info"]["readout_time"] - self.username = self._get_username() + logger.info(f"{self.scan_msg}") + try: + self.metadata = { + "scanID": scan_msg.content["scanID"], + "RID": scan_msg.content["info"]["RID"], + "queueID": scan_msg.content["info"]["queueID"], + } + self.scanID = scan_msg.content["scanID"] + self.scan_number = scan_msg.content["info"]["scan_number"] + self.exp_time = scan_msg.content["info"]["exp_time"] + self.frames_per_trigger = scan_msg.content["info"]["frames_per_trigger"] + self.num_points = scan_msg.content["info"]["num_points"] + self.scan_type = scan_msg.content["info"].get("scan_type", "step") + self.readout_time = scan_msg.content["info"]["readout_time"] + except Exception as exc: + logger.error(f"Failed to load scan metadata: {exc}.") + + self.username = self.get_username() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 15f208b..7556a9b 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -4,9 +4,8 @@ from typing import Any, List import numpy as np from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV -from ophyd import CamBase, DetectorBase, Device +from ophyd import DetectorBase, Device from ophyd import ADComponent as ADCpt -from ophyd.areadetector.plugins import FileBase from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin @@ -25,7 +24,7 @@ class EigerError(Exception): pass -class SlsDetectorCam(Device): # CamBase, FileBase): +class SlsDetectorCam(Device): detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") setting = ADCpt(EpicsSignalWithRBV, "Setting") delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") @@ -125,6 +124,7 @@ class Eiger9mCsaxs(DetectorBase): parent=parent, **kwargs, ) + self._stopped = False if device_manager is None and not sim_mode: raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") @@ -234,16 +234,22 @@ class Eiger9mCsaxs(DetectorBase): def _prep_file_writer(self) -> None: self.filepath = self.filewriter.compile_full_filename( - self.scaninfo.scan_number, "eiger.h5", 1000, 5, True + self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True ) # self._close_file_writer() logger.info(f" std_daq output filepath {self.filepath}") - self.std_client.start_writer_async( - { - "output_file": self.filepath, - "n_images": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger), - } - ) + try: + self.std_client.start_writer_async( + { + "output_file": self.filepath, + "n_images": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger), + } + ) + except Exception as exc: + time.sleep(5) + if self.std_client.get_status()["state"] == "READY": + raise EigerError(f"Timeout of start_writer_async with {exc}") + while True: det_ctrl = self.std_client.get_status()["acquisition"]["state"] if det_ctrl == "WAITING_IMAGES": @@ -277,24 +283,29 @@ class Eiger9mCsaxs(DetectorBase): msg.dumps(), ) self.arm_acquisition() - logger.info("Waiting for detector to be armed") + logger.info("Waiting for Eiger9m to be armed") while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): break + if self._stopped == True: + break time.sleep(0.005) - logger.info("Detector is armed") - + logger.info("Eiger9m is armed") + self._stopped = False return super().stage() def unstage(self) -> List[object]: """unstage the detector and file writer""" - logger.info("Waiting for eiger9M to return from acquisition") + logger.info("Waiting for Eiger9M to return from acquisition") while True: det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] if det_ctrl == 0: break + if self._stopped == True: + break time.sleep(0.005) + logger.info("Eiger9M finished") logger.info("Waiting for std daq to receive images") while True: @@ -302,7 +313,10 @@ class Eiger9mCsaxs(DetectorBase): # TODO if no writing was performed before if det_ctrl == "FINISHED": break + if self._stopped == True: + break time.sleep(0.005) + logger.info("Std_daq finished") # Message to BEC state = True @@ -311,7 +325,7 @@ class Eiger9mCsaxs(DetectorBase): MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) - logger.info("Eiger done") + self._stopped = False return super().unstage() def arm_acquisition(self) -> None: @@ -324,12 +338,9 @@ class Eiger9mCsaxs(DetectorBase): """Stop the scan, with camera and file writer""" self.cam.acquire.set(0) self._close_file_writer() - self.unstage() super().stop(success=success) self._stopped = True -# Automatically connect to test environmenr if directly invoked if __name__ == "__main__": eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) - eiger.stage() diff --git a/ophyd_devices/epics/devices/epics_motor_ex.py b/ophyd_devices/epics/devices/epics_motor_ex.py index a2b79b9..f4a38c0 100644 --- a/ophyd_devices/epics/devices/epics_motor_ex.py +++ b/ophyd_devices/epics/devices/epics_motor_ex.py @@ -40,5 +40,6 @@ class EpicsMotorEx(EpicsMotor): # set configuration attributes for key, value in attrs.items(): + # print out attributes that are being configured print("setting ", key, "=", value) getattr(self, key).put(value) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index a172572..319ce13 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -1,13 +1,14 @@ +import enum import os import time from typing import List from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device -from ophyd.mca import EpicsMCARecord, EpicsDXPMapping, EpicsDXPLowLevel, EpicsDXPMultiElementSystem -from ophyd.areadetector.plugins import HDF5Plugin, HDF5Plugin_V21, FilePlugin_V22 +from ophyd.mca import EpicsMCARecord +from ophyd.areadetector.plugins import HDF5Plugin_V21, FilePlugin_V22 from bec_lib.core.file_utils import FileWriterMixin -from bec_lib.core import MessageEndpoints, BECMessage, RedisConnector +from bec_lib.core import MessageEndpoints, BECMessage from bec_lib.core import bec_logger from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin @@ -16,6 +17,15 @@ from ophyd_devices.utils import bec_utils logger = bec_logger.logger +class FalconError(Exception): + pass + + +class DetectorState(int, enum.Enum): + DONE = 0 + ACQUIRING = 1 + + class EpicsDXPFalcon(Device): """All high-level DXP parameters for each channel""" @@ -37,12 +47,21 @@ class EpicsDXPFalcon(Device): current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") -class FalconError(Exception): - pass - - -class FalconHDF5Plugins(HDF5Plugin_V21, FilePlugin_V22): - pass +class FalconHDF5Plugins(Device): # HDF5Plugin_V21, FilePlugin_V22): + capture = Cpt(EpicsSignalWithRBV, "Capture") + enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config") + xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config") + lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'") + temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True) + # file_path = Cpt( + # EpicsSignalWithRBV, "FilePath", string=True, kind="config", path_semantics="posix" + # ) + file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config") + file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config") + file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config") + num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config") + file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config") + capture = Cpt(EpicsSignalWithRBV, "Capture") class FalconCsaxs(Device): @@ -77,6 +96,8 @@ class FalconCsaxs(Device): pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer") pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") + # HDF5 + def __init__( self, prefix="", @@ -101,153 +122,141 @@ class FalconCsaxs(Device): ) if device_manager is None and not sim_mode: raise FalconError("Add DeviceManager to initialization or init with sim_mode=True") - + self._stopped = False self.name = name self.wait_for_connection() # Make sure to be connected before talking to PVs if not sim_mode: + from bec_lib.core.bec_service import SERVICE_CONFIG + self.device_manager = device_manager self._producer = self.device_manager.producer + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) + self.scaninfo.load_scan_metadata() + self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - # TODO - self.scaninfo.username = "e21206" - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) self.readout = 0.003 # 3 ms - self._value_pixel_per_buffer = 16 - # TODO create file template from filewriter compile filename - self._file_template = f"%s%s_{self.name}.h5" - - self.num_frames = 0 - + self._value_pixel_per_buffer = 1 # 16 self._clean_up() self._init_hdf5_saving() self._init_mapping_mode() def _clean_up(self) -> None: """Clean up""" - self.hdf5.capture.set(0) - self.stop_all.set(1) - self.erase_all.set(1) + self.hdf5.capture.put(0) + self.stop_all.put(1) + self.erase_all.put(1) def _init_hdf5_saving(self) -> None: """Set up hdf5 save parameters""" - self.hdf5.enable.set(1) # EnableCallbacks - self.hdf5.xml_file_name.set("layout.xml") # Points to hardcopy of HDF5 Layout xml file - self.hdf5.lazy_open.set(1) # Yes -> To be checked how to add FilePlugin_V21+ - self.hdf5.temp_suffix.set("temps") # -> To be checked how to add FilePlugin_V22+ + self.hdf5.enable.put(1) # EnableCallbacks + self.hdf5.xml_file_name.put("layout.xml") # Points to hardcopy of HDF5 Layout xml file + self.hdf5.lazy_open.put(1) # Yes -> To be checked how to add FilePlugin_V21+ + self.hdf5.temp_suffix.put("temps") # -> To be checked how to add FilePlugin_V22+ def _init_mapping_mode(self) -> None: """Set up mapping mode params""" - self.collect_mode.set(1) # 1 MCA Mapping, 0 MCA Spectrum - self.preset_mode.set(1) # 1 Realtime - self.input_logic_polarity.set(0) # 0 Normal, 1 Inverted - self.pixel_advance_mode.set(1) # 0 User, 1 Gate, 2 Sync - self.ignore_gate.set(1) # 1 Yes - self.auto_pixels_per_buffer.set(0) # 0 Manual 1 Auto - self.pixels_per_buffer.set(16) # - - def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return BECMessage.ScanStatusMessage.loads(msg) - - def _load_scan_metadata(self) -> None: - scan_msg = self._get_current_scan_msg() - self.metadata = { - "scanID": scan_msg.content["scanID"], - "RID": scan_msg.content["info"]["RID"], - "queueID": scan_msg.content["info"]["queueID"], - } - self.scanID = scan_msg.content["scanID"] - self.scan_number = scan_msg.content["info"]["scan_number"] - self.exp_time = scan_msg.content["info"]["exp_time"] - self.num_frames = scan_msg.content["info"]["num_points"] - self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode() - self.device_manager.devices.mokev.read()["mokev"]["value"] - # self.triggermode = scan_msg.content["info"]["trigger_mode"] - self.filepath = self.filewriter.compile_full_filename( - self.scan_number, "falcon", 1000, 5, True - ) + self.collect_mode.put(1) # 1 MCA Mapping, 0 MCA Spectrum + self.preset_mode.put(1) # 1 Realtime + self.input_logic_polarity.put(0) # 0 Normal, 1 Inverted + self.pixel_advance_mode.put(1) # 0 User, 1 Gate, 2 Sync + self.ignore_gate.put(1) # 1 Yes + self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto + self.pixels_per_buffer.put(16) # def _prep_det(self) -> None: """Prepare detector for acquisition""" - self.collect_mode.set(1) - self.preset_real.set(self.exposure_time) - self.pixels_per_run.set(self.num_frames) - self.auto_pixels_per_buffer.set(0) - self.pixels_per_buffer.set(self._value_pixel_per_buffer) + self.collect_mode.put(1) + self.preset_real.put(self.scaninfo.exp_time) + self.pixels_per_run.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) + self.auto_pixels_per_buffer.put(0) + self.pixels_per_buffer.put(self._value_pixel_per_buffer) def _prep_file_writer(self) -> None: """Prep HDF5 weriting""" # TODO creta filename and destination path from filepath - self.destination_path = os.path.join(self.service_cfg["base_path"]) - self.filename = f"test_{self.scan_number}" - self.hdf5.file_path.set(self.destination_path) - self.hdf5.file_name.set(self.filename) - self.hdf5.file_template.set(self._file_template) - self.hdf5.num_capture.set(self.num_frames // self._value_pixel_per_buffer + 1) - self.hdf5.file_write_mode.set(2) - self.hdf5.capture.set(1) + self.destination_path = self.filewriter.compile_full_filename( + self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True + ) + # self.hdf5.file_path.set(self.destination_path) + file_path, file_name = os.path.split(self.destination_path) + self.hdf5.file_path.put(file_path) + self.hdf5.file_name.put(file_name) + self.hdf5.file_template.put(f"%s%s") + self.hdf5.num_capture.put(self.scaninfo.num_points // self._value_pixel_per_buffer + 1) + self.hdf5.file_write_mode.put(2) + self.hdf5.capture.put(1) def stage(self) -> List[object]: """stage the detector and file writer""" - # TODO remove once running from BEC - # self._load_scan_metadata() - self.scan_number = 10 - self.exp_time = 0.5 - self.num_frames = 3 - self.mokev = 12 + # TODO clean up needed? + # self._clean_up() + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.obj.read()[ + self.device_manager.devices.mokev.name + ]["value"] + logger.info("Waiting for pilatus2 to be armed") self._prep_det() + logger.info("Pilatus2 armed") + logger.info("Waiting for pilatus2 zmq stream to be ready") self._prep_file_writer() + logger.info("Pilatus2 zmq ready") - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg = BECMessage.FileMessage(file_path=self.destination_path, done=False) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) - + self.arm_acquisition() + logger.info("Waiting for Falcon to be armed") + while True: + det_ctrl = self.state.read()[self.state.name]["value"] + if det_ctrl == int(DetectorState.ACQUIRING): + break + if self._stopped == True: + break + time.sleep(0.005) + logger.info("Falcon is armed") + self._stopped = False return super().stage() - def acquire(self) -> None: - self.start_all.set(1) + def arm_acquisition(self) -> None: + self.start_all.put(1) def unstage(self) -> List[object]: + logger.info("Waiting for Falcon to return from acquisition") + while True: + det_ctrl = self.state.read()[self.state.name]["value"] + if det_ctrl == int(DetectorState.DONE): + break + if self._stopped == True: + break + time.sleep(0.005) + logger.info("Falcon done") + # TODO needed? self._clean_up() - # TODO check if acquisition is done and successful! state = True - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - self.producer.set_and_publish( - MessageEndpoints.public_file(self.metadata["scanID"], self.name), + msg = BECMessage.FileMessage(file_path=self.destination_path, done=True, successful=state) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.metadata["scanID"], self.name), msg.dumps(), ) + self._stopped = False return super().unstage() - def _check_falcon_done(self) -> bool: - state = self.state.read()[f"{self.name }_state"]["value"] - if state is [0, 1]: - return not bool(state) - else: - # TODO raise error - logger.warning("Returned in unknown state") - return state - def stop(self, *, success=False) -> None: - """Stop acquisition - Stop or Stop and Erase - """ - self._clean_up.set(1) - # self.erase_all.set(1) - self.unstage() + """Stop the scan, with camera and file writer""" + self._clean_up() super().stop(success=success) self._stopped = True - # Automatically connect to test environmenr if directly invoked - if __name__ == "__main__": falcon = FalconCsaxs(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) - falcon.stage() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index a041b01..5d61f62 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -93,6 +93,9 @@ class SIS38XX(Device): class McsCsaxs(SIS38XX): + SUB_PROGRESS = "progress" + SUB_VALUE = "value" + _default_sub = SUB_VALUE # scaler = Cpt(ScalerCH, "scaler1") # mca2 = Cpt(EpicsMCARecord, "mca2") @@ -127,6 +130,7 @@ class McsCsaxs(SIS38XX): # mca30 = Cpt(EpicsMCARecord, "mca30") # mca31 = Cpt(EpicsMCARecord, "mca31") # mca32 = Cpt(EpicsMCARecord, "mca32") + current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True) num_lines = Cpt( bec_utils.ConfigSignal, @@ -164,7 +168,6 @@ class McsCsaxs(SIS38XX): parent=parent, **kwargs, ) - if device_manager is None and not sim_mode: raise MCSError("Add DeviceManager to initialization or init with sim_mode=True") @@ -188,6 +191,8 @@ class McsCsaxs(SIS38XX): self._stopped = False self._acquisition_done = False self._lock = threading.RLock() + self.counter = 0 + self.n_points = 0 self._init_mcs() def _init_mcs(self) -> None: @@ -212,7 +217,17 @@ class McsCsaxs(SIS38XX): for mca in self.mca_names: signal = getattr(self, mca) signal.subscribe(self._on_mca_data, run=False) - self._counter = 0 + self.current_channel.subscribe(self._progress_update, run=False) + + def _progress_update(self, value, **kwargs) -> None: + num_lines = self.num_lines.get() + max_value = self.scaninfo.num_points + self._run_subs( + sub_type=self.SUB_PROGRESS, + value=self.counter * int(self.scaninfo.num_points / num_lines) + max(value - 1, 0), + max_value=max_value, + done=bool(max_value == self.counter), + ) @threadlocked def _on_mca_data(self, *args, obj=None, **kwargs) -> None: @@ -229,19 +244,18 @@ class McsCsaxs(SIS38XX): # return self._updated = True - self._counter += 1 - logger.info(f"counter {self._counter}") - if (self.scaninfo.scan_type == "fly" and self._counter == self.num_lines.get()) or ( - self.scaninfo.scan_type == "step" and self._counter == self.scaninfo.num_points + self.counter += 1 + if (self.scaninfo.scan_type == "fly" and self.counter == self.num_lines.get()) or ( + self.scaninfo.scan_type == "step" and self.counter == self.scaninfo.num_points ): self._acquisition_done = True self.stop_all.put(1, use_complete=False) self._send_data_to_bec() - self.erase_all.set(1) + self.erase_all.put(1) # Require wait for # time.sleep(0.01) self.mca_data = defaultdict(lambda: []) - self._counter = 0 + self.counter = 0 return self.erase_start.set(1) self._send_data_to_bec() @@ -257,7 +271,6 @@ class McsCsaxs(SIS38XX): "num_lines": self.num_lines.get(), } ) - logger.info(f"{self.mca_data}") msg = BECMessage.DeviceMessage( signals=dict(self.mca_data), metadata=self.scaninfo.scan_msg.metadata, @@ -276,16 +289,16 @@ class McsCsaxs(SIS38XX): def _set_acquisition_params(self) -> None: if self.scaninfo.scan_type == "step": - n_points = int(self.scaninfo.frames_per_trigger + 1) + self.n_points = int(self.scaninfo.frames_per_trigger + 1) elif self.scaninfo.scan_type == "fly": - n_points = int(self.scaninfo.num_points / int(self.num_lines.get()) + 1) + self.n_points = int(self.scaninfo.num_points / int(self.num_lines.get()) + 1) else: raise MCSError(f"Scantype {self.scaninfo} not implemented for MCS card") - if n_points > 10000: + if self.n_points > 10000: raise MCSError( - f"Requested number of points N={n_points} exceeds hardware limit of mcs card 10000 (N-1)" + f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card 10000 (N-1)" ) - self.num_use_all.set(n_points) + self.num_use_all.set(self.n_points) self.preset_real.set(0) def _set_trigger(self, trigger_source: TriggerSource) -> None: @@ -299,7 +312,7 @@ class McsCsaxs(SIS38XX): Check ReadoutMode class for more information about options """ # self.read_mode.set(ReadoutMode.EVENT) - self.erase_all.set(1) + self.erase_all.put(1) self.read_mode.set(ReadoutMode.EVENT) def _force_readout_mcs_card(self) -> None: @@ -307,7 +320,7 @@ class McsCsaxs(SIS38XX): def stage(self) -> List[object]: """stage the detector and file writer""" - logger.info("Stage Eiger") + logger.info("Stage mcs") self.scaninfo.load_scan_metadata() self._prep_det() self._prep_readout() @@ -347,7 +360,7 @@ class McsCsaxs(SIS38XX): Start: start_all Erase/Start: erase_start """ - self._counter = 0 + self.counter = 0 self.erase_start.set(1) # self.start_all.set(1) @@ -359,7 +372,7 @@ class McsCsaxs(SIS38XX): # self.erase_all.set(1) self._stopped = True self._acquisition_done = True - self._counter = 0 + self.counter = 0 super().stop(success=success) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index db97798..2efee34 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -6,11 +6,12 @@ from typing import List import requests import numpy as np -from ophyd.areadetector import ADComponent as ADCpt, PilatusDetectorCam, DetectorBase -from ophyd.areadetector.plugins import FileBase +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd import DetectorBase, Device +from ophyd import ADComponent as ADCpt from ophyd_devices.utils import bec_utils as bec_utils -from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector +from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger @@ -24,10 +25,6 @@ class PilatusError(Exception): pass -class PilatusDetectorCamEx(PilatusDetectorCam, FileBase): - pass - - class TriggerSource(int, enum.Enum): INTERNAL = 0 EXT_ENABLE = 1 @@ -36,6 +33,72 @@ class TriggerSource(int, enum.Enum): ALGINMENT = 4 +class SlsDetectorCam(Device): # CamBase, FileBase): + # detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") + # setting = ADCpt(EpicsSignalWithRBV, "Setting") + # beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") + # enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits") + # bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") + # trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") + # high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage") + # Receiver and data callback + # receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode") + # receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream") + # enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback") + # missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV") + # # Direct settings access + # setup_file = ADCpt(EpicsSignal, "SetupFile") + # load_setup = ADCpt(EpicsSignal, "LoadSetup") + # command = ADCpt(EpicsSignal, "Command") + # Mythen 3 + # counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask") + # counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold") + # counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold") + # counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold") + # gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay") + # gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width") + # gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay") + # gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width") + # gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay") + # gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width") + # Moench + # json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode") + # json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") + + # Eiger9M + # delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") + # num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") + # acquire = ADCpt(EpicsSignal, "Acquire") + # acquire_time = ADCpt(EpicsSignal, 'AcquireTime') + # detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") + # threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + # num_gates = ADCpt(EpicsSignalWithRBV, "NumGates") + # num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") + # timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") + + # Pilatus_2 300k + num_images = ADCpt(EpicsSignalWithRBV, "NumImages") + num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") + delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") + trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode") + acquire = ADCpt(EpicsSignal, "Acquire") + armed = ADCpt(EpicsSignalRO, "Armed") + + read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot") + detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV") + status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV") + acquire_time = ADCpt(EpicsSignal, "AcquireTime") + acquire_period = ADCpt(EpicsSignal, "AcquirePeriod") + threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") + file_path = ADCpt(EpicsSignalWithRBV, "FilePath") + file_name = ADCpt(EpicsSignalWithRBV, "FileName") + file_number = ADCpt(EpicsSignalWithRBV, "FileNumber") + auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement") + file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate") + file_format = ADCpt(EpicsSignalWithRBV, "FileNumber") + gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill") + + class PilatusCsaxs(DetectorBase): """Pilatus_2 300k detector for CSAXS @@ -43,12 +106,12 @@ class PilatusCsaxs(DetectorBase): Device class: PilatusDetectorCamEx Attributes: - name str: 'eiger' + name str: 'pilatus_2' prefix (str): PV prefix (X12SA-ES-PILATUS300K:) """ - cam = ADCpt(PilatusDetectorCamEx, "cam1:") + cam = ADCpt(SlsDetectorCam, "cam1:") def __init__( self, @@ -91,7 +154,7 @@ class PilatusCsaxs(DetectorBase): self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.filepath = "" + self.filepath_h5 = "" self.filewriter = FileWriterMixin(self.service_cfg) self.readout = 1e-3 # 3 ms @@ -100,28 +163,6 @@ class PilatusCsaxs(DetectorBase): msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) return BECMessage.ScanStatusMessage.loads(msg) - # def _load_scan_metadata(self) -> None: - # scan_msg = self._get_current_scan_msg() - # self.metadata = { - # "scanID": scan_msg.content["scanID"], - # "RID": scan_msg.content["info"]["RID"], - # "queueID": scan_msg.content["info"]["queueID"], - # } - # self.scanID = scan_msg.content["scanID"] - # self.scan_number = scan_msg.content["info"]["scan_number"] - # self.exp_time = scan_msg.content["info"]["exp_time"] - # self.num_frames = scan_msg.content["info"]["num_points"] - # self.username = self.device_manager.producer.get( - # MessageEndpoints.account() - # ).decode() - # self.device_manager.devices.mokev.read()["mokev"]["value"] - # # self.triggermode = scan_msg.content["info"]["trigger_mode"] - # # self.filename = self.filewriter.compile_full_filename( - # # self.scan_number, "pilatus_2", 1000, 5, True - # # ) - # # TODO fix with BEC running - # # self.filename = '/sls/X12SA/Data10/e21206/data/test.h5' - def _prep_det(self) -> None: # TODO slow reaction, seemed to have timeout. self._set_det_threshold() @@ -143,7 +184,7 @@ class PilatusCsaxs(DetectorBase): # self.cam.acquire_period.set(self.exp_time + self.readout) self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_exposures.set(1) - self._set_trigger(TriggerSource.INTERNAL) # EXT_TRIGGER) + self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER) def _set_trigger(self, trigger_source: TriggerSource) -> None: """Set trigger source for the detector, either directly to value or TriggerSource.* with @@ -164,12 +205,12 @@ class PilatusCsaxs(DetectorBase): self.filepath_h5 = self.filewriter.compile_full_filename( self.scaninfo.scan_number, "pilatus_2.h5", 1000, 5, True ) - self.cam.file_path.set(f"/dev/shm/zmq/") - self.cam.file_name.set(f"{self.scaninfo.username}_2_{self.scaninfo.scan_number:05d}") - self.cam.auto_increment.set(1) # auto increment - self.cam.file_number.set(0) # first iter - self.cam.file_format.set(0) # 0: TIFF - self.cam.file_template.set("%s%s_%5.5d.cbf") + self.cam.file_path.put(f"/dev/shm/zmq/") + self.cam.file_name.put(f"{self.scaninfo.username}_2_{self.scaninfo.scan_number:05d}") + self.cam.auto_increment.put(1) # auto increment + self.cam.file_number.put(0) # first iter + self.cam.file_format.put(0) # 0: TIFF + self.cam.file_template.put("%s%s_%5.5d.cbf") # compile filename basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/" @@ -254,17 +295,19 @@ class PilatusCsaxs(DetectorBase): if not res.ok: res.raise_for_status() except Exception as exc: - logger.info("exc") + logger.info(f"Pilatus2 wait threw Exception: {exc}") def _close_file_writer(self) -> None: """Close the file writer for pilatus_2 a zmq service is running on xbl-daq-34 that is waiting for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2 """ - - res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") - if not res.ok: - res.raise_for_status() + try: + res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") + if not res.ok: + res.raise_for_status() + except Exception as exc: + logger.info(f"Pilatus2 delete threw Exception: {exc}") def _stop_file_writer(self) -> None: res = requests.put( @@ -278,34 +321,48 @@ class PilatusCsaxs(DetectorBase): def stage(self) -> List[object]: """stage the detector and file writer""" + self._close_file_writer() + self._stop_file_writer() self.scaninfo.load_scan_metadata() self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] + logger.info("Waiting for pilatus2 to be armed") self._prep_det() + logger.info("Pilatus2 armed") + logger.info("Waiting for pilatus2 zmq stream to be ready") self._prep_file_writer() - self.acquire() + logger.info("Pilatus2 zmq ready") + msg = BECMessage.FileMessage( + file_path=self.filepath_h5, done=False, metadata={"input_path": self.destination_path} + ) return super().stage() + def pre_scan(self) -> None: + self.acquire() + def unstage(self) -> List[object]: """unstage the detector and file writer""" # Reset to software trigger self.triggermode = 0 + # TODO if images are missing, consider adding delay self._close_file_writer() self._stop_file_writer() # Only sent this out once data is written to disk since cbf to hdf5 converter will be triggered - msg = BECMessage.FileMessage(file_path=self.filepath, done=True) + msg = BECMessage.FileMessage( + file_path=self.filepath_h5, done=True, metadata={"input_path": self.destination_path} + ) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) - msg = BECMessage.FileMessage(file_path=self.filepath, done=True) self._producer.set_and_publish( MessageEndpoints.file_event(self.name), msg.dumps(), ) + logger.info("Pilatus2 done") return super().unstage() def acquire(self) -> None: diff --git a/ophyd_devices/galil/sgalil_ophyd.py b/ophyd_devices/galil/sgalil_ophyd.py index ae71a3f..3934ba0 100644 --- a/ophyd_devices/galil/sgalil_ophyd.py +++ b/ophyd_devices/galil/sgalil_ophyd.py @@ -155,7 +155,8 @@ class GalilController(Controller): def stop_all_axes(self) -> str: # return self.socket_put_and_receive(f"XQ#STOP,1") # Command stops all threads and motors! - return self.socket_put_and_receive(f"ST") + # self.socket_put_and_receive(f"ST") + return self.socket_put_and_receive(f"AB") def axis_is_referenced(self) -> bool: return bool(float(self.socket_put_and_receive(f"MG allaxref").strip())) @@ -405,7 +406,7 @@ class GalilSetpointSignal(GalilSignalBase): Returns: float: setpoint / target value """ - return self.setpoint + return self.setpoint * self.parent.sign @retry_once @threadlocked