From 1b8aabccc16568c83126d333e06305c79b82252b Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Tue, 20 Aug 2024 13:29:30 +0200 Subject: [PATCH 01/14] First implementation on x07mb-bec_001 after merging with x07mb-test --- .../startup/post_startup.py | 132 ++++++ .../startup/post_startup.py.testBranch | 182 ++++++++ .../startup/pre_startup.py.testBranch | 26 ++ .../device_configs/phoenix_devices.yaml | 73 +++ .../device_configs/phoenix_falcon.yaml | 14 + phoenix_bec/device_configs/phoenix_xmap.yaml | 14 + phoenix_bec/devices/Phoenix_trigger.py | 243 ++++++++++ phoenix_bec/devices/delay_generator_csaxs.py | 345 ++++++++++++++ phoenix_bec/devices/falcon_phoenix_no_hdf5.py | 363 +++++++++++++++ phoenix_bec/devices/xmap_phoenix_no_hdf5.py | 366 +++++++++++++++ .../Development_helpers/Develop_phoenix.py | 0 .../Development_helpers/EditDeviceClasses.py | 9 + .../Base_Classes/BASE_CLASS_devices.txt | 422 ++++++++++++++++++ .../Eiger_Addams.py | 159 +++++++ .../Eiger_Addams.py~ | 219 +++++++++ .../Eiger_Addams_commented.py | 226 ++++++++++ .../Eiger_Addams_commented.py~ | 227 ++++++++++ .../Learn_about_bec_shell/bec_commands.py | 17 + .../command_line_tricks.py | 2 + .../DefiningEpics_Channels.py | 95 ++++ .../BaseClass_Epics.py | 25 ++ phoenix_bec/local_scripts/Linescan_1.py | 86 ++++ phoenix_bec/local_scripts/PhoenixTemplate.py | 75 ++++ phoenix_bec/local_scripts/README.txt | 8 + .../TEST_ConfigPhoenix/__init__.py | 0 .../TEST_ConfigPhoenix/config/__init__.py | 1 + .../TEST_ConfigPhoenix/config/phoenix.py | 79 ++++ .../device_config/LOCAL_config_1.yaml | 24 + .../device_config/LOCAL_local_devices.yaml | 13 + .../LOCAL_phoenix_devices_LOCAL.yaml | 64 +++ .../device_config/phoenix_devices.yaml~ | 57 +++ .../TEST_ConfigPhoenix/devices/__init__.py | 0 .../devices/delay_generator_csaxs.py | 345 ++++++++++++++ .../devices/falcon_csaxs_ORIGINAL.py | 349 +++++++++++++++ .../devices/falcon_phoenix_no_hdf5.py | 362 +++++++++++++++ .../local_scripts/TOBEDELETED/phoenix.py_old2 | 104 +++++ .../TOBEDELETED/phoenix.py_old_wrongTAB | 104 +++++ phoenix_bec/local_scripts/p_test.py | 2 + .../local_scripts/test_phoenix_linescan.py | 93 ++++ phoenix_bec/scans/__init__.py | 1 + phoenix_bec/scans/phoenix_line_scan.py | 115 +++++ phoenix_bec/scripts/Current_setup.txt | 33 ++ phoenix_bec/scripts/README.txt | 2 + phoenix_bec/scripts/__init__.py | 1 + phoenix_bec/scripts/phoenix.py | 92 ++++ 45 files changed, 5169 insertions(+) create mode 100644 phoenix_bec/bec_ipython_client/startup/post_startup.py.testBranch create mode 100644 phoenix_bec/bec_ipython_client/startup/pre_startup.py.testBranch create mode 100644 phoenix_bec/device_configs/phoenix_devices.yaml create mode 100644 phoenix_bec/device_configs/phoenix_falcon.yaml create mode 100644 phoenix_bec/device_configs/phoenix_xmap.yaml create mode 100644 phoenix_bec/devices/Phoenix_trigger.py create mode 100644 phoenix_bec/devices/delay_generator_csaxs.py create mode 100644 phoenix_bec/devices/falcon_phoenix_no_hdf5.py create mode 100644 phoenix_bec/devices/xmap_phoenix_no_hdf5.py create mode 100644 phoenix_bec/local_scripts/Development_helpers/Develop_phoenix.py create mode 100644 phoenix_bec/local_scripts/Development_helpers/EditDeviceClasses.py create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_devices.txt create mode 100644 phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py create mode 100644 phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py~ create mode 100644 phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py create mode 100644 phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py~ create mode 100644 phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/bec_commands.py create mode 100644 phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/command_line_tricks.py create mode 100644 phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py create mode 100644 phoenix_bec/local_scripts/Examples/Learn_about_specific_classes/BaseClass_Epics.py create mode 100644 phoenix_bec/local_scripts/Linescan_1.py create mode 100644 phoenix_bec/local_scripts/PhoenixTemplate.py create mode 100644 phoenix_bec/local_scripts/README.txt create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/__init__.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/__init__.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/phoenix.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_config_1.yaml create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_local_devices.yaml create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_phoenix_devices_LOCAL.yaml create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/phoenix_devices.yaml~ create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/__init__.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/delay_generator_csaxs.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_csaxs_ORIGINAL.py create mode 100644 phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_phoenix_no_hdf5.py create mode 100644 phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old2 create mode 100644 phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old_wrongTAB create mode 100644 phoenix_bec/local_scripts/p_test.py create mode 100644 phoenix_bec/local_scripts/test_phoenix_linescan.py create mode 100644 phoenix_bec/scans/phoenix_line_scan.py create mode 100644 phoenix_bec/scripts/Current_setup.txt create mode 100644 phoenix_bec/scripts/README.txt create mode 100644 phoenix_bec/scripts/__init__.py create mode 100644 phoenix_bec/scripts/phoenix.py diff --git a/phoenix_bec/bec_ipython_client/startup/post_startup.py b/phoenix_bec/bec_ipython_client/startup/post_startup.py index 07d6da4..ffa0da2 100644 --- a/phoenix_bec/bec_ipython_client/startup/post_startup.py +++ b/phoenix_bec/bec_ipython_client/startup/post_startup.py @@ -34,3 +34,135 @@ to setup the prompts. """ # pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module +import time as tt +import sys +from IPython.core.magic import register_line_magic + + +from bec_lib.logger import bec_logger + +logger = bec_logger.logger +#logger = bec_logger.LOGLEVEL.TRACE + +#pylint: disable=invald-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module + +_session_name = "session_phoenix" +# SETUP PROMPTS +bec._ip.prompts.username = "PHOENIX" +bec._ip.prompts.status = 1 + +# make sure that edited modules are reloaded when changed + +print('phoenix_bec/bec_iphyon_client/startup/post_startup.py') +print('... set autoreload of modules') +bec._ip.run_line_magic("reload_ext","autoreload") +bec._ip.run_line_magic("autoreload", "2") + +print('autoreload loaded ') + + + +############################################################################# +# +#... register BL specific magic commands +# +############################################################################## + + + +@register_line_magic +def ph_reload(line): + + ########################################################################## + # + # this reloads certain phoenix related script to the iphython shell. + # useful for debugging/development to be revised for production + # aim of this magic is to quickl reload BL related stuff for debugging + # Most likely there are better ways to do this (possibly bec.reload_user_script() + # but syntax not clear, error messages. Here we know what we do + # + ###################################################################W##### + + from phoenix_bec.scripts import phoenix as PH + print('reload phoenix_bec.scripts.phoenix to iphyhton console') + print('to update version server restart server ') + # need to use global statement here, as I like to reload into space on + # iphyton consoel + global PH,phoenix + print('from phoenix_bec.scripts import phoenix as PH') + print('phoenix = PH.PhoenixBL()') + phoenix = PH.PhoenixBL() + #ph_config=PH.PhoenixConfighelper() +#enddef + +print('##################################################################') +print('register magic') +print('...... %ph_load_xmap ... to reload xmap configuration') + + + +@register_line_magic +def ph_load_xmap(line): + + ### + #magic for loading xmap + ### + + t0=tt.time() + phoenix_server.add_xmap() + print('elapsed time:', tt.time()-t0) +#enddef + +print('...... %ph_load_falcon ... to reload falcon configuration') +@register_line_magic +def ph_load_falcon(line): + + # magic to load falcon + + t0=tt.time() + phoenix_server.add_falcon() + print('elapsed time:', tt.time()-t0) +#enddef + +print('...... %ph_load_config ... to reload phoenix default configuration') +@register_line_magic +def ph_load_config(line): + t0=tt.time() + phoenix_server.add_phoenix_config() + print('elapsed time:', tt.time()-t0) +#enddef + + +##@register_line_magic +#def ph_post_startup(line): +# print('import phoenix_bec.bec_ipython_client.startup.post_startup does not work caused loop ') +# #import phoenix_bec.bec_ipython_client.startup.post_startup +# does not work seems to build a infinite stack... + +#################################################################################### +# +# init phoenix.py from server version as +# .................. phoenix_server=PhoenixBL() +# and in ipython shell only as +# .............. phoenix = Ph.Pheonix(BL() +## +##################################################################################### + +print('###############################################################') +print('init phoenix_bec/scripts/phoenix.py in two different ways') +print(' 1) phoenix_server = PhoenixBL() ... takes code from server version ') +print('SERBVR VERSION DOES NOT WORK ANYMORE ') +print('FOLDER SCRUIPT SEEMS TO BE NON_STANDARD!!!!!!! ') + +#phoenix_server=PhoenixBL() + +print(' 2) phoenix=PH.PhoenixBL() ... on inpython shell only! (for debugging)') + +from phoenix_bec.scripts import phoenix as PH +phoenix = PH.PhoenixBL() + + + +#from phoenix_bec.bec_ipython_client.plugins.phoenix import Phoenix +#from phoenix_bec.devices.falcon_phoenix_no_hdf5 import FalconHDF5Plugins + diff --git a/phoenix_bec/bec_ipython_client/startup/post_startup.py.testBranch b/phoenix_bec/bec_ipython_client/startup/post_startup.py.testBranch new file mode 100644 index 0000000..318ad9a --- /dev/null +++ b/phoenix_bec/bec_ipython_client/startup/post_startup.py.testBranch @@ -0,0 +1,182 @@ +""" +FILE +phoenix_bec.bec_iphyton_client + +Post startup script for the BEC client. This script is executed after the +IPython shell is started. It is used to load the beamline specific +information and to setup the prompts. + +The script is executed in the global namespace of the IPython shell. This +means that all variables defined here are available in the shell. + +While command-line arguments have to be set in the pre-startup script, the +post-startup script can be used to load beamline specific information and +to setup the prompts. + + + +################################################################# +# OLD CODE FROM CSAXS ONLY AS EXAMPLE NEXT LINES CAN BE IGNORED +################################################################ + from bec_lib.logger import bec_logger + + logger = bec_logger.logger + + # pylint: disable=import-error + _args = _main_dict["args"] + + _session_name = "cSAXS" + if _args.session.lower() == "lamni": + from csaxs_bec.bec_ipython_client.plugins.cSAXS import * + from csaxs_bec.bec_ipython_client.plugins.LamNI import * + + _session_name = "LamNI" + lamni = LamNI(bec) + logger.succ#ess("LamNI session loaded.") + + elif _args.session.lower() == "csaxs": + print("Loading cSAXS session") + from csaxs_bec.bec_ipython_client.plugins.cSAXS import * + + logger.success("cSAXS session loaded.") +##################################################################### +""" + + +import time as tt +import sys +from IPython.core.magic import register_line_magic + + +from bec_lib.logger import bec_logger + +logger = bec_logger.logger +#logger = bec_logger.LOGLEVEL.TRACE + +#pylint: disable=invald-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module + +_session_name = "session_phoenix" +# SETUP PROMPTS +bec._ip.prompts.username = "PHOENIX" +bec._ip.prompts.status = 1 + +# make sure that edited modules are reloaded when changed +print('phoenix_bec/bec_iphyon_client/startup/post_startup.py') +print('... set autoreload of modules') +bec._ip.run_line_magic("reload_ext","autoreload") +bec._ip.run_line_magic("autoreload", "2") + +print('autoreload loaded ') + +################################################################ +# +# next lines are not ideal and likely need to be removed they are a temporary fix as +# we had trouble finding certain paths. +# +# we need to work in server_env, otherwise no server is found +# path to ophyd devices is only way to find default ophyd devices +# path to python packages not ideal but only way to add pyp5 and pandas packages +# +############################################################## + + +print('post_startup.py : set some paths as temp fix. This needs to be solved ') + +ophyd_devices_path='/data/test/x07mb-test-bec/bec_deployment/ophyd_devices' +p_path='/data/test/x07mb-test-bec/bec_deployment/bec_server_venv/lib/python3.11/site-packages' + +print('add ',ophyd_devices_path) +print('add ',p_path) + +if ophyd_devices_path not in sys.path: + sys.path.insert(1, os.path.expandvars(ophyd_devices_path)) +#endif + +if p_path not in sys.path: + sys.path.insert(1, os.path.expandvars(p_path)) +#endif + + +############################################################################# +# +#... register BL specific magic commands +# +############################################################################## +# not clear, error messages. Here we know what we do. + # + ######################################################################## + + from phoenix_bec.scripts import phoenix as PH + print('reload phoenix_bec.scripts.phoenix to iphyhton console') + print('to update version server restart server ') + # need to use global statement here, as I like to reload into space on + # iphyton consoel + global PH,phoenix + print('from phoenix_bec.scripts import phoenix as PH') + print('phoenix = PH.PhoenixBL()') + phoenix = PH.PhoenixBL() + #ph_config=PH.PhoenixConfighelper() +#enddef + +print('##################################################################') +print('register magic') +print('...... %ph_load_xmap ... to reload xmap configuration') +@register_line_magic +def ph_load_xmap(line): + + ### + #magic for loading xmap + ### + + t0=tt.time() + phoenix_server.add_xmap() + print('elapsed time:', tt.time()-t0) +#enddef + +print('...... %ph_load_falcon ... to reload falcon configuration') +@register_line_magic +def ph_load_falcon(line): + + # magic to load falcon + + t0=tt.time() + phoenix_server.add_falcon() + print('elapsed time:', tt.time()-t0) +#enddef + +print('...... %ph_load_config ... to reload phoenix default configuration') +@register_line_magic +def ph_load_config(line): + t0=tt.time() + phoenix_server.add_phoenix_config() + print('elapsed time:', tt.time()-t0) +#enddef + + +##@register_line_magic +#def ph_post_startup(line): +# print('import phoenix_bec.bec_ipython_client.startup.post_startup does not work caused loop ') +# #import phoenix_bec.bec_ipython_client.startup.post_startup +# does not work seems to build a infinite stack... + +#################################################################################### +# +# init phoenix.py from server version as +# .................. phoenix_server=PhoenixBL() +# and in ipython shell only as +# .............. phoenix = Ph.Pheonix(BL() +## +##################################################################################### + +print('###############################################################') +print('init phoenix_bec/scripts/phoenix.py in two different ways') +print(' 1) phoenix_server = PhoenixBL() ... takes code from server version ') +phoenix_server=PhoenixBL() + +print(' 2) phoenix=PH.PhoenixBL() ... on inpython shell only! (for debugging)') +from phoenix_bec.scripts import phoenix as PH +phoenix = PH.PhoenixBL() + + +#from phoenix_bec.bec_ipython_client.plugins.phoenix import Phoenix +#from phoenix_bec.devices.falcon_phoenix_no_hdf5 import FalconHDF5Plugins diff --git a/phoenix_bec/bec_ipython_client/startup/pre_startup.py.testBranch b/phoenix_bec/bec_ipython_client/startup/pre_startup.py.testBranch new file mode 100644 index 0000000..e441b89 --- /dev/null +++ b/phoenix_bec/bec_ipython_client/startup/pre_startup.py.testBranch @@ -0,0 +1,26 @@ +""" +Pre-startup script for BEC client. This script is executed before the BEC client +is started. It can be used to add additional command line arguments. +""" + +from bec_lib.service_config import ServiceConfig + + +def extend_command_line_args(parser): + """ + Extend the command line arguments of the BEC client. + """ + + # parser.add_argument("--session", help="Session name", type=str, default="cSAXS") + + return parser + + +# def get_config() -> ServiceConfig: +# """ +# Create and return the service configuration. +# """ +# return ServiceConfig(redis={"host": "localhost", "port": 6379}) + + + diff --git a/phoenix_bec/device_configs/phoenix_devices.yaml b/phoenix_bec/device_configs/phoenix_devices.yaml new file mode 100644 index 0000000..241f85a --- /dev/null +++ b/phoenix_bec/device_configs/phoenix_devices.yaml @@ -0,0 +1,73 @@ +################################################### +# +# phoenix standard devices (motors) +# +# +##################################################### + +# +# MOTORS ES1 +# + +ScanX: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanX' + deviceTags: + - ES-MA1 + - phoenix_bec/device_configs/phoenix_devices.yaml + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +ScanY: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanY' + deviceTags: + - ES-MA1 + - phoenix_bec/device_configs/phoenix_devices.yaml + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false +# +# +# DIODES from ES1 ADC +# +# + +SAI_07_MEAN: + readoutPriority: monitored + description: DIODE + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: 'X07MB-OP2-SAI_07:MEAN' + deviceTags: + - PHOENIX + - phoenix_bec/device_configs/phoenix_devices.yaml + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false + +SAI_08_MEAN: + readoutPriority: monitored + description: DIODE + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: 'X07MB-OP2-SAI_08:MEAN' + deviceTags: + - PHOENIX + - phoenix_bec/device_configs/phoenix_devices.yaml + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false diff --git a/phoenix_bec/device_configs/phoenix_falcon.yaml b/phoenix_bec/device_configs/phoenix_falcon.yaml new file mode 100644 index 0000000..d59f886 --- /dev/null +++ b/phoenix_bec/device_configs/phoenix_falcon.yaml @@ -0,0 +1,14 @@ +falcon_nohdf5: + description: Falcon detector x-ray fluoresence II + deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconcSAXS + deviceConfig: + prefix: 'X07MB-SITORO:' + deviceTags: + - phoenix + - falcon + - no hdf5 + - phoenix_devices.yaml + onFailure: buffer + enabled: true + readoutPriority: async + softwareTrigger: false diff --git a/phoenix_bec/device_configs/phoenix_xmap.yaml b/phoenix_bec/device_configs/phoenix_xmap.yaml new file mode 100644 index 0000000..406949f --- /dev/null +++ b/phoenix_bec/device_configs/phoenix_xmap.yaml @@ -0,0 +1,14 @@ +xmap_nohdf5: + description: XMAP detector x-ray fluoresence II + deviceClass: phoenix_bec.devices.xmap_phoenix_no_hdf5.XMAPphoenix + deviceConfig: + prefix: 'X07MB-XMAP:' + deviceTags: + - phoenix + - xmap + - no hdf5 + - phoenix_bec/device_configs/phoenix_xmap.yaml + onFailure: buffer + enabled: true + readoutPriority: async + softwareTrigger: false diff --git a/phoenix_bec/devices/Phoenix_trigger.py b/phoenix_bec/devices/Phoenix_trigger.py new file mode 100644 index 0000000..a948ee5 --- /dev/null +++ b/phoenix_bec/devices/Phoenix_trigger.py @@ -0,0 +1,243 @@ +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO + +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +class PhoenixTriggerError(Exception): + """Base class for exceptions in this module.""" + + +class PhoenixTriggerTimeoutError(XMAPError): + """Raised when the PhoenixTrigger does not respond in time.""" + + +class PhoenixTriggerDetectorState(enum.IntEnum): + """Detector states for XMAP detector""" + + DONE = 0 + ACQUIRING = 1 + + + + +class PhoenixTriggerSetup(CustomDetectorMixin): + """ + This defines the trigger setup. + + + """ + + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + self.parent.cam.num_images.put(1) + self.parent.cam.image_mode.put(0) # Single + self.parent.cam.trigger_mode.put(0) # auto + else: + # In flyscan, the exp_time is the time between two triggers, + # which minus 15% is used as the acquisition time. + self.parent.cam.acquire_time.put(exposure_time * 0.85) + self.parent.cam.num_images.put(num_points) + self.parent.cam.image_mode.put(1) # Multiple + self.parent.cam.trigger_mode.put(1) # trigger + self.parent.cam.acquire.put(1, wait=False) # arm + + # file writer + self.parent.hdf.lazy_open.put(1) + self.parent.hdf.num_capture.put(num_points) + self.parent.hdf.file_write_mode.put(2) # Stream + self.parent.hdf.capture.put(1, wait=False) + self.parent.hdf.enable.put(1) # enable plugin + + # roi statistics to collect signal and background in a timeseries + self.parent.roistat.enable.put(1) + self.parent.roistat.ts_num_points.put(num_points) + self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start + + logger.success('XXXX stage XXXX') + + def on_trigger(self): + self.parent.cam.acquire.put(1, wait=False) + logger.success('XXXX trigger XXXX') + + return self.wait_with_status( + [(self.parent.cam.acquire.get, 0)], + self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, + all_signals=True + ) + + def on_complete(self): + status = DeviceStatus(self.parent) + + if self.parent.scaninfo.scan_type == 'step': + timeout = DETECTOR_TIMEOUT + else: + timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT + logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) + success = self.wait_for_signals( + [ + (self.parent.cam.acquire.get, 0), + (self.parent.hdf.capture.get, 0), + (self.parent.roistat.ts_acquiring.get, 'Done') + ], + timeout, + check_stopped=True, + all_signals=True + ) + + # publish file location + self.parent.filepath.put(self.parent.hdf.full_file_name.get()) + self.publish_file_location(done=True, successful=success) + + # publish timeseries data + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) + msg = messages.DeviceMessage( + signals={ + self.parent.roistat.roi1.name_.get(): { + 'value': self.parent.roistat.roi1.ts_total.get(), + }, + self.parent.roistat.roi2.name_.get(): { + 'value': self.parent.roistat.roi2.ts_total.get(), + }, + }, + metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg_dict={"data": msg}, + expire=1800, + ) + + logger.success('XXXX complete %d XXXX' % success) + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + def on_stop(self): + logger.success('XXXX stop XXXX') + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + + def on_unstage(self): + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + logger.success('XXXX unstage XXXX') + + +class EigerROIStatPlugin(ROIStatPlugin): + roi1 = ADCpt(ROIStatNPlugin, '1:') + roi2 = ADCpt(ROIStatNPlugin, '2:') + +class PhoenixTrigger(PSIDetectorBase): + + """ + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + in __init__ of PSIDetecor bases + class is initialized + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector + mca (EpicsMCARecord) : MCA parameters for XMAP detector + hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector + MIN_READOUT (float) : Minimum readout time for the detector + + + The class PhoenixTrigger is the class to be called via yaml configuration file + the input arguments are defined by PSIDetectorBase, + and need to be given in the yaml configuration file. + To adress chanels such as 'X07MB-OP2:SMPL-DONE': + + use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. + + PSIDetectorBase( + prefix='', + *,Q + name, + kind=None, + parent=None, + device_manager=None, + **kwargs, + ) + Docstring: + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + parent (object): instance of the parent device + device_manager (object): bec device manager + **kwargs: keyword arguments + File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py + Type: type + Subclasses: SimCamera, SimMonitorAsync + + + + + + """ + #custom_prepare_cls = PhoenixTriggerSetup + + #cam = ADCpt(SLSDetectorCam, 'cam1:') + + + #X07MB-OP2:START-CSMPL.. cont on / off + + # X07MB-OP2:SMPL.. take single sample + #X07MB-OP2:INTR-COUNT.. counter run up + #X07MB-OP2:TOTAL-CYCLES .. cycles set + #X07MB-OP2:SMPL-DONE + + QUESTION HOW does ADCpt kno the EPICS prefix?????? + + #image = ADCpt(ImagePlugin, 'image1:') + #roi1 = ADCpt(ROIPlugin, 'ROI1:') + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + #stats1 = ADCpt(StatsPlugin, 'Stats1:') + #stats2 = ADCpt(StatsPlugin, 'Stats2:') + roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') + #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') + #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') + hdf = ADCpt(HDF5Plugin, 'HDF1:') + ) diff --git a/phoenix_bec/devices/delay_generator_csaxs.py b/phoenix_bec/devices/delay_generator_csaxs.py new file mode 100644 index 0000000..c0d521b --- /dev/null +++ b/phoenix_bec/devices/delay_generator_csaxs.py @@ -0,0 +1,345 @@ +from bec_lib import bec_logger +from ophyd import Component +from ophyd_devices.interfaces.base_classes.psi_delay_generator_base import ( + DDGCustomMixin, + PSIDelayGeneratorBase, + TriggerSource, +) +from ophyd_devices.utils import bec_utils + +logger = bec_logger.logger + + +class DelayGeneratorError(Exception): + """Exception raised for errors.""" + + +class DDGSetup(DDGCustomMixin): + """ + Mixin class for DelayGenerator logic at cSAXS. + + At cSAXS, multiple DDGs were operated at the same time. There different behaviour is + implemented in the ddg_config signals that are passed via the device config. + """ + + def initialize_default_parameter(self) -> None: + """Method to initialize default parameters.""" + for ii, channel in enumerate(self.parent.all_channels): + self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel]) + + self.parent.set_channels("amplitude", self.parent.amplitude.get()) + self.parent.set_channels("offset", self.parent.offset.get()) + # Setup reference + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # Set threshold level for ext. pulses + self.parent.level.put(self.parent.thres_trig_level.get()) + + def prepare_ddg(self) -> None: + """ + Method to prepare scan logic of cSAXS + + Two scantypes are supported: "step" and "fly": + - step: Scan is performed by stepping the motor and acquiring data at each step + - fly: Scan is performed by moving the motor with a constant velocity and acquiring data + + Custom logic for different DDG behaviour during scans. + + - set_high_on_exposure : If True, then TTL signal is high during + the full exposure time of the scan (all frames). + E.g. Keep shutter open for the full scan. + - fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel. + If the value is 0, then the width of the TTL pulse is determined, + no matter which parameters are passed from the scaninfo for exposure time + - set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones + were: SINGLE_SHOT, EXT_RISING_EDGE + """ + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # scantype "step" + if self.parent.scaninfo.scan_type == "step": + # High on exposure means that the signal + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.frames_per_trigger + * (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + # scantype "fly" + elif self.parent.scaninfo.scan_type == "fly": + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + + self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.num_points + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + + else: + raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}") + # Set common DDG parameters + self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.parent.set_channels("delay", 0.0) + + def on_trigger(self) -> None: + """Method to be executed upon trigger""" + if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT: + self.parent.trigger_shot.put(1) + + def check_scan_id(self) -> None: + """ + Method to check if scan_id has changed. + + If yes, then it changes parent.stopped to True, which will stop further actions. + """ + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + def finished(self) -> None: + """Method checks if DDG finished acquisition""" + + def on_pre_scan(self) -> None: + """ + Method called by pre_scan hook in parent class. + + Executes trigger if premove_trigger is Trus. + """ + if self.parent.premove_trigger.get() is True: + self.parent.trigger_shot.put(1) + + +class DelayGeneratorcSAXS(PSIDelayGeneratorBase): + """ + DG645 delay generator at cSAXS (multiple can be in use depending on the setup) + + Default values for setting up DDG. + Note: checks of set calues are not (only partially) included, check manual for details on possible settings. + https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf + + - delay_burst : (float >=0) Delay between trigger and first pulse in burst mode + - delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition + - additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line) + - polarity : (list of 0/1) polarity for different channels + - amplitude : (float) amplitude voltage of TTLs + - offset : (float) offset for ampltitude + - thres_trig_level : (float) threshold of trigger amplitude + + Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg): + + - set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan. + # TODO trigger_width and fixed_ttl could be combined into single list. + - fixed_ttl_width : (list of either 1 or 0), one for each channel. + - trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value. + - set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG. + - premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan). + - set_high_on_stage : (bool) if True, then TTL signal should go high already on stage. + """ + + custom_prepare_cls = DDGSetup + + delay_burst = Component( + 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_config" + ) + + additional_triggers = Component( + bec_utils.ConfigSignal, + name="additional_triggers", + kind="config", + config_storage_name="ddg_config", + ) + + polarity = Component( + bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config" + ) + + fixed_ttl_width = Component( + bec_utils.ConfigSignal, + name="fixed_ttl_width", + kind="config", + config_storage_name="ddg_config", + ) + + amplitude = Component( + 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_config" + ) + + thres_trig_level = Component( + bec_utils.ConfigSignal, + name="thres_trig_level", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_exposure = Component( + bec_utils.ConfigSignal, + name="set_high_on_exposure", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_stage = Component( + bec_utils.ConfigSignal, + name="set_high_on_stage", + kind="config", + config_storage_name="ddg_config", + ) + + set_trigger_source = Component( + bec_utils.ConfigSignal, + name="set_trigger_source", + kind="config", + config_storage_name="ddg_config", + ) + + trigger_width = Component( + bec_utils.ConfigSignal, + name="trigger_width", + kind="config", + config_storage_name="ddg_config", + ) + premove_trigger = Component( + bec_utils.ConfigSignal, + name="premove_trigger", + kind="config", + config_storage_name="ddg_config", + ) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + sim_mode=False, + ddg_config=None, + **kwargs, + ): + """ + Args: + prefix (str, optional): Prefix of the device. Defaults to "". + name (str): Name of the device. + kind (str, optional): Kind of the device. Defaults to None. + read_attrs (list, optional): List of attributes to read. Defaults to None. + configuration_attrs (list, optional): List of attributes to configure. Defaults to None. + parent (Device, optional): Parent device. Defaults to None. + device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None. + sim_mode (bool, optional): Simulation mode flag. Defaults to False. + ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None. + + """ + # Default values for ddg_config signals + self.ddg_config = { + # Setup default values + f"{name}_delay_burst": 0, + f"{name}_delta_width": 0, + f"{name}_additional_triggers": 0, + f"{name}_polarity": [1, 1, 1, 1, 1], + f"{name}_amplitude": 4.5, + f"{name}_offset": 0, + f"{name}_thres_trig_level": 2.5, + # Values for different behaviour during scans + f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0], + f"{name}_trigger_width": None, + f"{name}_set_high_on_exposure": False, + f"{name}_set_high_on_stage": False, + f"{name}_set_trigger_source": "SINGLE_SHOT", + f"{name}_premove_trigger": False, + } + if ddg_config is not None: + # pylint: disable=expression-not-assigned + [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + sim_mode=sim_mode, + **kwargs, + ) + + +if __name__ == "__main__": + # Start delay generator in simulation mode. + # Note: To run, access to Epics must be available. + dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True) diff --git a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py new file mode 100644 index 0000000..f4f5cbd --- /dev/null +++ b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py @@ -0,0 +1,363 @@ +# +# +# changes version for PHOENIX WITHOUT HDF5 plugin to be revised/renamed... +# +# + +import enum +import os +import threading + +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd.mca import EpicsMCARecord +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +logger = bec_logger.logger + + +class FalconError(Exception): + """Base class for exceptions in this module.""" + + +class FalconTimeoutError(FalconError): + """Raised when the Falcon does not respond in time.""" + + +class DetectorState(enum.IntEnum): + """Detector states for Falcon detector""" + + DONE = 0 + ACQUIRING = 1 + + +class TriggerSource(enum.IntEnum): + """Trigger source for Falcon detector""" + + USER = 0 + GATE = 1 + SYNC = 2 + + +class MappingSource(enum.IntEnum): + """Mapping source for Falcon detector""" + + SPECTRUM = 0 + MAPPING = 1 + + +class EpicsDXPFalcon(Device): + """ + DXP parameters for Falcon detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ + + 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") + + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + + +class FalconHDF5Plugins(Device): + """ + HDF5 parameters for Falcon detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ + + """ ---------------------------------------------------------------------------- + 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") + 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") + queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config") + array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") + """ + +class FalconSetup(CustomDetectorMixin): + """ + Falcon setup class for cSAXS + + Parent class: CustomDetectorMixin + + """ + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._lock = threading.RLock() + + def on_init(self) -> None: + """Initialize Falcon detector""" + self.initialize_default_parameter() + self.initialize_detector() + self.initialize_detector_backend() + + def initialize_default_parameter(self) -> None: + """ + Set default parameters for Falcon + + This will set: + - readout (float): readout time in seconds + - value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro + + """ + self.parent.value_pixel_per_buffer = 20 + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize Falcon detector""" + self.stop_detector() + self.stop_detector_backend() + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + # 1 Realtime + self.parent.preset_mode.put(1) + # 0 Normal, 1 Inverted + self.parent.input_logic_polarity.put(0) + # 0 Manual 1 Auto + self.parent.auto_pixels_per_buffer.put(0) + # Sets the number of pixels/spectra in the buffer + self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for Falcon.""" + w=0 + #---------------------------------------------------------------------- + #self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + #self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + #self.parent.hdf5.lazy_open.put(1) + #self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + #self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + #self.parent.nd_array_mode.put(1) + + def on_stage(self) -> None: + """Prepare detector and backend for acquisition""" + self.prepare_detector() + self.prepare_data_backend() + self.publish_file_location(done=False, successful=False) + self.arm_acquisition() + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + w=9 + """ -------------------------------------------------------------- + self.parent.filepath.set( + self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") + ).wait() + file_path, file_name = os.path.split(self.parent.filepath.get()) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put("%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + """ + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.TIMEOUT_FOR_SIGNALS, + check_stopped=True, + all_signals=False, + ): + raise FalconTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def on_unstage(self) -> None: + """Unstage detector and backend""" + pass + + def on_complete(self) -> None: + """Complete detector and backend""" + #------------------------------------------------------------------ + #self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS) + #self.publish_file_location(done=True, successful=True) + w=9 + def on_stop(self) -> None: + """Stop detector and backend""" + self.stop_detector() + #self.stop_detector_backend() + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + #------------------------------------------------------------------- + #signal_conditions = [ + # (lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE) + #] + + #if not self.wait_for_signals( + # signal_conditions=signal_conditions, + # timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, + # all_signals=False, + #): + # # Retry stop detector and wait for remaining time + # raise FalconTimeoutError( + # f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + # ) + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + #self.parent.hdf5.capture.put(0) + w=0 + + def finished(self, timeout: int = 5) -> None: + """Check if scan finished succesfully""" + with self._lock: + total_frames = int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ) + signal_conditions = [ + (self.parent.dxp.current_pixel.get, total_frames), + # (self.parent.hdf5.array_counter.get, total_frames), --------------------- + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()}," + f" send data {self.parent.hdf5.array_counter.get()} from total_frames" + f" {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + def set_trigger( + self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 + ) -> None: + """ + Set triggering mode for detector + + Args: + mapping_mode (MappingSource): Mapping mode for the detector + trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal + ignore_gate (int): Ignore gate from TTL signal; defaults to 0 + + """ + mapping = int(mapping_mode) + trigger = trigger_source + self.parent.collect_mode.put(mapping) + self.parent.pixel_advance_mode.put(trigger) + self.parent.ignore_gate.put(ignore_gate) + + +class FalconcSAXS(PSIDetectorBase): + """ + Falcon Sitoro detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector + mca (EpicsMCARecord) : MCA parameters for Falcon detector + hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = FalconSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + TIMEOUT_FOR_SIGNALS = 5 + + # specify class attributes + dxp = Cpt(EpicsDXPFalcon, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") + + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") + 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") + 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) + 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") + nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") + + +if __name__ == "__main__": + falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) diff --git a/phoenix_bec/devices/xmap_phoenix_no_hdf5.py b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py new file mode 100644 index 0000000..57b0d19 --- /dev/null +++ b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py @@ -0,0 +1,366 @@ +# +# +# changes version for PHOENIX WITHOUT HDF5 plugin to be revised/renamed... +# +# + +import enum +import os +import threading + +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd.mca import EpicsMCARecord +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +logger = bec_logger.logger +#bec_logger.level = bec_logger.LOGLEVEL.TRACE +bec_logger.level = bec_logger.LOGLEVEL.INFO + + + +class XMAPError(Exception): + """Base class for exceptions in this module.""" + + +class XMAPTimeoutError(XMAPError): + """Raised when the XMAP does not respond in time.""" + + +class DetectorState(enum.IntEnum): + """Detector states for XMAP detector""" + + DONE = 0 + ACQUIRING = 1 + + +class TriggerSource(enum.IntEnum): + """Trigger source for XMAP detector""" + + USER = 0 + GATE = 1 + SYNC = 2 + + +class MappingSource(enum.IntEnum): + """Mapping source for XMAP detector""" + + SPECTRUM = 0 + MAPPING = 1 + + +class EpicsDXPXMAP(Device): + """ + DXP parameters for XMAP detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + + elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime") + elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime") + elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime") + + # Energy Filter PVs .... uncomment falcon stuff + #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") + + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + +class XMAPHDF5Plugins(Device): + """ + HDF5 parameters for XMAP detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ + + """ ---------------------------------------------------------------------------- + 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") + 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") + queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config") + array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") + """ + +class XMAPSetup(CustomDetectorMixin): + """ + XMAP setup class for phoenix + + Parent class: CustomDetectorMixin + + """ + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._lock = threading.RLock() + + def on_init(self) -> None: + """Initialize XMAP detector""" + self.initialize_default_parameter() + self.initialize_detector() + self.initialize_detector_backend() + + def initialize_default_parameter(self) -> None: + """ + Set default parameters for XMAP + + This will set: + - readout (float): readout time in seconds + - value_pixel_per_buffer (int): number of spectra in buffer of XMAP + + """ + self.parent.value_pixel_per_buffer = 20 + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize XMAP detector""" + self.stop_detector() + self.stop_detector_backend() + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + # 1 Realtime + self.parent.preset_mode.put(1) + # 0 Normal, 1 Inverted + self.parent.input_logic_polarity.put(0) + # 0 Manual 1 Auto + self.parent.auto_pixels_per_buffer.put(0) + # Sets the number of pixels/spectra in the buffer + self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for XMAP.""" + w=0 + #---------------------------------------------------------------------- + #self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + #self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + #self.parent.hdf5.lazy_open.put(1) + #self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + #self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + #self.parent.nd_array_mode.put(1) + + def on_stage(self) -> None: + """Prepare detector and backend for acquisition""" + self.prepare_detector() + self.prepare_data_backend() + self.publish_file_location(done=False, successful=False) + self.arm_acquisition() + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + w=9 + """ -------------------------------------------------------------- + self.parent.filepath.set( + self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") + ).wait() + file_path, file_name = os.path.split(self.parent.filepath.get()) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put("%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + """ + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.TIMEOUT_FOR_SIGNALS, + check_stopped=True, + all_signals=False, + ): + raise XMAPTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def on_unstage(self) -> None: + """Unstage detector and backend""" + pass + + def on_complete(self) -> None: + """Complete detector and backend""" + #------------------------------------------------------------------ + #self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS) + #self.publish_file_location(done=True, successful=True) + w=9 + def on_stop(self) -> None: + """Stop detector and backend""" + self.stop_detector() + #self.stop_detector_backend() + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + #------------------------------------------------------------------- + #signal_conditions = [ + # (lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE) + #]stage2 = StageXY(prefix='X07MB',name='-ES-MA1', name='stage2') + # timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, + # all_signals=False, + #): + # # Retry stop detector and wait for remaining time + # raise XMAPTimeoutError( + # f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + # ) + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + #self.parent.hdf5.capture.put(0) + w=0 + + def finished(self, timeout: int = 5) -> None: + """Check if scan finished succesfully""" + with self._lock: + total_frames = int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ) + signal_conditions = [ + (self.parent.dxp.current_pixel.get, total_frames), + # (self.parent.hdf5.array_counter.get, total_frames), --------------------- + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"XMAP missed a trigger: received trigger {self.parent.dxp.current_pixel.get()}," + f" send data {self.parent.hdf5.array_counter.get()} from total_frames" + f" {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + def set_trigger( + self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 + ) -> None: + """ + Set triggering mode for detector + + Args: + mapping_mode (MappingSource): Mapping mode for the detector + trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal + ignore_gate (int): Ignore gate from TTL signal; defaults to 0 + + """ + mapping = int(mapping_mode) + trigger = trigger_source + self.parent.collect_mode.put(mapping) + self.parent.pixel_advance_mode.put(trigger) + self.parent.ignore_gate.put(ignore_gate) + + +class XMAPphoenix(PSIDetectorBase): + """MCA + XMAP detector for phoenix + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + in __init__ of PSIDetecor base + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector + mca (EpicsMCARecord) : MCA parameters for XMAP detector + hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = XMAPSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + TIMEOUT_FOR_SIGNALS = 5 + + # specify class attributes + dxp = Cpt(EpicsDXPXMAP, "dxp1:") + + mca1 = Cpt(EpicsMCARecord, "mca1") + mca2 = Cpt(EpicsMCARecord, "mca2") + mca3 = Cpt(EpicsMCARecord, "mca3") + mca4 = Cpt(EpicsMCARecord, "mca4") + print('load hdf5') + #hdf5 = Cpt(XMAPHDF5Plugins, "HDF1:") + + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") + 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") + #triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True) #=========== falcon only + # events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True) #=========== falcon only + #input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True) #=========== falcon only + #output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True) #=========== falcon only + 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") + #nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") + print('DONE connecton chanels in XMAPphoenix') diff --git a/phoenix_bec/local_scripts/Development_helpers/Develop_phoenix.py b/phoenix_bec/local_scripts/Development_helpers/Develop_phoenix.py new file mode 100644 index 0000000..e69de29 diff --git a/phoenix_bec/local_scripts/Development_helpers/EditDeviceClasses.py b/phoenix_bec/local_scripts/Development_helpers/EditDeviceClasses.py new file mode 100644 index 0000000..7c65a3b --- /dev/null +++ b/phoenix_bec/local_scripts/Development_helpers/EditDeviceClasses.py @@ -0,0 +1,9 @@ +from phoenix_bec.devices.xmap_phoenix_no_hdf5 import XMAPphoenix +from phoenix_bec.scripts.phoenix import PhoenixBL + +phoenix=PhoenixBL() +phoenix.read_phoenix_config() + +# +# how do we get this to iphython command line ? +# \ No newline at end of file diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_devices.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_devices.txt new file mode 100644 index 0000000..857eb5b --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_devices.txt @@ -0,0 +1,422 @@ +This file contains a selection of base classes to remember where to find them and how they look + + +######################################################################################### +# +# +# PART I device classes psi_detector_base.py +# CONTAINS +# DetectorInitErroR(Exception) +# CustomDetectorMixing +# PSIDetectorBase(Device) +# +# +######################################################################################### + +psi_detector_base.py +https://gitlab.psi.ch/bec/ophyd_devices/-/blob/main/ophyd_devices/interfaces/base_classes/psi_detector_base.py +""This module contains the base class for SLS detectors. We follow the approach to integrate +PSI detectors into the BEC system based on this base class. The base class is used to implement +certain methods that are expected by BEC, such as stage, unstage, trigger, stop, etc... +We use composition with a custom prepare class to implement BL specific logic for the detector. +The beamlines need to inherit from the CustomDetectorMixing for their mixin classes.""" + +file psi_detector_base.py + + + +import os + +class DetectorInitError(Exception): + """Raised when initiation of the device class fails, + due to missing device manager or not started in sim_mode.""" + + +class CustomDetectorMixin: + """ + Mixin class for custom detector logic + + This class is used to implement BL specific logic for the detector. + It is used in the PSIDetectorBase class. + + For the integration of a new detector, the following functions should + help with integrating functionality, but additional ones can be added. + + Check PSIDetectorBase for the functions that are called during relevant function calls of + stage, unstage, trigger, stop and _init. + """ + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + self.parent = parent + + def on_init(self) -> None: + """ + Init sequence for the detector + """ +_base.py +ready has all current parameters for the upcoming scan. + + In case the backend service is writing data on disk, this step should include publishing + a file_event and file_message to BEC to inform the system where the data is written to. + + IMPORTANT: + It must be safe to assume that the device is ready for the scan + to start immediately once this function is finished. + """ + + def on_unstage(self) -> None: + """ + Specify actions to be executed during unstage. + + This step should include checking if the acqusition was successful, + and publishing the file location and file event message, + with flagged done to BEC. + """ + + def on_stop(self) -> None: + """ + Specify actions to be executed during stop. + This must also set self.parent.stopped to True. + + This step should include stopping the detector and backend service. + """ + + def on_trigger(self) -> None | DeviceStatus: + """ + Specify actions to be executed upon receiving trigger signal. + Return a DeviceStatus object or None + """ + + def on_pre_scan(self) -> None: + """ + Specify actions to be executed right before a scan starts. + + Only use if needed, and it is recommended to keep this function as short/fast as possible. + """ + + def on_complete(self) -> None | DeviceStatus: + """ + Specify actions to be executed when the scan is complete. + + This can for instance be to check with the detector and backend if all data is written succsessfully. + """ + + def publish_file_location(self, done: bool, successful: bool, metadata: dict = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + metadata (dict): additional metadata to publish + """ + if metadata is None: + metadata = {} + + msg = messages.FileMessage( + file_path=self.parent.filepath.get(), + done=done, + successful=successful, + metadata=metadata, + ) + pipe = self.parent.connector.pipeline() + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + def wait_for_signals( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + ) -> bool: + """ + Convenience wrapper to allow waiting for signals to reach a certain condition. + For EPICs PVs, an example usage is pasted at the bottom. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + + Returns: + bool: True if all signals are in the desired state, False if timeout is reached + + >>> Example usage for EPICS PVs: + >>> self.wait_for_signals(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True) + """ + + timer = 0 + while True: + checks = [ + get_current_state() == condition + for get_current_state, condition in signal_conditions + ] + if check_stopped is True and self.parent.stopped is True: + return False + if (all_signals and all(checks)) or (not all_signals and any(checks)): + return True + if timer > timeout: + return False + time.sleep(interval) + timer += interval + + def wait_with_status( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + exception_on_timeout: Exception = TimeoutError("Timeout while waiting for signals"), + ) -> DeviceStatus: + """Utility function to wait for signals in a thread. + Returns a DevicesStatus object that resolves either to set_finished or set_exception. + The DeviceStatus is attached to the parent device, i.e. the detector object inheriting from PSIDetectorBase. + + Usage: + This function should be used to wait for signals to reach a certain condition, especially in the context of + on_trigger and on_complete. If it is not used, functions may block and slow down the performance of BEC. + It will return a DeviceStatus object that is to be returned from the function. Once the conditions are met, + the DeviceStatus will be set to set_finished in case of success or set_exception in case of a timeout or exception. + The exception can be specified with the exception_on_timeout argument. The default exception is a TimeoutError. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + + Returns: + DeviceStatus: DeviceStatus object that resolves either to set_finished or set_exception + """ + + status = DeviceStatus(self.parent) + + # utility function to wrap the wait_for_signals function + def wait_for_signals_wrapper( + status: DeviceStatus, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool, + interval: float, + all_signals: bool, + exception_on_timeout: Exception = TimeoutError("Timeout while waiting for signals"), + ): + """Convenient wrapper around wait_for_signals to set status based on the result. + + Args: + status (DeviceStatus): DeviceStatus object to be set + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + """ + try: + result = self.wait_for_signals( + signal_conditions, timeout, check_stopped, interval, all_signals + ) + if result: + status.set_finished() + else: + status.set_exception(exception_on_timeout) + except Exception as exc: + status.set_exception(exc=exc) + + thread = threading.Thread( + target=wait_for_signals_wrapper, + args=( + status, + signal_conditions, + timeout, + check_stopped, + interval, + all_signals, + exception_on_timeout, + ), + daemon=True, + ) + thread.start() + return status + + +class PSIDetectorBase(Device): + """ + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + parent (object): instance of the parent device + device_manager (object): bec device manager + **kwargs: keyword arguments + """ + + filepath = Component(SetableSignal, value="", kind=Kind.config) + + custom_prepare_cls = CustomDetectorMixin + + def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, parent=parent, **kwargs) + self.stopped = False + self.name = name + self.service_cfg = None + self.scaninfo = None + self.filewriter = None + self._update_filewriter() + + if not issubclass(self.custom_prepare_cls, CustomDetectorMixin): + raise DetectorInitError("Custom prepare class must be subclass of CustomDetectorMixin") + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + + if device_manager: + self._update_service_config() + self.device_manager = device_manager + else: + self.device_manager = bec_utils.DMMock() + base_path = kwargs["basepath"] if "basepath" in kwargs else "." + self.service_cfg = {"base_path": os.path.abspath(base_path)} + + self.connector = self.device_manager.connector + self._update_scaninfo() + self._update_filewriter() + self._init() + + def _update_filewriter(self) -> None: + """Update filewriter with service config""" + self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector) + + def _update_scaninfo(self) -> None: + """Update scaninfo from BecScaninfoMixing + This depends on device manager and operation/sim_mode + """ + self.scaninfo = BecScaninfoMixin(self.device_manager) + self.scaninfo.load_scan_metadata() + + def _update_service_config(self) -> None: + """Update service config from BEC service config + + If bec services are not running and SERVICE_CONFIG is NONE, we fall back to the current directory. + """ + # pylint: disable=import-outside-toplevel + from bec_lib.bec_service import SERVICE_CONFIG + + if SERVICE_CONFIG: + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] + return + self.service_cfg = {"base_path": os.path.abspath(".")} + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and set stopped flagged to True if it has.""" + old_scan_id = self.scaninfo.scan_id + self.scaninfo.load_scan_metadata() + if self.scaninfo.scan_id != old_scan_id: + self.stopped = True + + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters""" + self.custom_prepare.on_init() + + def stage(self) -> list[object]: + """ + Stage device in preparation for a scan. + First we check if the device is already staged. Stage is idempotent, + if staged twice it should raise (we let ophyd.Device handle the raise here). + We reset the stopped flag and get the scaninfo from BEC, before calling custom_prepare.on_stage. + + Returns: + list(object): list of objects that were staged + + """ + if self._staged != Staged.no: + return super().stage() + self.stopped = False + self.scaninfo.load_scan_metadata() + self.custom_prepare.on_stage() + return super().stage() + + def pre_scan(self) -> None: + """Pre-scan logic. + + This function will be called from BEC directly before the scan core starts, and should only implement + time-critical actions. Therefore, it should also be kept as short/fast as possible. + I.e. Arming a detector in case there is a risk of timing out. + """ + self.custom_prepare.on_pre_scan() + + def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + # pylint: disable=assignment-from-no-return + status = self.custom_prepare.on_trigger() + if isinstance(status, DeviceStatus): + return status + return super().trigger() + + def complete(self) -> None: + """Complete the acquisition, called from BEC. + + This function is called after the scan is complete, just before unstage. + We can check here with the data backend and detector if the acquisition successfully finished. + + Actions are implemented in custom_prepare.on_complete since they are beamline specific. + """ + # pylint: disable=assignment-from-no-return + status = self.custom_prepare.on_complete() + if isinstance(status, DeviceStatus): + return status + status = DeviceStatus(self) + status.set_finished() + return status + + def unstage(self) -> list[object]: + """ + Unstage device after a scan. + + We first check if the scanID has changed, thus, the scan was unexpectedly interrupted but the device was not stopped. + If that is the case, the stopped flag is set to True, which will immediately unstage the device. + + Custom_prepare.on_unstage is called to allow for BL specific logic to be executed. + + Returns: + list(object): list of objects that were unstaged + """ + self.check_scan_id() + if self.stopped is True: + return super().unstage() + self.custom_prepare.on_unstage() + self.stopped = False + return super().unstage() + + def stop(self, *, success=False) -> None: + """ + Stop the scan, with camera and file writer + + """ + self.custom_prepare.on_stop() + super().stop(success=success) + self.stopped = True diff --git a/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py new file mode 100644 index 0000000..46b205c --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py @@ -0,0 +1,159 @@ +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd_devices.devices.areadetector.cam import SLSDetectorCam +from ophyd_devices.devices.areadetector.plugins import ( + ImagePlugin_V35 as ImagePlugin, + StatsPlugin_V35 as StatsPlugin, + HDF5Plugin_V35 as HDF5Plugin, + ROIPlugin_V35 as ROIPlugin, + ROIStatPlugin_V35 as ROIStatPlugin, + ROIStatNPlugin_V35 as ROIStatNPlugin, +) +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + self.parent.cam.num_images.put(1) + self.parent.cam.image_mode.put(0) # Single + self.parent.cam.trigger_mode.put(0) # auto + else: + # In flyscan, the exp_time is the time between two triggers, + # which minus 15% is used as the acquisition time. + self.parent.cam.acquire_time.put(exposure_time * 0.85) + self.parent.cam.num_images.put(num_points) + self.parent.cam.image_mode.put(1) # Multiple + self.parent.cam.trigger_mode.put(1) # trigger + self.parent.cam.acquire.put(1, wait=False) # arm + + # file writer + self.parent.hdf.lazy_open.put(1) + self.parent.hdf.num_capture.put(num_points) + self.parent.hdf.file_write_mode.put(2) # Stream + self.parent.hdf.capture.put(1, wait=False) + self.parent.hdf.enable.put(1) # enable plugin + + # roi statistics to collect signal and background in a timeseries + self.parent.roistat.enable.put(1) + self.parent.roistat.ts_num_points.put(num_points) + self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start + + logger.success('XXXX stage XXXX') + + def on_trigger(self): + self.parent.cam.acquire.put(1, wait=False) + logger.success('XXXX trigger XXXX') + + return self.wait_with_status( + [(self.parent.cam.acquire.get, 0)], + self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, + all_signals=True + ) + + def on_complete(self): + status = DeviceStatus(self.parent) + + if self.parent.scaninfo.scan_type == 'step': + timeout = DETECTOR_TIMEOUT + else: + timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT + logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) + success = self.wait_for_signals( + [ + (self.parent.cam.acquire.get, 0), + (self.parent.hdf.capture.get, 0), + (self.parent.roistat.ts_acquiring.get, 'Done') + ], + timeout, + check_stopped=True, + all_signals=True + ) + + # publish file location + self.parent.filepath.put(self.parent.hdf.full_file_name.get()) + self.publish_file_location(done=True, successful=success) + + # publish timeseries data + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) + msg = messages.DeviceMessage( + signals={ + self.parent.roistat.roi1.name_.get(): { + 'value': self.parent.roistat.roi1.ts_total.get(), + }, + self.parent.roistat.roi2.name_.get(): { + 'value': self.parent.roistat.roi2.ts_total.get(), + }, + }, + metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg_dict={"data": msg}, + expire=1800, + ) + + logger.success('XXXX complete %d XXXX' % success) + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + def on_stop(self): + logger.success('XXXX stop XXXX') + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + + def on_unstage(self): + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + logger.success('XXXX unstage XXXX') + + +class EigerROIStatPlugin(ROIStatPlugin): + roi1 = ADCpt(ROIStatNPlugin, '1:') + roi2 = ADCpt(ROIStatNPlugin, '2:') + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + + cam = ADCpt(SLSDetectorCam, 'cam1:') + + #image = ADCpt(ImagePlugin, 'image1:') + #roi1 = ADCpt(ROIPlugin, 'ROI1:') + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + #stats1 = ADCpt(StatsPlugin, 'Stats1:') + #stats2 = ADCpt(StatsPlugin, 'Stats2:') + roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') + #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') + #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') + hdf = ADCpt(HDF5Plugin, 'HDF1:') + ) diff --git a/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py~ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py~ new file mode 100644 index 0000000..a4131cd --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams.py~ @@ -0,0 +1,219 @@ +""" +This was takem from Addam repository +and commented after discussion with Xiaquiang + +####################################### +#Strutur of software +####################################### + +1) Eiger500KSetup(CustomDetectorMixin)---> inherits from CustomdetectorMixing + +2) class Eiger500K(PSIDetectorBase) ---> inherits from PSIDetectorBase + + +These calsses are linkes to each other by the command + +custom_prepare_cls = Eiger500KSetup + +it references/activates the self.parent used in Eiger500Ksetup to attributes defined in Eiger500K. +for example in Eiger500K, we define cam = ADCpt(SLSDetectorCam, 'cam1:'), which is referenced in \ +Eiger500Ksetup by the command +self.parent.cam.array_counter.put(0) + + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + + + + + ... etc + + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + cam = ADCpt(SLSDetectorCam, 'cam1:') + + .... etc + +################################################### +# +# Using ROI in flyscans +# +################################################### +Here the roi plugin of the area detector is used + + +""" + +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd_devices.devices.areadetector.cam import SLSDetectorCam +from ophyd_devices.devices.areadetector.plugins import ( + ImagePlugin_V35 as ImagePlugin, + StatsPlugin_V35 as StatsPlugin, + HDF5Plugin_V35 as HDF5Plugin, + ROIPlugin_V35 as ROIPlugin, + ROIStatPlugin_V35 as ROIStatPlugin, + ROIStatNPlugin_V35 as ROIStatNPlugin, +) +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + self.parent.cam.num_images.put(1) + self.parent.cam.image_mode.put(0) # Single + self.parent.cam.trigger_mode.put(0) # auto + else: + # In flyscan, the exp_time is the time between two triggers, + # which minus 15% is used as the acquisition time. + self.parent.cam.acquire_time.put(exposure_time * 0.85) + self.parent.cam.num_images.put(num_points) + self.parent.cam.image_mode.put(1) # Multiple + self.parent.cam.trigger_mode.put(1) # trigger + self.parent.cam.acquire.put(1, wait=False) # arm + + # file writer + self.parent.hdf.lazy_open.put(1) + self.parent.hdf.num_capture.put(num_points) + self.parent.hdf.file_write_mode.put(2) # Stream + self.parent.hdf.capture.put(1, wait=False) + self.parent.hdf.enable.put(1) # enable plugin + + # roi statistics to collect signal and background in a timeseries + self.parent.roistat.enable.put(1) + self.parent.roistat.ts_num_points.put(num_points) + self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start + + logger.success('XXXX stage XXXX') + + def on_trigger(self): + self.parent.cam.acquire.put(1, wait=False) + logger.success('XXXX trigger XXXX') + + return self.wait_with_status( + [(self.parent.cam.acquire.get, 0)], + self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, + all_signals=True + ) + + def on_complete(self): + status = DeviceStatus(self.parent) + + if self.parent.scaninfo.scan_type == 'step': + timeout = DETECTOR_TIMEOUT + else: + timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT + logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) + success = self.wait_for_signals( + [ + (self.parent.cam.acquire.get, 0), + (self.parent.hdf.capture.get, 0), + (self.parent.roistat.ts_acquiring.get, 'Done') + ], + timeout, + check_stopped=True, + all_signals=True + ) + + # publish file location + self.parent.filepath.put(self.parent.hdf.full_file_name.get()) + self.publish_file_location(done=True, successful=success) + + # publish timeseries data + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) + msg = messages.DeviceMessage( + signals={ + self.parent.roistat.roi1.name_.get(): { + 'value': self.parent.roistat.roi1.ts_total.get(), + }, + self.parent.roistat.roi2.name_.get(): { + 'value': self.parent.roistat.roi2.ts_total.get(), + }, + }, + metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg_dict={"data": msg}, + expire=1800, + ) + + logger.success('XXXX complete %d XXXX' % success) + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + def on_stop(self): + logger.success('XXXX stop XXXX') + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + + def on_unstage(self): + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + logger.success('XXXX unstage XXXX') + + +class EigerROIStatPlugin(ROIStatPlugin): + roi1 = ADCpt(ROIStatNPlugin, '1:') + roi2 = ADCpt(ROIStatNPlugin, '2:') + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + + cam = ADCpt(SLSDetectorCam, 'cam1:') + + #image = ADCpt(ImagePlugin, 'image1:') + #roi1 = ADCpt(ROIPlugin, 'ROI1:') + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + #stats1 = ADCpt(StatsPlugin, 'Stats1:') + #stats2 = ADCpt(StatsPlugin, 'Stats2:') + roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') + #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') + #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') + hdf = ADCpt(HDF5Plugin, 'HDF1:') diff --git a/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py new file mode 100644 index 0000000..e7d534c --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py @@ -0,0 +1,226 @@ +""" + + +This was taken from Addam repository +and commented after discussion with Xiaquiang +As this also uses the are detector software, we might use is as well for teh flacon /xmap integration. +This is based on falcon integration at cSAXS. Advantage over integration from BEC team is that this integration is very slim, and does not contain channels which are nor needed for data acquisition. +One could consider to split integration into two classes, as slim one for data acquisition, and a more complete one for 'operation and monitoring' +The channels in the 2nd class would then the saved only before a scan, which the 'data acquisition class' would be read at each data point. + +####################################### +# Strutur of software +####################################### + +1) Eiger500KSetup(CustomDetectorMixin)---> inherits from CustomdetectorMixing + +2) class Eiger500K(PSIDetectorBase) ---> inherits from PSIDetectorBase + + +These calsses are linkes to each other by the command + +custom_prepare_cls = Eiger500KSetup + +it references/activates the self.parent used in Eiger500Ksetup to attributes defined in Eiger500K. +for example in Eiger500K, we define cam = ADCpt(SLSDetectorCam, 'cam1:'), which is referenced in \ +Eiger500Ksetup by the command +self.parent.cam.array_counter.put(0) + + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + + ... etc + + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + cam = ADCpt(SLSDetectorCam, 'cam1:') + + .... etc + +################################################### +# +# flyscans +# +################################################### + +Images seem to be saves via hdf5 plugin (no live view is possible() + +ROI is used to store 0d Signal. +These data are stored continuously collected an array in the ROI plugin +this could be used to store ROI data of XMAP/FALCON + +""" + +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd_devices.devices.areadetector.cam import SLSDetectorCam +from ophyd_devices.devices.areadetector.plugins import ( + ImagePlugin_V35 as ImagePlugin, + StatsPlugin_V35 as StatsPlugin, + HDF5Plugin_V35 as HDF5Plugin, + ROIPlugin_V35 as ROIPlugin, + ROIStatPlugin_V35 as ROIStatPlugin, + ROIStatNPlugin_V35 as ROIStatNPlugin, +) +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + self.parent.cam.num_images.put(1) + self.parent.cam.image_mode.put(0) # Single + self.parent.cam.trigger_mode.put(0) # auto + else: + # In flyscan, the exp_time is the time between two triggers, + # which minus 15% is used as the acquisition time. + self.parent.cam.acquire_time.put(exposure_time * 0.85) + self.parent.cam.num_images.put(num_points) + self.parent.cam.image_mode.put(1) # Multiple + self.parent.cam.trigger_mode.put(1) # trigger + self.parent.cam.acquire.put(1, wait=False) # arm + + # file writer + self.parent.hdf.lazy_open.put(1) + self.parent.hdf.num_capture.put(num_points) + self.parent.hdf.file_write_mode.put(2) # Stream + self.parent.hdf.capture.put(1, wait=False) + self.parent.hdf.enable.put(1) # enable plugin + + # roi statistics to collect signal and background in a timeseries + self.parent.roistat.enable.put(1) + self.parent.roistat.ts_num_points.put(num_points) + self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start + + logger.success('XXXX stage XXXX') + + def on_trigger(self): + self.parent.cam.acquire.put(1, wait=False) + logger.success('XXXX trigger XXXX') + + return self.wait_with_status( + [(self.parent.cam.acquire.get, 0)], + self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, + all_signals=True + ) + + def on_complete(self): + status = DeviceStatus(self.parent) + + if self.parent.scaninfo.scan_type == 'step': + timeout = DETECTOR_TIMEOUT + else: + timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT + logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) + success = self.wait_for_signals( + [ + (self.parent.cam.acquire.get, 0), + (self.parent.hdf.capture.get, 0), + (self.parent.roistat.ts_acquiring.get, 'Done') + ], + timeout, + check_stopped=True, + all_signals=True + ) + + # publish file location + self.parent.filepath.put(self.parent.hdf.full_file_name.get()) + self.publish_file_location(done=True, successful=success) + + # publish timeseries data + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) + msg = messages.DeviceMessage( + signals={ + self.parent.roistat.roi1.name_.get(): { + 'value': self.parent.roistat.roi1.ts_total.get(), + }, + self.parent.roistat.roi2.name_.get(): { + 'value': self.parent.roistat.roi2.ts_total.get(), + }, + }, + metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg_dict={"data": msg}, + expire=1800, + ) + + logger.success('XXXX complete %d XXXX' % success) + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + def on_stop(self): + logger.success('XXXX stop XXXX') + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + + def on_unstage(self): + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + logger.success('XXXX unstage XXXX') + + +class EigerROIStatPlugin(ROIStatPlugin): + roi1 = ADCpt(ROIStatNPlugin, '1:') + roi2 = ADCpt(ROIStatNPlugin, '2:') + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + + cam = ADCpt(SLSDetectorCam, 'cam1:') + + #image = ADCpt(ImagePlugin, 'image1:') + #roi1 = ADCpt(ROIPlugin, 'ROI1:') + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + #stats1 = ADCpt(StatsPlugin, 'Stats1:') + #stats2 = ADCpt(StatsPlugin, 'Stats2:') + roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') + #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') + #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') + hdf = ADCpt(HDF5Plugin, 'HDF1:') diff --git a/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py~ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py~ new file mode 100644 index 0000000..062cbcb --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Commented_software_from_other_Beamlines/Eiger_Addams_commented.py~ @@ -0,0 +1,227 @@ +""" +This was takem from Addam repository +and commented after discussion with Xiaquiang + +As this also uses the aere detector software, we might use is as well for teh flacon /xmap integration. +This is based on falcon integration at cSAXS. Advantage over integration from BEC team is that this integration is very slem, and +does not contain channels which are nor needed for data aquisition. +One could consider to split integration into two classses, as slim one for data aquisition, and a more complete one for 'operation and monitoring' +The channekls in the 2nd calss would then the saved only before a scan, which the 'data aquisition class' would be read at each data point. + + +####################################### +# Strutur of software +####################################### + +1) Eiger500KSetup(CustomDetectorMixin)---> inherits from CustomdetectorMixing + +2) class Eiger500K(PSIDetectorBase) ---> inherits from PSIDetectorBase + + +These calsses are linkes to each other by the command + +custom_prepare_cls = Eiger500KSetup + +it references/activates the self.parent used in Eiger500Ksetup to attributes defined in Eiger500K. +for example in Eiger500K, we define cam = ADCpt(SLSDetectorCam, 'cam1:'), which is referenced in \ +Eiger500Ksetup by the command +self.parent.cam.array_counter.put(0) + + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + + ... etc + + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + cam = ADCpt(SLSDetectorCam, 'cam1:') + + .... etc + +################################################### +# +# flyscans +# +################################################### + +Images seem to be saves via hdf5 plugin (no live vuiew is possible() +ROI +Here the roi plugin of the area detector is used. + + + +""" + +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd_devices.devices.areadetector.cam import SLSDetectorCam +from ophyd_devices.devices.areadetector.plugins import ( + ImagePlugin_V35 as ImagePlugin, + StatsPlugin_V35 as StatsPlugin, + HDF5Plugin_V35 as HDF5Plugin, + ROIPlugin_V35 as ROIPlugin, + ROIStatPlugin_V35 as ROIStatPlugin, + ROIStatNPlugin_V35 as ROIStatNPlugin, +) +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +class Eiger500KSetup(CustomDetectorMixin): + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + def on_stage(self): + exposure_time = self.parent.scaninfo.exp_time + num_points = self.parent.scaninfo.num_points + + # camera acquisition parameters + self.parent.cam.array_counter.put(0) + if self.parent.scaninfo.scan_type == 'step': + self.parent.cam.acquire_time.put(exposure_time) + self.parent.cam.num_images.put(1) + self.parent.cam.image_mode.put(0) # Single + self.parent.cam.trigger_mode.put(0) # auto + else: + # In flyscan, the exp_time is the time between two triggers, + # which minus 15% is used as the acquisition time. + self.parent.cam.acquire_time.put(exposure_time * 0.85) + self.parent.cam.num_images.put(num_points) + self.parent.cam.image_mode.put(1) # Multiple + self.parent.cam.trigger_mode.put(1) # trigger + self.parent.cam.acquire.put(1, wait=False) # arm + + # file writer + self.parent.hdf.lazy_open.put(1) + self.parent.hdf.num_capture.put(num_points) + self.parent.hdf.file_write_mode.put(2) # Stream + self.parent.hdf.capture.put(1, wait=False) + self.parent.hdf.enable.put(1) # enable plugin + + # roi statistics to collect signal and background in a timeseries + self.parent.roistat.enable.put(1) + self.parent.roistat.ts_num_points.put(num_points) + self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start + + logger.success('XXXX stage XXXX') + + def on_trigger(self): + self.parent.cam.acquire.put(1, wait=False) + logger.success('XXXX trigger XXXX') + + return self.wait_with_status( + [(self.parent.cam.acquire.get, 0)], + self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, + all_signals=True + ) + + def on_complete(self): + status = DeviceStatus(self.parent) + + if self.parent.scaninfo.scan_type == 'step': + timeout = DETECTOR_TIMEOUT + else: + timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT + logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) + success = self.wait_for_signals( + [ + (self.parent.cam.acquire.get, 0), + (self.parent.hdf.capture.get, 0), + (self.parent.roistat.ts_acquiring.get, 'Done') + ], + timeout, + check_stopped=True, + all_signals=True + ) + + # publish file location + self.parent.filepath.put(self.parent.hdf.full_file_name.get()) + self.publish_file_location(done=True, successful=success) + + # publish timeseries data + metadata = self.parent.scaninfo.scan_msg.metadata + metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) + msg = messages.DeviceMessage( + signals={ + self.parent.roistat.roi1.name_.get(): { + 'value': self.parent.roistat.roi1.ts_total.get(), + }, + self.parent.roistat.roi2.name_.get(): { + 'value': self.parent.roistat.roi2.ts_total.get(), + }, + }, + metadata=self.parent.scaninfo.scan_msg.metadata + ) + self.parent.connector.xadd( + topic=MessageEndpoints.device_async_readback( + scan_id=self.parent.scaninfo.scan_id, device=self.parent.name + ), + msg_dict={"data": msg}, + expire=1800, + ) + + logger.success('XXXX complete %d XXXX' % success) + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + def on_stop(self): + logger.success('XXXX stop XXXX') + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + + def on_unstage(self): + self.parent.cam.acquire.put(0) + self.parent.hdf.capture.put(0) + self.parent.roistat.ts_control.put(2) + logger.success('XXXX unstage XXXX') + + +class EigerROIStatPlugin(ROIStatPlugin): + roi1 = ADCpt(ROIStatNPlugin, '1:') + roi2 = ADCpt(ROIStatNPlugin, '2:') + +class Eiger500K(PSIDetectorBase): + """ + """ + custom_prepare_cls = Eiger500KSetup + + cam = ADCpt(SLSDetectorCam, 'cam1:') + + #image = ADCpt(ImagePlugin, 'image1:') + #roi1 = ADCpt(ROIPlugin, 'ROI1:') + #roi2 = ADCpt(ROIPlugin, 'ROI2:') + #stats1 = ADCpt(StatsPlugin, 'Stats1:') + #stats2 = ADCpt(StatsPlugin, 'Stats2:') + roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') + #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') + #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') + hdf = ADCpt(HDF5Plugin, 'HDF1:') diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/bec_commands.py b/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/bec_commands.py new file mode 100644 index 0000000..1f31817 --- /dev/null +++ b/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/bec_commands.py @@ -0,0 +1,17 @@ + + + + + +print('#######################################') +print('bec.show_global_vars()') +bec.show_global_vars() + +print('#######################################') +print('bec.show_all_commands()') +bec.show_all_commands() + + +print('#######################################') +print('bec.list_user_scripts()') +bec.list_user_scripts() diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/command_line_tricks.py b/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/command_line_tricks.py new file mode 100644 index 0000000..f38103f --- /dev/null +++ b/phoenix_bec/local_scripts/Examples/Learn_about_bec_shell/command_line_tricks.py @@ -0,0 +1,2 @@ +print('Work with unix shell comands :') +print('use %ls %mkdir etc ') diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py new file mode 100644 index 0000000..840cffb --- /dev/null +++ b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py @@ -0,0 +1,95 @@ +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd import Component as Cpt + +#option I via direct acces to classes + +def print_dic(clname,cl): + print('') + print('-------- ',clname) + for ii in cl.__dict__: + if '_' not in ii: + + try: + print(ii,' ---- ',cl.__getattribute__(ii)) + except: + print(ii) + + + +ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') +ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') +DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') +SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') +CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + +#prefix='XXXX:' +y_cpt = Cpt(EpicsMotor, 'ScanX') +# Option 2 using component + +device_ins=Device('X07MB-ES-MA1:',name=('device_name')) +print(' initialzation of device_in=Device(X07MB-ES-MA1:,name=(device_name)') +print('device_ins.__init__') +print(device_ins.__init__) +print_dic('class Device',Device) +print_dic('instance of device device_ins',device_ins) + + +print(' ') +print('DEFINE class StageXY... prefix variable not defined ') + +class StageXY(Device): + + print('in StageXY') + try: + print('prefix',prefix) + except: + print('prefix not defined') + + x = Cpt(EpicsMotor, 'ScanX') + y = Cpt(EpicsMotor, 'ScanY') + +class StageXY2(Device): + #def __init__(self,prefix,name=''): + # print('in StageXY') + # print(prefix) + ##print('prefix',prefix) + + x = Cpt(EpicsMotor, 'ScanX') + y = Cpt(EpicsMotor, 'ScanY') + + + +print() +print('init xy_stage, use input parameter from Device and prefix is defined') +xy_stage = StageXY('X07MB-ES-MA1:', name='stage') + +print_dic('class StageXY',StageXY) +print_dic('instance of StageXY',xy_stage) + +############################################# +# This is basic bluesky +# Epics motor def seems to use init params in device, whcih are +# __init__( +# self, +# prefix="", +# *, +# name, +# kind=None, +# read_attrs=None, +# configuration_attrs=None, +# parent=None, +# **kwargs, +# ): +# +######################################################### + +print('xy_stage.x.prefix') +print(xy_stage.x.prefix) +xy_stage.__dict__ + + +# to move motor use +# stage.x.move(0) +# to see all dict +# stage.x.__dict__ diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_specific_classes/BaseClass_Epics.py b/phoenix_bec/local_scripts/Examples/Learn_about_specific_classes/BaseClass_Epics.py new file mode 100644 index 0000000..33bbb99 --- /dev/null +++ b/phoenix_bec/local_scripts/Examples/Learn_about_specific_classes/BaseClass_Epics.py @@ -0,0 +1,25 @@ +import epics as ep + +################ +# Testing base class epics +# The raw code is located in the file +# bec_client_venv/lib64/python3.11/site-packages/epics/__init__.py +# in bec start script by run -i +# option -i ensures taht iphyjon shell and scritp are in same namespace +# run -i BaseClass_Epics.py + + +pvname='X07MB-OP2-SAI_07:INP-OFS' + +print('ep.cainfo(pvname)') +ep.cainfo(pvname) +print('caget(pvname)') + +ep.caput(pvname,0.5,wait=.1) +res=ep.caget(pvname) +print('1:',res) +ep.caput(pvname,0.01,wait=.1) +res=ep.caget(pvname) +print('2',res) +print('Start camon to see effect , change value of pv ') +ep.camonitor(pvname) diff --git a/phoenix_bec/local_scripts/Linescan_1.py b/phoenix_bec/local_scripts/Linescan_1.py new file mode 100644 index 0000000..c35f269 --- /dev/null +++ b/phoenix_bec/local_scripts/Linescan_1.py @@ -0,0 +1,86 @@ +#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation + +#bec.config.load_demo_config() + +bec.config.update_session_with_file("config/config_1.yaml") + +os.system('mv *.yaml tmp') + + + +class PhoenixBL: + + #define some epics channels + + def __init__(self): + from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + from ophyd import Component as Cpt + self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + self.fielda =EpicsSignal(name='SMPL',read_pv='X07MB-SCAN:scan1.P1SP',write_pv='X07MB-SCAN:scan1.P1SP') +#end class + +ph=PhoenixBL() + +print('---------------------------------') + +# scan will not diode +print(' SCAN DO NOT READ DIODE ') +dev.PH_curr_conf.readout_priority='baseline' # do not read detector +ti=tt.time_ns() +s1=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) +tf=tt.time_ns() + +print('elapsed time',(tf-ti)/1e9) +# scan will read diode +print(' SCAN READ DIODE ') +tt.sleep(2) +dev.PH_curr_conf.readout_priority='monitored' # read detector + +s2=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=11,exp_time=.3,relative=False,delay=2) + + +""" +next lines do not work as pandas is not installed on test system + +res1 = s1.scan.to_pandas() +re1 = res1.to_numpy() +print('Scana') +print(res1) +print('') +print('Scan2 at pandas ') +print(res2) +print('Scan2 as numpy ') +print(res2) +""" + + + + + + \ No newline at end of file diff --git a/phoenix_bec/local_scripts/PhoenixTemplate.py b/phoenix_bec/local_scripts/PhoenixTemplate.py new file mode 100644 index 0000000..af47e90 --- /dev/null +++ b/phoenix_bec/local_scripts/PhoenixTemplate.py @@ -0,0 +1,75 @@ +""" +Scritpt to be developed as template for phoenic scritps +""" + #from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd importPhoenixTemplate.pyitioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +import time + +#import ophyd +import os +import sys +import importlib +import ophyd + + +#logger = bec_logger.logger + # load local configuration +#bec.config.load_demo_config() + +# .. define base path for directory with scripts + +PhoenixBL=0 +from ConfigPHOENIX.config.phoenix import PhoenixBL +#from ConfigPHOENIX.devices.falcon_csaxs import FalconSetup +# initialize general parameter +ph=PhoenixBL() + +bec.config.update_session_with_file('./ConfigPHOENIX/device_config/phoenix_devices.yaml') + +time.sleep(1) + + + +s1=scans.line_scan(dev.ScanX,0,0.002,steps=4,exp_time=1,relative=False,delay=2) + +s2=scans.phoenix_line_scan(dev.ScanX,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) + + + + +""" +print('---------------------------------') + +# scan will not diode +print(' SCAN DO NOT READ DIODE ') +dev.PH_curr_conf.readout_priority='baseline' # do not read detector +ti=tt.time_ns() +s1=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) +tf=tt.time_ns() + +print('elapsed time',(tf-ti)/1e9) +# scan will read diode +print(' SCAN READ DIODE ')s is not installed on test system +ScanX_conf,0,0.002,steps=11,exp_time=.3,relative=False,delay=2) + +""" +""" +next lines do not work as pandas is not installed on test system + +res1 = s1.scan.to_pandas() +re1 = res1.to_numpy() +print('Scana') +print(res1) +print('') +print('Scan2 at pandas ') +print(res2) +print('Scan2 as numpy ') +print(res2) +""" diff --git a/phoenix_bec/local_scripts/README.txt b/phoenix_bec/local_scripts/README.txt new file mode 100644 index 0000000..6d32ff0 --- /dev/null +++ b/phoenix_bec/local_scripts/README.txt @@ -0,0 +1,8 @@ +This diretory is for scripts, test etc. which are not loaded into the server. + +Hence no directory should contain a file named +__init__.py + + +For now we keep it in the phoenix_bec structure, but for operation, such files should be located out side of the +bec_phoenix plugin. diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/__init__.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/__init__.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/__init__.py new file mode 100644 index 0000000..187bcd0 --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/__init__.py @@ -0,0 +1 @@ +from .phoenix import PhoenixBL \ No newline at end of file diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/phoenix.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/phoenix.py new file mode 100644 index 0000000..3d7b855 --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/config/phoenix.py @@ -0,0 +1,79 @@ +#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus +from bec_lib.logger import bec_logger +logger = bec_logger.logger + +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation +#bec.config.load_demo_config() + +# .. define base path for directory with scripts + + +class PhoenixBL(): + """ + + General class for PHOENIX beamline + + """ + #define some epics channels + #scan_name = "phoenix_base" + + def __init__(self): + """ + init PhoenixBL() in ConfigPHOENIX.config.phoenix + """ + import os + #from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + #from ophyd import Component as Cpt + #self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + #self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + #self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + #self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + #self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + #self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + # load local configuration + + print('init PhoenixBL') + + self.path_scripts_local = '/data/test/x07mb-test-bec/bec_deployment/LocalScripts/' + self.path_config_local = self.path_scripts_local + 'ConfigPHOENIX/' # base dir for local configurations + self.path_devices_local = self.path_config_local + 'device_config/' # local yamal file + self.file_device_conf = self.path_devices_local + 'phoenix_devices.yaml' + + #bec.config.update_session_with_file(self.file_device_conf) + # last command created yaml backup, for now just move it away + #os.system('mv *.yaml '+Devices_local+'/recovery_configs') + #os.system('mv *.yaml tmp') + + def read_def_config(): + bec.config.update_session_with_file(self.file_device_conf) + + + def print_setup(self): + """ + docstring print_setup + + + """ + + print(self.path_scripts_local) + diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_config_1.yaml b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_config_1.yaml new file mode 100644 index 0000000..78f6158 --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_config_1.yaml @@ -0,0 +1,24 @@ +PH_ScanX_conf: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanX' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false +PH_curr_conf: + readoutPriority: monitored + description: DIODE + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: 'X07MB-OP2-SAI_07:MEAN' + deviceTags: + - PHOENIX + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false + \ No newline at end of file diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_local_devices.yaml b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_local_devices.yaml new file mode 100644 index 0000000..5a2cd7a --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_local_devices.yaml @@ -0,0 +1,13 @@ + +Falcon: + readoutPriority: baseline + description: 'Falcon' + deviceClass: .ConfigPHOENIX.devices.falcon_phoenix_no_hdf5 + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanX' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + + diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_phoenix_devices_LOCAL.yaml b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_phoenix_devices_LOCAL.yaml new file mode 100644 index 0000000..7e47722 --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/LOCAL_phoenix_devices_LOCAL.yaml @@ -0,0 +1,64 @@ +falcon: + description: Falcon detector x-ray fluoresence + deviceClass: phoenix_bec.devices._csaxs.FalconcSAXS + deviceConfig: + prefix: 'X07MB-SITORO:' + deviceTags: + - cSAXS + - falcon + onFailure: buffer + enabled: true + readoutPriority: async + softwareTrigger: false + +# MOTORS ES1 +# +ScanX: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanX' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +ScanY: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanY' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false +# +# +# DIODES from ES1 ADC +# +# +#SAI_07_MEAN: +# readoutPriority: monitored +# description: DIODE +# deviceClass: ophyd.EpicsSignalRO +# deviceConfig: +# auto_monitor: true +# read_pv: 'X07MB-OP2-SAI_07:MEAN' +# onFailure: buffer +# enabled: true +# readOnly: true +# softwareTrigger: false + +#SAI_08_MEAN: +# readoutPriority: monitored +# description: DIODE +# deviceClass: ophyd.EpicsSignalRO +# deviceConfig: +# auto_monitor: true +# read_pv: 'X07MB-OP2-SAI_08:MEAN' +# onFailure: buffer +# enabled: true +# readOnly: true +# softwareTrigger: false \ No newline at end of file diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/phoenix_devices.yaml~ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/phoenix_devices.yaml~ new file mode 100644 index 0000000..633bd84 --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/device_config/phoenix_devices.yaml~ @@ -0,0 +1,57 @@ +# +# MOTORS ES1 +# +ScanX: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanX' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +ScanY: + readoutPriority: baseline + description: 'Horizontal sample position' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MB-ES-MA1:ScanY' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false +# +# +# DIODES from ES1 ADC +# +# + +SAI_07_MEAN: + readoutPriority: monitored + description: DIODE + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: 'X07MB-OP2-SAI_07:MEAN' + deviceTags: + - PHOENIX + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false + +SAI_08_MEAN: + readoutPriority: monitored + description: DIODE + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + auto_monitor: true + read_pv: 'X07MB-OP2-SAI_08:MEAN' + deviceTags: + - PHOENIX + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false \ No newline at end of file diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/__init__.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/delay_generator_csaxs.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/delay_generator_csaxs.py new file mode 100644 index 0000000..c0d521b --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/delay_generator_csaxs.py @@ -0,0 +1,345 @@ +from bec_lib import bec_logger +from ophyd import Component +from ophyd_devices.interfaces.base_classes.psi_delay_generator_base import ( + DDGCustomMixin, + PSIDelayGeneratorBase, + TriggerSource, +) +from ophyd_devices.utils import bec_utils + +logger = bec_logger.logger + + +class DelayGeneratorError(Exception): + """Exception raised for errors.""" + + +class DDGSetup(DDGCustomMixin): + """ + Mixin class for DelayGenerator logic at cSAXS. + + At cSAXS, multiple DDGs were operated at the same time. There different behaviour is + implemented in the ddg_config signals that are passed via the device config. + """ + + def initialize_default_parameter(self) -> None: + """Method to initialize default parameters.""" + for ii, channel in enumerate(self.parent.all_channels): + self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel]) + + self.parent.set_channels("amplitude", self.parent.amplitude.get()) + self.parent.set_channels("offset", self.parent.offset.get()) + # Setup reference + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_channels( + "reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs] + ) + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # Set threshold level for ext. pulses + self.parent.level.put(self.parent.thres_trig_level.get()) + + def prepare_ddg(self) -> None: + """ + Method to prepare scan logic of cSAXS + + Two scantypes are supported: "step" and "fly": + - step: Scan is performed by stepping the motor and acquiring data at each step + - fly: Scan is performed by moving the motor with a constant velocity and acquiring data + + Custom logic for different DDG behaviour during scans. + + - set_high_on_exposure : If True, then TTL signal is high during + the full exposure time of the scan (all frames). + E.g. Keep shutter open for the full scan. + - fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel. + If the value is 0, then the width of the TTL pulse is determined, + no matter which parameters are passed from the scaninfo for exposure time + - set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones + were: SINGLE_SHOT, EXT_RISING_EDGE + """ + self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) + # scantype "step" + if self.parent.scaninfo.scan_type == "step": + # High on exposure means that the signal + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.frames_per_trigger + * (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + # scantype "fly" + elif self.parent.scaninfo.scan_type == "fly": + if self.parent.set_high_on_exposure.get(): + # caluculate parameters + exp_time = ( + self.parent.delta_width.get() + + self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + + self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1) + ) + total_exposure = exp_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = 1 + self.parent.additional_triggers.get() + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + for value, channel in zip( + self.parent.fixed_ttl_width.get(), self.parent.all_channels + ): + logger.debug(f"Trying to set DDG {channel} to {value}") + if value != 0: + self.parent.set_channels("width", value, channels=[channel]) + else: + # caluculate parameters + exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time + total_exposure = exp_time + self.parent.scaninfo.readout_time + delay_burst = self.parent.delay_burst.get() + num_burst_cycle = ( + self.parent.scaninfo.num_points + self.parent.additional_triggers.get() + ) + + # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too + if not self.parent.trigger_width.get(): + self.parent.set_channels("width", exp_time) + else: + self.parent.set_channels("width", self.parent.trigger_width.get()) + + else: + raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}") + # Set common DDG parameters + self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") + self.parent.set_channels("delay", 0.0) + + def on_trigger(self) -> None: + """Method to be executed upon trigger""" + if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT: + self.parent.trigger_shot.put(1) + + def check_scan_id(self) -> None: + """ + Method to check if scan_id has changed. + + If yes, then it changes parent.stopped to True, which will stop further actions. + """ + old_scan_id = self.parent.scaninfo.scan_id + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scan_id != old_scan_id: + self.parent.stopped = True + + def finished(self) -> None: + """Method checks if DDG finished acquisition""" + + def on_pre_scan(self) -> None: + """ + Method called by pre_scan hook in parent class. + + Executes trigger if premove_trigger is Trus. + """ + if self.parent.premove_trigger.get() is True: + self.parent.trigger_shot.put(1) + + +class DelayGeneratorcSAXS(PSIDelayGeneratorBase): + """ + DG645 delay generator at cSAXS (multiple can be in use depending on the setup) + + Default values for setting up DDG. + Note: checks of set calues are not (only partially) included, check manual for details on possible settings. + https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf + + - delay_burst : (float >=0) Delay between trigger and first pulse in burst mode + - delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition + - additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line) + - polarity : (list of 0/1) polarity for different channels + - amplitude : (float) amplitude voltage of TTLs + - offset : (float) offset for ampltitude + - thres_trig_level : (float) threshold of trigger amplitude + + Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg): + + - set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan. + # TODO trigger_width and fixed_ttl could be combined into single list. + - fixed_ttl_width : (list of either 1 or 0), one for each channel. + - trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value. + - set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG. + - premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan). + - set_high_on_stage : (bool) if True, then TTL signal should go high already on stage. + """ + + custom_prepare_cls = DDGSetup + + delay_burst = Component( + 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_config" + ) + + additional_triggers = Component( + bec_utils.ConfigSignal, + name="additional_triggers", + kind="config", + config_storage_name="ddg_config", + ) + + polarity = Component( + bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config" + ) + + fixed_ttl_width = Component( + bec_utils.ConfigSignal, + name="fixed_ttl_width", + kind="config", + config_storage_name="ddg_config", + ) + + amplitude = Component( + 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_config" + ) + + thres_trig_level = Component( + bec_utils.ConfigSignal, + name="thres_trig_level", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_exposure = Component( + bec_utils.ConfigSignal, + name="set_high_on_exposure", + kind="config", + config_storage_name="ddg_config", + ) + + set_high_on_stage = Component( + bec_utils.ConfigSignal, + name="set_high_on_stage", + kind="config", + config_storage_name="ddg_config", + ) + + set_trigger_source = Component( + bec_utils.ConfigSignal, + name="set_trigger_source", + kind="config", + config_storage_name="ddg_config", + ) + + trigger_width = Component( + bec_utils.ConfigSignal, + name="trigger_width", + kind="config", + config_storage_name="ddg_config", + ) + premove_trigger = Component( + bec_utils.ConfigSignal, + name="premove_trigger", + kind="config", + config_storage_name="ddg_config", + ) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + device_manager=None, + sim_mode=False, + ddg_config=None, + **kwargs, + ): + """ + Args: + prefix (str, optional): Prefix of the device. Defaults to "". + name (str): Name of the device. + kind (str, optional): Kind of the device. Defaults to None. + read_attrs (list, optional): List of attributes to read. Defaults to None. + configuration_attrs (list, optional): List of attributes to configure. Defaults to None. + parent (Device, optional): Parent device. Defaults to None. + device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None. + sim_mode (bool, optional): Simulation mode flag. Defaults to False. + ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None. + + """ + # Default values for ddg_config signals + self.ddg_config = { + # Setup default values + f"{name}_delay_burst": 0, + f"{name}_delta_width": 0, + f"{name}_additional_triggers": 0, + f"{name}_polarity": [1, 1, 1, 1, 1], + f"{name}_amplitude": 4.5, + f"{name}_offset": 0, + f"{name}_thres_trig_level": 2.5, + # Values for different behaviour during scans + f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0], + f"{name}_trigger_width": None, + f"{name}_set_high_on_exposure": False, + f"{name}_set_high_on_stage": False, + f"{name}_set_trigger_source": "SINGLE_SHOT", + f"{name}_premove_trigger": False, + } + if ddg_config is not None: + # pylint: disable=expression-not-assigned + [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + sim_mode=sim_mode, + **kwargs, + ) + + +if __name__ == "__main__": + # Start delay generator in simulation mode. + # Note: To run, access to Epics must be available. + dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True) diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_csaxs_ORIGINAL.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_csaxs_ORIGINAL.py new file mode 100644 index 0000000..962eb9f --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_csaxs_ORIGINAL.py @@ -0,0 +1,349 @@ +import enum +import os +import threading + +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd.mca import EpicsMCARecord +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +logger = bec_logger.logger + + +class FalconError(Exception): + """Base class for exceptions in this module.""" + + +class FalconTimeoutError(FalconError): + """Raised when the Falcon does not respond in time.""" + + +class DetectorState(enum.IntEnum): + """Detector states for Falcon detector""" + + DONE = 0 + ACQUIRING = 1 + + +class TriggerSource(enum.IntEnum): + """Trigger source for Falcon detector""" + + USER = 0 + GATE = 1 + SYNC = 2 + + +class MappingSource(enum.IntEnum): + """Mapping source for Falcon detector""" + + SPECTRUM = 0 + MAPPING = 1 + + +class EpicsDXPFalcon(Device): + """ + DXP parameters for Falcon detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ + + 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") + + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + + +class FalconHDF5Plugins(Device): + """ + HDF5 parameters for Falcon detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ + + 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") + 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") + queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config") + array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") + + +class FalconSetup(CustomDetectorMixin): + """ + Falcon setup class for cSAXS + + Parent class: CustomDetectorMixin + + """ + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._lock = threading.RLock() + + def on_init(self) -> None: + """Initialize Falcon detector""" + self.initialize_default_parameter() + self.initialize_detector() + self.initialize_detector_backend() + + def initialize_default_parameter(self) -> None: + """ + Set default parameters for Falcon + + This will set: + - readout (float): readout time in seconds + - value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro + + """ + self.parent.value_pixel_per_buffer = 20 + self.update_readout_time() + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize Falcon detector""" + self.stop_detector() + self.stop_detector_backend() + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + # 1 Realtime + self.parent.preset_mode.put(1) + # 0 Normal, 1 Inverted + self.parent.input_logic_polarity.put(0) + # 0 Manual 1 Auto + self.parent.auto_pixels_per_buffer.put(0) + # Sets the number of pixels/spectra in the buffer + self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for Falcon.""" + self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + self.parent.hdf5.lazy_open.put(1) + self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + self.parent.nd_array_mode.put(1) + + def on_stage(self) -> None: + """Prepare detector and backend for acquisition""" + self.prepare_detector() + self.prepare_data_backend() + self.publish_file_location(done=False, successful=False) + self.arm_acquisition() + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + self.parent.filepath.set( + self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") + ).wait() + file_path, file_name = os.path.split(self.parent.filepath.get()) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put("%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.TIMEOUT_FOR_SIGNALS, + check_stopped=True, + all_signals=False, + ): + raise FalconTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def on_unstage(self) -> None: + """Unstage detector and backend""" + pass + + def on_complete(self) -> None: + """Complete detector and backend""" + self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS) + self.publish_file_location(done=True, successful=True) + + def on_stop(self) -> None: + """Stop detector and backend""" + self.stop_detector() + self.stop_detector_backend() + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + + signal_conditions = [ + (lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE) + ] + + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, + all_signals=False, + ): + # Retry stop detector and wait for remaining time + raise FalconTimeoutError( + f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + ) + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + self.parent.hdf5.capture.put(0) + + def finished(self, timeout: int = 5) -> None: + """Check if scan finished succesfully""" + with self._lock: + total_frames = int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ) + signal_conditions = [ + (self.parent.dxp.current_pixel.get, total_frames), + (self.parent.hdf5.array_counter.get, total_frames), + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()}," + f" send data {self.parent.hdf5.array_counter.get()} from total_frames" + f" {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + def set_trigger( + self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 + ) -> None: + """ + Set triggering mode for detector + + Args: + mapping_mode (MappingSource): Mapping mode for the detector + trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal + ignore_gate (int): Ignore gate from TTL signal; defaults to 0 + + """ + mapping = int(mapping_mode) + trigger = trigger_source + self.parent.collect_mode.put(mapping) + self.parent.pixel_advance_mode.put(trigger) + self.parent.ignore_gate.put(ignore_gate) + + +class FalconcSAXS(PSIDetectorBase): + """ + Falcon Sitoro detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector + mca (EpicsMCARecord) : MCA parameters for Falcon detector + hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = FalconSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + TIMEOUT_FOR_SIGNALS = 5 + + # specify class attributes + dxp = Cpt(EpicsDXPFalcon, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") + + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") + 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") + 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) + 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") + nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") + + +if __name__ == "__main__": + falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) diff --git a/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_phoenix_no_hdf5.py new file mode 100644 index 0000000..d62bc1f --- /dev/null +++ b/phoenix_bec/local_scripts/TEST_ConfigPhoenix/devices/falcon_phoenix_no_hdf5.py @@ -0,0 +1,362 @@ +# +# # +# # +# # copied file from csaxs, but with all hdf5 commentred out.. (lazy for quit testing ) +# # file needs to be renamed +# # +# # +# +# +import enum +import os +import threading + +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd.mca import EpicsMCARecord +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +logger = bec_logger.logger + + +class FalconError(Exception): + """Base class for exceptions in this module.""" + + +class FalconTimeoutError(FalconError): + """Raised when the Falcon does not respond in time.""" + + +class DetectorState(enum.IntEnum): + """Detector states for Falcon detector""" + + DONE = 0 + ACQUIRING = 1 + + +class TriggerSource(enum.IntEnum): + """Trigger source for Falcon detector""" + + USER = 0 + GATE = 1 + SYNC = 2 + + +class MappingSource(enum.IntEnum): + """Mapping source for Falcon detector""" + + SPECTRUM = 0 + MAPPING = 1 + + +class EpicsDXPFalcon(Device): + """ + DXP parameters for Falcon detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ + + 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") + + current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") + + +class FalconHDF5Plugins(Device): + """ + HDF5 parameters for Falcon detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ + + 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") + 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") + queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config") + array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") + + +class FalconSetup(CustomDetectorMixin): + """ + Falcon setup class for cSAXS + + Parent class: CustomDetectorMixin + + """ + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._lock = threading.RLock() + + def on_init(self) -> None: + """Initialize Falcon detector""" + self.initialize_default_parameter() + self.initialize_detector() + self.initialize_detector_backend() + + def initialize_default_parameter(self) -> None: + """ + Set default parameters for Falcon + + This will set: + - readout (float): readout time in seconds + - value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro + + """ + #self.parent.value_pixel_per_buffer = 20 ------------- + #self.update_readout_time() + w=2 -------------- + + def update_readout_time(self) -> None: + """Set readout time for Eiger9M detector""" + readout_time = ( + self.parent.scaninfo.readout_time + if hasattr(self.parent.scaninfo, "readout_time") + else self.parent.MIN_READOUT + ) + self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) + + def initialize_detector(self) -> None: + """Initialize Falcon detector""" + self.stop_detector() + self.stop_detector_backend() + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + # 1 Realtime + self.parent.preset_mode.put(1) + # 0 Normal, 1 Inverted + self.parent.input_logic_polarity.put(0) + # 0 Manual 1 Auto + self.parent.auto_pixels_per_buffer.put(0) + # Sets the number of pixels/spectra in the buffer + self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for Falcon.""" + self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + self.parent.hdf5.lazy_open.put(1) + self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + self.parent.nd_array_mode.put(1) + + def on_stage(self) -> None: + """Prepare detector and backend for acquisition""" + self.prepare_detector() + self.prepare_data_backend() + self.publish_file_location(done=False, successful=False) + self.arm_acquisition() + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + self.parent.filepath.set( + self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") + ).wait() + file_path, file_name = os.path.split(self.parent.filepath.get()) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put("%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + + def arm_acquisition(self) -> None: + """Arm detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.TIMEOUT_FOR_SIGNALS, + check_stopped=True, + all_signals=False, + ): + raise FalconTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def on_unstage(self) -> None: + """Unstage detector and backend""" + pass + + def on_complete(self) -> None: + """Complete detector and backend""" + self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS) + self.publish_file_location(done=True, successful=True) + + def on_stop(self) -> None: + """Stop detector and backend""" + self.stop_detector() + self.stop_detector_backend() + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + + #signal_conditions = [ + # (lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE) + #] + signal_condition = [] + ## --------- next commented out wg hdf 5 -------------------------------------------- + #if not self.wait_for_signals( + # signal_conditions=signal_conditions, + # timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, + # all_signals=False, + #): + # # Retry stop detector and wait for remaining time + # raise FalconTimeoutError( + # f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + # ) + + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + #self.parent.hdf5.capture.put(0) --------------------------- + w=3 + + def finished(self, timeout: int = 5) -> None: + """Check if scan finished succesfully""" + with self._lock: + total_frames = int( + self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger + ) + signal_conditions = [ + (self.parent.dxp.current_pixel.get, total_frames), + (self.parent.hdf5.array_counter.get, total_frames), + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()}," + f" send data {self.parent.hdf5.array_counter.get()} from total_frames" + f" {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + def set_trigger( + self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 + ) -> None: + """ + Set triggering mode for detector + + Args: + mapping_mode (MappingSource): Mapping mode for the detector + trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal + ignore_gate (int): Ignore gate from TTL signal; defaults to 0 + + """ + mapping = int(mapping_mode) + trigger = trigger_source + self.parent.collect_mode.put(mapping) + self.parent.pixel_advance_mode.put(trigger) + self.parent.ignore_gate.put(ignore_gate) + + +class FalconcSAXS(PSIDetectorBase): + """ + Falcon Sitoro detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector + mca (EpicsMCARecord) : MCA parameters for Falcon detector + hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + # specify Setup class + custom_prepare_cls = FalconSetup + # specify minimum readout time for detector + MIN_READOUT = 3e-3 + TIMEOUT_FOR_SIGNALS = 5 + + # specify class attributes + dxp = Cpt(EpicsDXPFalcon, "dxp1:") + mca = Cpt(EpicsMCARecord, "mca1") + # hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") ------------------ + + stop_all = Cpt(EpicsSignal, "StopAll") + erase_all = Cpt(EpicsSignal, "EraseAll") + start_all = Cpt(EpicsSignal, "StartAll") + state = Cpt(EpicsSignal, "Acquiring") + 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") + 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) + 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") + nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") + + +if __name__ == "__main__": + falcon = FalconcSAXS(name="falcon", prefix="X07MB-SITORO:", sim_mode=True) diff --git a/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old2 b/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old2 new file mode 100644 index 0000000..5b10954 --- /dev/null +++ b/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old2 @@ -0,0 +1,104 @@ +s#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +from bec_lib.config_helper import ConfigHelper +from bec_lib.logger import bec_logger +logger = bec_logger.logger + +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation +#bec.config.load_demo_config() + +# .. define base path for directory with scripts + + +class PhoenixBL(): + """ + + General class for PHOENIX beamline from phoenix_bec/phoenic_bec/scripts + + """ + + def __init__(self): + + + """ + init PhoenixBL() in phoenix_bec/scripts + + """ + + + import os + + print('..... init PhoenixBL from phoenix_bec/scripts/phoenix.py') + + #from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + #from ophyd import Component as Cpt + #self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + #self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + #self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + #self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + #self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + #self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + # load local configuration + + + + self.path_scripts_local = '/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/phoenix_bec/local_scripts/' + self.path_config_local = self.path_scripts_local + 'TEST_ConfigPhoenix/' # base dir for local configurations + self.path_devices_local = self.path_config_local + 'Local_device_config/' # local yamal file + self.file_devices_file_local = self.path_devices_local + 'phoenix_devices.yaml' + + + self.path_phoenix_bec ='/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/' + self.path_devices = self.path_phoenix_bec + 'phoenix_bec/device_configs/' # local yamal file + self.file_devices_file = self.path_phoenix_bec + 'phoenix_bec/device_configs/phoenix_devices.yaml' # local yamal file + + def read_local_phoenix_config(self): + print('read file ') + print(self.file_phoenix_devices_file) + bec.config.update_session_with_file(self.file_devices_file_local) + + def add_phoenix_config(self): + print('add_phoenix_config ') + print('self.file_devices_file') + bec.config.update_session_with_file(self.file_devices_file) + + def add_xmap(self): + print('add xmap ') + print(self.path_devices+'phoenix_xmap.yaml') + bec.config.update_session_with_file(self.path_devices+'phoenix_xmap.yaml',timeout=50) + + def add_falcon(self): + print('add_xmap') + print(self.path_devices+'/phoenix_falcon.yaml') + bec.config.wait_for_config_reply() + bec.config.update_session_with_file(self.path_devices+'/phoenix_falcon.yaml') + + def show_phoenix_setup(self): + print(self.path_phoenix_bec) + os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') + + + + + + diff --git a/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old_wrongTAB b/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old_wrongTAB new file mode 100644 index 0000000..61f7e6c --- /dev/null +++ b/phoenix_bec/local_scripts/TOBEDELETED/phoenix.py_old_wrongTAB @@ -0,0 +1,104 @@ +#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +from bec_lib.config_helper import ConfigHelper +from bec_lib.logger import bec_logger +logger = bec_logger.logger + +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation +#bec.config.load_demo_config() + +# .. define base path for directory with scripts + + +class PhoenixBL(): + """ + + General class for PHOENIX beamline from phoenix_bec/phoenic_bec/scripts + + """ + + def __init__(self): + + + """ + init PhoenixBL() in phoenix_bec/scripts + + """ + + + import os + + print('..... init PhoenixBL from phoenix_bec/scripts/phoenix.py') + + #from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + #from ophyd import Component as Cpt + #self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + #self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + #self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + #self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + #self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + #self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + # load local configuration + + + + self.path_scripts_local = '/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/phoenix_bec/local_scripts/' + self.path_config_local = self.path_scripts_local + 'TEST_ConfigPhoenix/' # base dir for local configurations + self.path_devices_local = self.path_config_local + 'Local_device_config/' # local yamal file + self.file_devices_file_local = self.path_devices_local + 'phoenix_devices.yaml' + + + self.path_phoenix_bec ='/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/' + self.path_devices = self.path_phoenix_bec + 'phoenix_bec/device_configs/' # local yamal file + self.file_devices_file = self.path_phoenix_bec + 'phoenix_bec/device_configs/phoenix_devices.yaml' # local yamal file + + def read_local_phoenix_config(self): + print('read file ') + print(self.file_phoenix_devices_file) + bec.config.update_session_with_file(self.file_devices_file_local) + + def add_phoenix_config(self): + print('add_phoenix_config ') + print('self.file_devices_file') + bec.config.update_session_with_file(self.file_devices_file) + + def add_xmap(self): + print('add xmap ') + print(self.path_devices+'phoenix_xmap.yaml') + bec.config.update_session_with_file(self.path_devices+'phoenix_xmap.yaml',timeout=50) + + def add_falcon(self): + print('add_xmap') + print(self.path_devices+'/phoenix_falcon.yaml') + bec.config.wait_for_config_reply() + bec.config.update_session_with_file(self.path_devices+'/phoenix_falcon.yaml') + + def show_phoenix_setup(self): + print(self.path_phoenix_bec) + os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') + + + + + + diff --git a/phoenix_bec/local_scripts/p_test.py b/phoenix_bec/local_scripts/p_test.py new file mode 100644 index 0000000..7f27639 --- /dev/null +++ b/phoenix_bec/local_scripts/p_test.py @@ -0,0 +1,2 @@ +print('test') + diff --git a/phoenix_bec/local_scripts/test_phoenix_linescan.py b/phoenix_bec/local_scripts/test_phoenix_linescan.py new file mode 100644 index 0000000..5539901 --- /dev/null +++ b/phoenix_bec/local_scripts/test_phoenix_linescan.py @@ -0,0 +1,93 @@ +#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation + +#bec.config.load_demo_config() + +bec.config.update_session_with_file("config/config_1.yaml") + +os.system('mv *.yaml tmp') + + + +class PhoenixBL: + + #define some epics channels + + def __init__(self): + from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + from ophyd import Component as Cpt + self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + self.fielda =EpicsSignal(name='SMPL',read_pv='X07MB-SCAN:scan1.P1SP',write_pv='X07MB-SCAN:scan1.P1SP') +#end class + +ph=PhoenixBL() + +print('---------------------------------') + +# scan will not diode +print(' SCAN ') +dev.PH_curr_conf.readout_priority='baseline' # do not read detector +dev.PH_curr_conf.readout_priority='monitored' # read detector + + +ti=tt.time_ns() +print('start scan ') +tt.sleep(.2) +s1=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=2,exp_time=1,relative=False,delay=2) +tf=tt.time_ns() +print('elapsed time',(tf-ti)/1e9) + +s1.scan.data + +for thiskey in s1.scan.data.keys(): + print(thiskey) + print(s1.scan.data[thiskey]) + + +#ww=s1.scan.data['Ph_ScanX_conf'] +#print(ww) + +""" +next lines do not work as pandas is not installed on test system + +res1 = s1.scan.to_pandas() +re1 = res1.to_numpy() +print('Scana') +print(res1) +print('') +print('Scan2 at pandas ') +print(res2) +print('Scan2 as numpy ') +print(res2) +""" + + + + + + \ No newline at end of file diff --git a/phoenix_bec/scans/__init__.py b/phoenix_bec/scans/__init__.py index e69de29..2cead0a 100644 --- a/phoenix_bec/scans/__init__.py +++ b/phoenix_bec/scans/__init__.py @@ -0,0 +1 @@ +from .phoenix_line_scan import PhoenixLineScan \ No newline at end of file diff --git a/phoenix_bec/scans/phoenix_line_scan.py b/phoenix_bec/scans/phoenix_line_scan.py new file mode 100644 index 0000000..1ac0ff8 --- /dev/null +++ b/phoenix_bec/scans/phoenix_line_scan.py @@ -0,0 +1,115 @@ +""" + +SCAN PLUGINS for PHOENIX + +All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden +but they are executed in a specific order: + +- self.initialize # initialize the class if needed +- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed) +- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions: + - self._calculate_positions # calculate the positions + - self._set_positions_offset # apply the previously retrieved scan position shift (if needed) + - self._check_limits # tests to ensure the limits won't be reached +- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names +- self.stage # stage all devices for the upcoming acquisiton +- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan +- self.pre_scan # perform additional actions before the scan starts +- self.scan_core # run a loop over all position + - self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments +- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish +- self.unstage # unstage all devices that have been staged before +- self.cleanup # send a close scan message and perform additional cleanups if needed +""" + +# import time + +# import numpy as np + +# from bec_lib.endpoints import MessageEndpoints +# from bec_lib.logger import bec_logger +# from bec_lib import messages +# from bec_server.scan_server.errors import ScanAbortion +# from bec_server.scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase + +# logger = bec_logger.logger + + +from bec_server.scan_server.scans import ScanBase, ScanArgType +import numpy as np +import time +from bec_lib.logger import bec_logger + +logger = bec_logger.logger + +class PhoenixLineScan(ScanBase): + scan_name = "phoenix_line_scanZZZ" + required_kwargs = ["steps", "relative"] + arg_input = { + "device": ScanArgType.DEVICE, + "start": ScanArgType.FLOAT, + "stop": ScanArgType.FLOAT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + gui_config = { + "Movement Parameters": ["steps", "relative"], + "Acquisition Parameters": ["exp_time", "burst_at_each_point"], + } + + def __init__( + self, + *args, + exp_time: float = 0, + steps: int = None, + relative: bool = False, + burst_at_each_point: int = 1, + setup_device:str= None, + **kwargs, + ): + """ + A phoenix line scan for one or more motors. + + Args: + *args (Device, float, float): pairs of device / start position / end position + exp_time (float): exposure time in s. Default: 0 + steps (int): number of steps. Default: 10 + relative (bool): if True, the start and end positions are relative to the current position. Default: False + burst_a Specifies the level of type checking analysis to perform. +ans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.steps = steps + self.setup_device = setup_device + print('INIT CLASS PhoenixLineScan') + time.sleep(1) + + + def _calculate_positions(self) -> None: + axis = [] + for _, val in self.caller_args.items(): + ax_pos = np.linspace(val[0], val[1], self.steps, dtype=float) + axis.append(ax_pos) + self.positions = np.array(list(zip(*axis)), dtype=float) + + def _at_each_point(self, ind=None, pos=None): + yield from self._move_scan_motors_and_wait(pos) + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + time.sleep(self.settling_time) + if self.setup_device: + yield from self.stubs.send_rpc_and_wait(self.setup_device, "velocity.set", 1) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="read", group="scan_motor", wait_group="readout_primary" + ) + + self.point_id += 1 \ No newline at end of file diff --git a/phoenix_bec/scripts/Current_setup.txt b/phoenix_bec/scripts/Current_setup.txt new file mode 100644 index 0000000..f6f27a2 --- /dev/null +++ b/phoenix_bec/scripts/Current_setup.txt @@ -0,0 +1,33 @@ +####################################################### + +Definiton from file local_scripts/Documentation/Current_Setup.txt + +####################################################### + +Current setup for bec --- to be professionanlized + +Description of current setup local_scripts/Documentation/Current_Setup.txt + +/phoenix_bec/phoenix_bec/bec_ipython_client/startup/post_startup.py + .. for commands to start/init bec iphython shell + .. here we init phoenix=PhoenixBL() + +/bec_deployment/phoenix_bec/phoenix_bec/scripts + .. autoloaded scripts directory + .. for solidified scritps + .. file PhoenixBL in phoenix.py defines BL core functions + +/bec_deployment/phoenix_bec/phoenix_bec/devices + .. yamal files for device + +/bec_deployment/phoenix_bec/phoenix_bec/local_scripts + .. collection of local scripts for testing purposes + .. all local configurations start name with LOCAL to minimize confusion + +to run startup file: + +phoenix_bec/bec_ipython_client/startup/post_startup.py + +Magic commands defiend in post_startup.py (should all start with ph_) + +%ph_reload : reloads module phoenix.py to ipython shell BUT not to server \ No newline at end of file diff --git a/phoenix_bec/scripts/README.txt b/phoenix_bec/scripts/README.txt new file mode 100644 index 0000000..2e306a0 --- /dev/null +++ b/phoenix_bec/scripts/README.txt @@ -0,0 +1,2 @@ +Directory for general code specific fro PHOENIX +Seems to be loaded outomatically diff --git a/phoenix_bec/scripts/__init__.py b/phoenix_bec/scripts/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/phoenix_bec/scripts/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phoenix_bec/scripts/phoenix.py b/phoenix_bec/scripts/phoenix.py new file mode 100644 index 0000000..4d3b4f2 --- /dev/null +++ b/phoenix_bec/scripts/phoenix.py @@ -0,0 +1,92 @@ +#from unittest import mock +import os +import sys +import time as tt + +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus + +from bec_lib.config_helper import ConfigHelper +from bec_lib.logger import bec_logger +logger = bec_logger.logger + + +#import ophyd +#logger = bec_logger.logger +# load simulation +#bec.config.load_demo_config() + +# .. define base path for directory with scripts + +class PhoenixBL(): + """ + # + # General class for PHOENIX beamline located in phoenix_bec/phoenic_bec/scripts + # + """ + def __init__(self): + """ + init PhoenixBL() in phoenix_bec/scripts + + """ + + + import os + + print('..... init PhoenixBL from phoenix_bec/scripts/phoenix.py') + + #from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + #from ophyd import Component as Cpt + #self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + #self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + #self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + #self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + #self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + #self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + # load local configuration + + self.path_scripts_local = '/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/phoenix_bec/local_scripts/' + self.path_config_local = self.path_scripts_local + 'TEST_ConfigPhoenix/' # base dir for local configurations + self.path_devices_local = self.path_config_local + 'Local_device_config/' # local yamal file + self.file_devices_file_local = self.path_devices_local + 'phoenix_devices.yaml' + + self.path_phoenix_bec ='/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/' + self.path_devices = self.path_phoenix_bec + 'phoenix_bec/device_configs/' # local yamal file + self.file_devices_file = self.path_phoenix_bec + 'phoenix_bec/device_configs/phoenix_devices.yaml' # local yamal file + + def read_local_phoenix_config(self): + print('read file ') + print(self.file_phoenix_devices_file) + bec.config.update_session_with_file(self.file_devices_file_local) + + def add_phoenix_config(self): + print('add_phoenix_config ') + print('self.file_devices_file') + bec.config.update_session_with_file(self.file_devices_file) + + def add_xmap(self): + print('add xmap ') + print(self.path_devices+'phoenix_xmap.yaml') + + bec.config.update_session_with_file(self.path_devices+'phoenix_xmap.yaml',timeout=100) + + def add_falcon(self): + print('add_xmap') + print(self.path_devices+'/phoenix_falcon.yaml') + bec.config.wait_for_config_reply() + bec.config.update_session_with_file(self.path_devices+'/phoenix_falcon.yaml') + + def show_phoenix_setup(self): + print(self.path_phoenix_bec) + os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') -- 2.49.1 From 7359d1b8f6ca867efc5e157b941ce5bf4cbf1f8f Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Tue, 20 Aug 2024 13:41:43 +0200 Subject: [PATCH 02/14] Change readm in scripts --- phoenix_bec/scripts/README.md | 10 ++++++++++ phoenix_bec/scripts/README.txt | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 phoenix_bec/scripts/README.md delete mode 100644 phoenix_bec/scripts/README.txt diff --git a/phoenix_bec/scripts/README.md b/phoenix_bec/scripts/README.md new file mode 100644 index 0000000..57ffb75 --- /dev/null +++ b/phoenix_bec/scripts/README.md @@ -0,0 +1,10 @@ +## scripts + +Directory for general phoenix specific python code + +to autoload register in __init__.py + + +## FILES + +phoenix.py Base file with general definitions \ No newline at end of file diff --git a/phoenix_bec/scripts/README.txt b/phoenix_bec/scripts/README.txt deleted file mode 100644 index 2e306a0..0000000 --- a/phoenix_bec/scripts/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -Directory for general code specific fro PHOENIX -Seems to be loaded outomatically -- 2.49.1 From 20aaa069de8666b59a68d8849bf01054f6c88fe3 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Wed, 21 Aug 2024 17:52:24 +0200 Subject: [PATCH 03/14] next some base classed copied and commented to local scripts --- phoenix_bec/devices/Phoenix_trigger.py | 243 -- phoenix_bec/devices/phoenix_trigger.py | 182 ++ .../BASE_CLASS_delay_GENERATOR.TXT | 510 ++++ .../Base_Classes/BASE_CLASS_scan_stubs.txt | 570 +++++ .../Base_Classes/BASE_CLASS_scans.txt | 513 ++++ .../Base_Classes/BAse_CLASS_ScanBase.txt | 2061 +++++++++++++++++ .../DefiningEpics_Channels_details.py | 120 + phoenix_bec/local_scripts/README.md | 9 + 8 files changed, 3965 insertions(+), 243 deletions(-) delete mode 100644 phoenix_bec/devices/Phoenix_trigger.py create mode 100644 phoenix_bec/devices/phoenix_trigger.py create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_delay_GENERATOR.TXT create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scan_stubs.txt create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt create mode 100644 phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels_details.py create mode 100644 phoenix_bec/local_scripts/README.md diff --git a/phoenix_bec/devices/Phoenix_trigger.py b/phoenix_bec/devices/Phoenix_trigger.py deleted file mode 100644 index a948ee5..0000000 --- a/phoenix_bec/devices/Phoenix_trigger.py +++ /dev/null @@ -1,243 +0,0 @@ -from ophyd import ( - ADComponent as ADCpt, - Device, - DeviceStatus, -) - -from ophyd import Component as Cpt -from ophyd import Device, EpicsSignal, EpicsSignalRO - -from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin - -from bec_lib import bec_logger, messages -from bec_lib.endpoints import MessageEndpoints - -logger = bec_logger.logger - -DETECTOR_TIMEOUT = 5 - -class PhoenixTriggerError(Exception): - """Base class for exceptions in this module.""" - - -class PhoenixTriggerTimeoutError(XMAPError): - """Raised when the PhoenixTrigger does not respond in time.""" - - -class PhoenixTriggerDetectorState(enum.IntEnum): - """Detector states for XMAP detector""" - - DONE = 0 - ACQUIRING = 1 - - - - -class PhoenixTriggerSetup(CustomDetectorMixin): - """ - This defines the trigger setup. - - - """ - - def __init__(self, *args, parent:Device = None, **kwargs): - super().__init__(*args, parent=parent, **kwargs) - self._counter = 0 - - def on_stage(self): - exposure_time = self.parent.scaninfo.exp_time - num_points = self.parent.scaninfo.num_points - - # camera acquisition parameters - self.parent.cam.array_counter.put(0) - if self.parent.scaninfo.scan_type == 'step': - self.parent.cam.acquire_time.put(exposure_time) - self.parent.cam.num_images.put(1) - self.parent.cam.image_mode.put(0) # Single - self.parent.cam.trigger_mode.put(0) # auto - else: - # In flyscan, the exp_time is the time between two triggers, - # which minus 15% is used as the acquisition time. - self.parent.cam.acquire_time.put(exposure_time * 0.85) - self.parent.cam.num_images.put(num_points) - self.parent.cam.image_mode.put(1) # Multiple - self.parent.cam.trigger_mode.put(1) # trigger - self.parent.cam.acquire.put(1, wait=False) # arm - - # file writer - self.parent.hdf.lazy_open.put(1) - self.parent.hdf.num_capture.put(num_points) - self.parent.hdf.file_write_mode.put(2) # Stream - self.parent.hdf.capture.put(1, wait=False) - self.parent.hdf.enable.put(1) # enable plugin - - # roi statistics to collect signal and background in a timeseries - self.parent.roistat.enable.put(1) - self.parent.roistat.ts_num_points.put(num_points) - self.parent.roistat.ts_control.put(0, wait=False) # Erase/Start - - logger.success('XXXX stage XXXX') - - def on_trigger(self): - self.parent.cam.acquire.put(1, wait=False) - logger.success('XXXX trigger XXXX') - - return self.wait_with_status( - [(self.parent.cam.acquire.get, 0)], - self.parent.scaninfo.exp_time + DETECTOR_TIMEOUT, - all_signals=True - ) - - def on_complete(self): - status = DeviceStatus(self.parent) - - if self.parent.scaninfo.scan_type == 'step': - timeout = DETECTOR_TIMEOUT - else: - timeout = self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points + DETECTOR_TIMEOUT - logger.success('XXXX %s XXXX' % self.parent.roistat.ts_acquiring.get()) - success = self.wait_for_signals( - [ - (self.parent.cam.acquire.get, 0), - (self.parent.hdf.capture.get, 0), - (self.parent.roistat.ts_acquiring.get, 'Done') - ], - timeout, - check_stopped=True, - all_signals=True - ) - - # publish file location - self.parent.filepath.put(self.parent.hdf.full_file_name.get()) - self.publish_file_location(done=True, successful=success) - - # publish timeseries data - metadata = self.parent.scaninfo.scan_msg.metadata - metadata.update({"async_update": "append", "num_lines": self.parent.roistat.ts_current_point.get()}) - msg = messages.DeviceMessage( - signals={ - self.parent.roistat.roi1.name_.get(): { - 'value': self.parent.roistat.roi1.ts_total.get(), - }, - self.parent.roistat.roi2.name_.get(): { - 'value': self.parent.roistat.roi2.ts_total.get(), - }, - }, - metadata=self.parent.scaninfo.scan_msg.metadata - ) - self.parent.connector.xadd( - topic=MessageEndpoints.device_async_readback( - scan_id=self.parent.scaninfo.scan_id, device=self.parent.name - ), - msg_dict={"data": msg}, - expire=1800, - ) - - logger.success('XXXX complete %d XXXX' % success) - if success: - status.set_finished() - else: - status.set_exception(TimeoutError()) - return status - - def on_stop(self): - logger.success('XXXX stop XXXX') - self.parent.cam.acquire.put(0) - self.parent.hdf.capture.put(0) - self.parent.roistat.ts_control.put(2) - - def on_unstage(self): - self.parent.cam.acquire.put(0) - self.parent.hdf.capture.put(0) - self.parent.roistat.ts_control.put(2) - logger.success('XXXX unstage XXXX') - - -class EigerROIStatPlugin(ROIStatPlugin): - roi1 = ADCpt(ROIStatNPlugin, '1:') - roi2 = ADCpt(ROIStatNPlugin, '2:') - -class PhoenixTrigger(PSIDetectorBase): - - """ - Parent class: PSIDetectorBase - - class attributes: - custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, - inherits from CustomDetectorMixin - in __init__ of PSIDetecor bases - class is initialized - self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) - PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector - dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector - mca (EpicsMCARecord) : MCA parameters for XMAP detector - hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector - MIN_READOUT (float) : Minimum readout time for the detector - - - The class PhoenixTrigger is the class to be called via yaml configuration file - the input arguments are defined by PSIDetectorBase, - and need to be given in the yaml configuration file. - To adress chanels such as 'X07MB-OP2:SMPL-DONE': - - use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. - - PSIDetectorBase( - prefix='', - *,Q - name, - kind=None, - parent=None, - device_manager=None, - **kwargs, - ) - Docstring: - Abstract base class for SLS detectors - - Class attributes: - custom_prepare_cls (object): class for custom prepare logic (BL specific) - - Args: - prefix (str): EPICS PV prefix for component (optional) - name (str): name of the device, as will be reported via read() - kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal - omitted -> readout ignored for read 'ophydobj.read()' - normal -> readout for read - config -> config parameter for 'ophydobj.read_configuration()' - hinted -> which attribute is readout for read - parent (object): instance of the parent device - device_manager (object): bec device manager - **kwargs: keyword arguments - File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py - Type: type - Subclasses: SimCamera, SimMonitorAsync - - - - - - """ - #custom_prepare_cls = PhoenixTriggerSetup - - #cam = ADCpt(SLSDetectorCam, 'cam1:') - - - #X07MB-OP2:START-CSMPL.. cont on / off - - # X07MB-OP2:SMPL.. take single sample - #X07MB-OP2:INTR-COUNT.. counter run up - #X07MB-OP2:TOTAL-CYCLES .. cycles set - #X07MB-OP2:SMPL-DONE - - QUESTION HOW does ADCpt kno the EPICS prefix?????? - - #image = ADCpt(ImagePlugin, 'image1:') - #roi1 = ADCpt(ROIPlugin, 'ROI1:') - #roi2 = ADCpt(ROIPlugin, 'ROI2:') - #stats1 = ADCpt(StatsPlugin, 'Stats1:') - #stats2 = ADCpt(StatsPlugin, 'Stats2:') - roistat = ADCpt(EigerROIStatPlugin, 'ROIStat1:') - #roistat1 = ADCpt(ROIStatNPlugin, 'ROIStat1:1:') - #roistat2 = ADCpt(ROIStatNPlugin, 'ROIStat1:2:') - hdf = ADCpt(HDF5Plugin, 'HDF1:') - ) diff --git a/phoenix_bec/devices/phoenix_trigger.py b/phoenix_bec/devices/phoenix_trigger.py new file mode 100644 index 0000000..d457eb6 --- /dev/null +++ b/phoenix_bec/devices/phoenix_trigger.py @@ -0,0 +1,182 @@ +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO + +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +import time + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +#class PhoenixTriggerError(Exce start_csmpl=Cpt(EPicsSignal,'START-CSMPL') # cont on / off + + + + + +class PhoenixTriggerSetup(CustomDetectorMixin): + """ + This defines the PHOENIX trigger setup. + + + """ + + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + WW + def on_stage(self): + # is this called on each point in scan or just before scan ??? + print('on stage') + self.parent.start_csmpl.put(0) + time.sleep(0.05) + cycles=self.parent.total_cycles.get() + time.sleep(0.05) + cycles=self.parent.total_cycles.put(0) + time.sleep(0.05) + cycles=self.parent.smpl.put(2) + time.sleep(0.5) + cycles=self.parent.total_cycles.put(cycles) + + logger.success('PhoenixTrigger on stage') + + def on_trigger(self): + + self.parent.start_smpl.put(1) + time.sleep(0.05) # use blocking + logger.success('PhoenixTrigger on_trigger') + + return self.wait_with_status( + [(self.parent.smpl_done.get, 1)]) + + + + +# logger.success(' PhoenixTrigger on_trigger complete ') + +# if success: +# status.set_finished() +# else: +# status.set_exception(TimeoutError()) +# return status + + + + def on_complete(self): + + timeout =10 + + + logger.success('XXXX complete %d XXXX' % success) + + success = self.wait_for_signals( + [ + (self.parent.smpl_done.get, 0)) + ], + timeout, + check_stopped=True, + all_signals=True + ) + + + + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + + + + def on_stop(self): + logger.success(' PhoenixTrigger on_stop ') + + self.parent.csmpl.put(1) + logger.success(' PhoenixTrigger on_stop finished ') + + def on_unstage(self): + logger.success(' PhoenixTrigger on_unstage ') + self.parent.csmpl.put(1) + self.parent.smpl.put(1) + logger.success(' PhoenixTrigger on_unstage finished ') + + + + + +class PhoenixTrigger(PSIDetectorBase): + + """ + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + in __init__ of PSIDetecor bases + class is initialized + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector + mca (EpicsMCARecord) : MCA parameters for XMAP detector + hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector + MIN_READOUT (float) : Minimum readout time for the detector + + + The class PhoenixTrigger is the class to be called via yaml configuration file + the input arguments are defined by PSIDetectorBase, + and need to be given in the yaml configuration file. + To adress chanels such as 'X07MB-OP2:SMPL-DONE': + + use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. + + PSIDetectorBase( + prefix='', + *,Q + name, + kind=None, + parent=None, + device_manager=None, + **kwargs, + ) + Docstring: + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + parent (object): instance of the parent device + device_manager (object): bec device manager + **kwargs: keyword arguments + File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py + Type: type + Subclasses: EpicsSignal + """ + + custom_prepare_cls = PhoenixTriggerSetup + + + start_csmpl = Cpt(EpicsSignal,'START-CSMPL') # cont on / off + intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up + total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set + smpl_done = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done + diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_delay_GENERATOR.TXT b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_delay_GENERATOR.TXT new file mode 100644 index 0000000..03c76ea --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_delay_GENERATOR.TXT @@ -0,0 +1,510 @@ +import enum +import time +from typing import Any + +from bec_lib import bec_logger +from ophyd import ( + Component, + Device, + DeviceStatus, + EpicsSignal, + EpicsSignalRO, + Kind, + PVPositioner, + Signal, +) +from ophyd.device import Staged +from ophyd.pseudopos import ( + PseudoPositioner, + PseudoSingle, + pseudo_position_argument, + real_position_argument, +) + +from ophyd_devices.utils import bec_utils +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin + +logger = bec_logger.logger + + +class DelayGeneratorError(Exception): + """Exception raised for errors.""" + + +class DeviceInitError(DelayGeneratorError): + """Error upon failed initialization, invoked by missing device manager or device not started in sim_mode.""" + + +class DelayGeneratorNotOkay(DelayGeneratorError): + """Error when DDG is not okay""" + + +class TriggerSource(enum.IntEnum): + """ + Class for trigger options of DG645 + + Used to set the trigger source of the DG645 by setting the value + e.g. source.put(TriggerSource.Internal) + Exp: + TriggerSource.Internal + """ + + 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 DelayStatic(Device): + """ + Static axis for the T0 output channel + + It allows setting the logic levels, but the timing is fixed. + The signal is high after receiving the trigger until the end + of the holdoff period. + """ + + # Other channel stuff + ttl_mode = Component(EpicsSignal, "OutputModeTtlSS.PROC", kind=Kind.config) + nim_mode = Component(EpicsSignal, "OutputModeNimSS.PROC", kind=Kind.config) + polarity = Component( + EpicsSignal, + "OutputPolarityBI", + write_pv="OutputPolarityBO", + name="polarity", + kind=Kind.config, + ) + amplitude = Component( + EpicsSignal, "OutputAmpAI", write_pv="OutputAmpAO", name="amplitude", kind=Kind.config + ) + offset = Component( + EpicsSignal, "OutputOffsetAI", write_pv="OutputOffsetAO", name="offset", kind=Kind.config + ) + + +class DummyPositioner(PVPositioner): + """Dummy Positioner to set AO, AI and ReferenceMO.""" + + 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) + + +class DelayPair(PseudoPositioner): + """ + Delay pair interface + + Virtual motor interface to a pair of signals (on the frontpanel - AB/CD/EF/GH). + It offers a simple delay and pulse width interface. + """ + + # The pseudo positioner axes + delay = Component(PseudoSingle, limits=(0, 2000.0), name="delay") + width = Component(PseudoSingle, limits=(0, 2000.0), name="pulsewidth") + ch1 = Component(DummyPositioner, name="ch1") + ch2 = Component(DummyPositioner, name="ch2") + io = Component(DelayStatic, name="io") + + def __init__(self, *args, **kwargs): + # Change suffix names before connecting (a bit of dynamic connections) + self.__class__.__dict__["ch1"].suffix = kwargs["channel"][0] + self.__class__.__dict__["ch2"].suffix = kwargs["channel"][1] + self.__class__.__dict__["io"].suffix = kwargs["channel"] + + del kwargs["channel"] + # Call parent to start the connections + super().__init__(*args, **kwargs) + + @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) + + @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) + + +class DDGCustomMixin: + """ + Mixin class for custom DelayGenerator logic within PSIDelayGeneratorBase. + + This class provides a parent class for implementation of BL specific logic of the device. + It is also possible to pass implementing certain methods, e.g. finished or on_trigger, + based on the setup and desired operation mode at the beamline. + + Args: + parent (object): instance of PSIDelayGeneratorBase + **kwargs: keyword arguments + """ + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + self.parent = parent + + def initialize_default_parameter(self) -> None: + """ + Method to initialize default parameters for DDG. + + Called upon initiating the base class. + It should be used to set the DDG default parameters. + These may include: amplitude, offsets, delays, etc. + """ + + def prepare_ddg(self) -> None: + """ + Method to prepare the DDG for the upcoming scan. + + Called by the stage method of the base class. + It should be used to set the DDG parameters for the upcoming scan. + """ + + def on_trigger(self) -> None: + """Method executed upon trigger call in parent class""" + + def finished(self) -> None: + """Method to check if DDG is finished with the scan""" + + def on_pre_scan(self) -> None: + """ + Method executed upon pre_scan call in parent class. + + Covenient to implement time sensitive actions to be executed right before start of the scan. + Example could be to open the shutter by triggering a pulse via pre_scan. + """ + + def check_scan_id(self) -> None: + """Method to check if there is a new scan_id, called by stage.""" + + def is_ddg_okay(self, raise_on_error=False) -> None: + """ + Method to check if DDG is okay + + It checks the status PV of the DDG and tries to clear the error if it is not okay. + It will rerun itself and raise DelayGeneratorNotOkay if DDG is still not okay. + + Args: + raise_on_error (bool, optional): raise exception if DDG is not okay. Defaults to False. + """ + status = self.parent.status.read()[self.parent.status.name]["value"] + if status != "STATUS OK" and not raise_on_error: + logger.warning(f"DDG returns {status}, trying to clear ERROR") + self.parent.clear_error() + time.sleep(1) + self.is_ddg_okay(raise_on_error=True) + elif status != "STATUS OK": + raise DelayGeneratorNotOkay(f"DDG failed to start with status: {status}") + + +class PSIDelayGeneratorBase(Device): + """ + Abstract base class for DelayGenerator DG645 + + This class implements a thin Ophyd wrapper around the Stanford Research DG645 + digital delay generator. + + The DG645 generates 8+1 signals: A, B, C, D, E, F, G, H and T0. Front panel outputs + T0, AB, CD, EF and GH are combinations of these signals. Back panel outputs are + directly routed signals. Signals are not independent. + + Signal pairs, e.g. AB, CD, EF, GH, are implemented as DelayPair objects. They + have a TTL pulse width, delay and a reference signal to which they are being triggered. + In addition, the io layer allows setting amplitude, offset and polarity for each pair. + + Detailed information can be found in the manual: + https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str) : EPICS PV prefix for component (optional) + name (str) : name of the device, as will be reported via read() + kind (str) : member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + read_attrs (list) : sequence of attribute names to read + configuration_attrs (list) : sequence of attribute names via config_parameters + parent (object) : instance of the parent device + device_manager (object) : bec device manager + sim_mode (bool) : simulation mode, if True, no device manager is required + **kwargs : keyword arguments + attributes : lazy_wait_for_connection : bool + """ + + # Custom_prepare_cls + custom_prepare_cls = DDGCustomMixin + + SUB_PROGRESS = "progress" + SUB_VALUE = "value" + _default_sub = SUB_VALUE + + USER_ACCESS = ["set_channels", "_set_trigger", "burst_enable", "burst_disable", "reload_config"] + + # Assign PVs from DDG645 + 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") + + # Front Panel + channelT0 = Component(DelayStatic, "T0", name="T0") + channelAB = Component(DelayPair, "", name="AB", channel="AB") + channelCD = Component(DelayPair, "", name="CD", channel="CD") + channelEF = Component(DelayPair, "", name="EF", channel="EF") + channelGH = Component(DelayPair, "", name="GH", channel="GH") + + holdoff = Component( + EpicsSignal, + "TriggerHoldoffAI", + write_pv="TriggerHoldoffAO", + name="trigger_holdoff", + kind=Kind.config, + ) + inhibit = Component( + EpicsSignal, + "TriggerInhibitMI", + write_pv="TriggerInhibitMO", + name="trigger_inhibit", + kind=Kind.config, + ) + source = Component( + EpicsSignal, + "TriggerSourceMI", + write_pv="TriggerSourceMO", + name="trigger_source", + kind=Kind.config, + ) + level = Component( + EpicsSignal, + "TriggerLevelAI", + write_pv="TriggerLevelAO", + name="trigger_level", + kind=Kind.config, + ) + rate = Component( + EpicsSignal, + "TriggerRateAI", + write_pv="TriggerRateAO", + name="trigger_rate", + kind=Kind.config, + ) + trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config") + burstMode = Component( + EpicsSignal, "BurstModeBI", write_pv="BurstModeBO", name="burstmode", kind=Kind.config + ) + burstConfig = Component( + EpicsSignal, "BurstConfigBI", write_pv="BurstConfigBO", name="burstconfig", kind=Kind.config + ) + burstCount = Component( + EpicsSignal, "BurstCountLI", write_pv="BurstCountLO", name="burstcount", kind=Kind.config + ) + burstDelay = Component( + EpicsSignal, "BurstDelayAI", write_pv="BurstDelayAO", name="burstdelay", kind=Kind.config + ) + burstPeriod = Component( + EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config + ) + + 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 DeviceInitError( + f"No device manager for device: {name}, and not started sim_mode: {sim_mode}. Add" + " DeviceManager to initialization or init with sim_mode=True" + ) + # Init variables + self.sim_mode = sim_mode + self.stopped = False + self.name = name + self.scaninfo = None + self.timeout = 5 + self.all_channels = ["channelT0", "channelAB", "channelCD", "channelEF", "channelGH"] + self.all_delay_pairs = ["AB", "CD", "EF", "GH"] + self.wait_for_connection(all_signals=True) + + # Init custom prepare class with BL specific logic + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + if not sim_mode: + self.device_manager = device_manager + else: + self.device_manager = bec_utils.DMMock() + self.connector = self.device_manager.connector + self._update_scaninfo() + self._init() + + def _update_scaninfo(self) -> None: + """ + Method to updated scaninfo from BEC. + + In sim_mode, scaninfo output is mocked - see bec_scaninfo_mixin.py + """ + self.scaninfo = BecScaninfoMixin(self.device_manager, self.sim_mode) + self.scaninfo.load_scan_metadata() + + def _init(self) -> None: + """Method to initialize custom parameters of the DDG.""" + self.custom_prepare.initialize_default_parameter() + self.custom_prepare.is_ddg_okay() + + def set_channels(self, signal: str, value: Any, channels: list = None) -> None: + """ + Method to set signals on DelayPair and DelayStatic channels. + + Signals can be set on the DelayPair and DelayStatic channels. The method checks + if the signal is available on the channel and sets it. It works for both, DelayPair + and Delay Static although signals are hosted in different layers. + + Args: + signal (str) : signal to set (width, delay, amplitude, offset, polarity) + value (Any) : value to set + channels (list, optional) : list of channels to set. Defaults to self.all_channels (T0,AB,CD,EF,GH) + """ + 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 set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source on DDG - possible values defined in TriggerSource enum""" + value = int(trigger_source) + self.source.put(value) + + 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 burst configs are 'all' and 'first'" + + self.burstMode.put(1) + self.burstCount.put(count) + self.burstDelay.put(delay) + self.burstPeriod.put(period) + + if config == "all": + self.burstConfig.put(0) + elif config == "first": + self.burstConfig.put(1) + + def burst_disable(self): + """Disable burst mode""" + self.burstMode.put(0) + + def stage(self) -> list[object]: + """ + Method to stage the device. + + Called in preparation for a scan. + + Internal Calls: + - scaninfo.load_scan_metadata : load scan metadata + - custom_prepare.prepare_ddg : prepare DDG for measurement + - is_ddg_okay : check if DDG is okay + + Returns: + list(object): list of objects that were staged + """ + if self._staged != Staged.no: + return super().stage() + self.stopped = False + self.scaninfo.load_scan_metadata() + self.custom_prepare.prepare_ddg() + self.custom_prepare.is_ddg_okay() + # At the moment needed bc signal might not be reliable, BEC too fast. + # Consider removing this overhead in future! + time.sleep(0.05) + return super().stage() + + def trigger(self) -> DeviceStatus: + """ + Method to trigger the acquisition. + + Internal Call: + - custom_prepare.on_trigger : execute BL specific action + """ + self.custom_prepare.on_trigger() + return super().trigger() + + def pre_scan(self) -> None: + """ + Method pre_scan gets executed directly before the scan + + Internal Call: + - custom_prepare.on_pre_scan : execute BL specific action + """ + self.custom_prepare.on_pre_scan() + + def unstage(self) -> list[object]: + """ + Method unstage gets called at the end of a scan. + + If scan (self.stopped is True) is stopped, returns directly. + Otherwise, checks if the DDG finished acquisition + + Internal Calls: + - custom_prepare.check_scan_id : check if scan_id changed or detector stopped + - custom_prepare.finished : check if device finished acquisition (succesfully) + - is_ddg_okay : check if DDG is okay + + Returns: + list(object): list of objects that were unstaged + """ + self.custom_prepare.check_scan_id() + if self.stopped is True: + return super().unstage() + self.custom_prepare.finished() + self.custom_prepare.is_ddg_okay() + self.stopped = False + return super().unstage() + + def stop(self, *, success=False) -> None: + """ + Method to stop the DDG + + #TODO Check if the pulse generation can be interruppted + + Internal Call: + - custom_prepare.is_ddg_okay : check if DDG is okay + """ + self.custom_prepare.is_ddg_okay() + super().stop(success=success) + self.stopped = True diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scan_stubs.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scan_stubs.txt new file mode 100644 index 0000000..f672ae6 --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scan_stubs.txt @@ -0,0 +1,570 @@ +""" +Scan stubs are commands that can be used to control devices during a scan. They typically yield device messages that are +consumed by the scan worker and potentially forwarded to the device server. +""" + +from __future__ import annotations + +import threading +import time +import uuid +from collections.abc import Callable +from typing import Generator, Literal + +import numpy as np + +from bec_lib import messages +from bec_lib.connector import ConnectorBase +from bec_lib.device import Status +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger + +from .errors import DeviceMessageError, ScanAbortion + +logger = bec_logger.logger + + +class ScanStubs: + """ + Scan stubs are commands that can be used to control devices during a scan. They typically yield device messages that are + consumed by the scan worker and potentially forwarded to the device server. + """ + + def __init__( + self, + connector: ConnectorBase, + device_msg_callback: Callable = None, + shutdown_event: threading.Event = None, + ) -> None: + self.connector = connector + self.device_msg_metadata = ( + device_msg_callback if device_msg_callback is not None else lambda: {} + ) + self.shutdown_event = shutdown_event + + @staticmethod + def _exclude_nones(input_dict: dict): + for key in list(input_dict.keys()): + if input_dict[key] is None: + input_dict.pop(key) + + def _device_msg(self, **kwargs): + """""" + msg = messages.DeviceInstructionMessage(**kwargs) + msg.metadata = {**self.device_msg_metadata(), **msg.metadata} + return msg + + def send_rpc_and_wait(self, device: str, func_name: str, *args, **kwargs) -> any: + """Perform an RPC (remote procedure call) on a device and wait for its return value. + + Args: + device (str): Name of the device + func_name (str): Function name. The function name will be appended to the device. + args (tuple): Arguments to pass on to the RPC function + kwargs (dict): Keyword arguments to pass on to the RPC function + + Raises: + ScanAbortion: Raised if the RPC's success is False + + Returns: + any: Return value of the executed rpc function + + Examples: + >>> send_rpc_and_wait("samx", "controller.my_custom_function") + """ + rpc_id = str(uuid.uuid4()) + parameter = { + "device": device, + "func": func_name, + "rpc_id": rpc_id, + "args": args, + "kwargs": kwargs, + } + yield from self.rpc( + device=device, parameter=parameter, metadata={"response": True, "RID": rpc_id} + ) + return self._get_from_rpc(rpc_id) + + def _get_from_rpc(self, rpc_id) -> any: + """ + Get the return value from an RPC call. + + Args: + rpc_id (str): RPC ID + + Raises: + ScanAbortion: Raised if the RPC's success flag is False + + Returns: + any: Return value of the RPC call + """ + + while not self.shutdown_event.is_set(): + msg = self.connector.get(MessageEndpoints.device_rpc(rpc_id)) + if msg: + break + time.sleep(0.001) + if self.shutdown_event.is_set(): + raise ScanAbortion("The scan was aborted.") + if not msg.content["success"]: + error = msg.content["out"] + if isinstance(error, dict) and {"error", "msg", "traceback"}.issubset( + set(error.keys()) + ): + error_msg = f"During an RPC, the following error occured:\n{error['error']}: {error['msg']}.\nTraceback: {error['traceback']}\n The scan will be aborted." + else: + error_msg = "During an RPC, an error occured" + raise ScanAbortion(error_msg) + + logger.debug(msg.content.get("out")) + return_val = msg.content.get("return_val") + if not isinstance(return_val, dict): + return return_val + if return_val.get("type") == "status" and return_val.get("RID"): + return Status(self.connector, return_val.get("RID")) + return return_val + + def set_with_response( + self, *, device: str, value: float, request_id: str = None, metadata=None + ) -> Generator[None, None, None]: + """Set a device to a specific value and return the request ID. Use :func:`request_is_completed` to later check if the request is completed. + + Args: + device (str): Device name. + value (float): Target value. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`request_is_completed` + + """ + request_id = str(uuid.uuid4()) if request_id is None else request_id + metadata = metadata if metadata is not None else {} + metadata.update({"response": True, "RID": request_id}) + yield from self.set(device=device, value=value, wait_group="set", metadata=metadata) + return request_id + + def request_is_completed(self, RID: str) -> bool: + """Check if a request that was initiated with :func:`set_with_response` is completed. + + Args: + RID (str): Request ID. + + Returns: + bool: True if the request is completed, False otherwise. + + """ + msg = self.connector.lrange(MessageEndpoints.device_req_status_container(RID), 0, -1) + if not msg: + return False + return True + + def set_and_wait( + self, *, device: list[str], positions: list | np.ndarray + ) -> Generator[None, None, None]: + """Set devices to a specific position and wait completion. + + Args: + device (list[str]): List of device names. + positions (list | np.ndarray): Target position. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`set`, :func:`wait`, :func:`set_with_response` + + """ + if not isinstance(positions, list) and not isinstance(positions, np.ndarray): + positions = [positions] + if len(positions) == 0: + return + for ind, val in enumerate(device): + yield from self.set(device=val, value=positions[ind], wait_group="scan_motor") + yield from self.wait(device=device, wait_type="move", wait_group="scan_motor") + + def read_and_wait( + self, *, wait_group: str, device: list = None, group: str = None, point_id: int = None + ) -> Generator[None, None, None]: + """Trigger a reading and wait for completion. + + Args: + wait_group (str): wait group + device (list, optional): List of device names. Can be specified instead of group. Defaults to None. + group (str, optional): Group name of devices. Can be specified instead of device. Defaults to None. + point_id (int, optional): _description_. Defaults to None. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + """ + self._check_device_and_groups(device, group) + yield from self.read(device=device, group=group, wait_group=wait_group, point_id=point_id) + yield from self.wait(device=device, wait_type="read", group=group, wait_group=wait_group) + + def open_scan( + self, + *, + scan_motors: list, + readout_priority: dict, + num_pos: int, + scan_name: str, + scan_type: Literal["step", "fly"], + positions=None, + metadata=None, + ) -> Generator[None, None, None]: + """Open a new scan. + + Args: + scan_motors (list): List of scan motors. + readout_priority (dict): Modification of the readout priority. + num_pos (int): Number of positions within the scope of this scan. + positions (list): List of positions for this scan. + scan_name (str): Scan name. + scan_type (str): Scan type (e.g. 'step' or 'fly') + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + """ + yield self._device_msg( + device=None, + action="open_scan", + parameter={ + "scan_motors": scan_motors, + "readout_priority": readout_priority, + "num_points": num_pos, + "positions": positions, + "scan_name": scan_name, + "scan_type": scan_type, + }, + metadata=metadata, + ) + + def kickoff( + self, *, device: str, parameter: dict = None, wait_group="kickoff", metadata=None + ) -> Generator[None, None, None]: + """Kickoff a fly scan device. + + Args: + device (str): Device name of flyer. + parameter (dict, optional): Additional parameters that should be forwarded to the device. Defaults to {}. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + parameter = parameter if parameter is not None else {} + parameter = {"configure": parameter, "wait_group": wait_group} + yield self._device_msg( + device=device, action="kickoff", parameter=parameter, metadata=metadata + ) + + def complete(self, *, device: str, metadata=None) -> Generator[None, None, None]: + """Complete a fly scan device. + + Args: + device (str): Device name of flyer. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg(device=device, action="complete", parameter={}, metadata=metadata) + + def get_req_status(self, device: str, RID: str, DIID: int) -> int: + """Check if a device request status matches the given RID and DIID + + Args: + device (str): device under inspection + RID (str): request ID + DIID (int): device instruction ID + + Returns: + int: 1 if the request status matches the RID and DIID, 0 otherwise + """ + msg = self.connector.get(MessageEndpoints.device_req_status(device)) + if not msg: + return 0 + matching_RID = msg.metadata.get("RID") == RID + matching_DIID = msg.metadata.get("DIID") == DIID + if matching_DIID and matching_RID: + return 1 + return 0 + + def get_device_progress(self, device: str, RID: str) -> float | None: + """Get reported device progress + + Args: + device (str): Name of the device + RID (str): request ID + + Returns: + float: reported progress value + + """ + msg = self.connector.get(MessageEndpoints.device_progress(device)) + if not msg: + return None + matching_RID = msg.metadata.get("RID") == RID + if not matching_RID: + return None + if not isinstance(msg, messages.ProgressMessage): + raise DeviceMessageError( + f"Expected to receive a Progressmessage for device {device} but instead received {msg}." + ) + return msg.content["value"] + + def close_scan(self) -> Generator[None, None, None]: + """ + Close the scan. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`open_scan` + """ + + yield self._device_msg(device=None, action="close_scan", parameter={}) + + def stage(self) -> Generator[None, None, None]: + """ + Stage all devices + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`unstage` + """ + yield self._device_msg(device=None, action="stage", parameter={}) + + def unstage(self) -> Generator[None, None, None]: + """ + Unstage all devices + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`stage` + """ + yield self._device_msg(device=None, action="unstage", parameter={}) + + def pre_scan(self) -> Generator[None, None, None]: + """ + Trigger pre-scan actions on all devices. Typically, pre-scan actions are called directly before the scan core starts and + are used to perform time-critical actions. + The event will be sent to all devices that have a pre_scan method implemented. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg(device=None, action="pre_scan", parameter={}) + + def baseline_reading(self) -> Generator[None, None, None]: + """ + Run the baseline readings. This will readout all devices that are marked with the readout_priority "baseline". + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + """ + yield self._device_msg( + device=None, + action="baseline_reading", + parameter={}, + metadata={"readout_priority": "baseline"}, + ) + + def wait( + self, + *, + wait_type: Literal["move", "read", "trigger"], + device: list[str] | str | None = None, + group: Literal["scan_motor", "primary", None] = None, + wait_group: str = None, + wait_time: float = None, + ): + """Wait for an event. + + Args: + wait_type (Literal["move", "read", "trigger"]): Type of wait event. Can be "move", "read" or "trigger". + device (list[str] | str, optional): List of device names. Defaults to None. + group (Literal["scan_motor", "primary", None]): Device group that can be used instead of device. Defaults to None. + wait_group (str, optional): Wait group for this event. Defaults to None. + wait_time (float, optional): Wait time (for wait_type="trigger"). Defaults to None. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + Example: + >>> yield from self.stubs.wait(wait_type="move", group="scan_motor", wait_group="scan_motor") + >>> yield from self.stubs.wait(wait_type="read", group="scan_motor", wait_group="my_readout_motors") + + """ + self._check_device_and_groups(device, group) + parameter = {"type": wait_type, "time": wait_time, "group": group, "wait_group": wait_group} + self._exclude_nones(parameter) + yield self._device_msg(device=device, action="wait", parameter=parameter) + + def read( + self, + *, + wait_group: str, + device: list[str] | str | None = None, + point_id: int | None = None, + group: Literal["scan_motor", "primary", None] = None, + ) -> Generator[None, None, None]: + """ + Trigger a reading on a device or device group. + + Args: + wait_group (str): Wait group for this event. The specified wait group can later be used + to wait for the completion of this event. Please note that the wait group has to be + unique. within the scope of the read / wait event. + device (list, optional): Device name. Can be used instead of group. Defaults to None. + point_id (int, optional): point_id to assign this reading to point within the scan. Defaults to None. + group (Literal["scan_motor", "primary", None], optional): Device group. Can be used instead of device. Defaults to None. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + Example: + >>> yield from self.stubs.read(wait_group="readout_primary", group="primary", point_id=self.point_id) + >>> yield from self.stubs.read(wait_group="sample_stage", device="samx", point_id=self.point_id) + + """ + self._check_device_and_groups(device, group) + parameter = {"group": group, "wait_group": wait_group} + metadata = {"point_id": point_id} + self._exclude_nones(parameter) + self._exclude_nones(metadata) + yield self._device_msg(device=device, action="read", parameter=parameter, metadata=metadata) + + def publish_data_as_read( + self, *, device: str, data: dict, point_id: int + ) -> Generator[None, None, None]: + """ + Publish the given data as a read event and assign it to the given point_id. + This method can be used to customize the assignment of data to a specific point within a scan. + + Args: + device (str): Device name. + data (dict): Data that should be published. + point_id (int): point_id that should be attached to this data. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + metadata = {"point_id": point_id} + yield self._device_msg( + device=device, + action="publish_data_as_read", + parameter={"data": data}, + metadata=metadata, + ) + + def trigger(self, *, group: str, point_id: int) -> Generator[None, None, None]: + """Trigger a device group. Note that the trigger event is not blocking and does not wait for the completion of the trigger event. + To wait for the completion of the trigger event, use the :func:`wait` command, specifying the wait_type as "trigger". + + Args: + group (str): Device group that should receive the trigger. + point_id (int): point_id that should be attached to this trigger event. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + see also: :func:`wait` + """ + yield self._device_msg( + device=None, + action="trigger", + parameter={"group": group}, + metadata={"point_id": point_id}, + ) + + def set(self, *, device: str, value: float, wait_group: str, metadata=None): + """Set the device to a specific value. This is similar to the direct set command + in the command-line interface. The wait_group can be used to wait for the completion of this event. + For a set operation, this simply means that the device has acknowledged the set command and does not + necessarily mean that the device has reached the target value. + + Args: + device (str): Device name + value (float): Target value. + wait_group (str): wait group for this event. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + .. warning:: + + Do not use this command to kickoff a long running operation. Use :func:`kickoff` instead or, if the + device does not support the kickoff command, use :func:`set_with_response` instead. + + see also: :func:`wait`, :func:`set_and_wait`, :func:`set_with_response` + + """ + yield self._device_msg( + device=device, + action="set", + parameter={"value": value, "wait_group": wait_group}, + metadata=metadata, + ) + + def open_scan_def(self) -> Generator[None, None, None]: + """ + Open a new scan definition + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg(device=None, action="open_scan_def", parameter={}) + + def close_scan_def(self) -> Generator[None, None, None]: + """ + Close a scan definition + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg(device=None, action="close_scan_def", parameter={}) + + def close_scan_group(self) -> Generator[None, None, None]: + """ + Close a scan group + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg(device=None, action="close_scan_group", parameter={}) + + def rpc(self, *, device: str, parameter: dict, metadata=None) -> Generator[None, None, None]: + """Perfrom an RPC (remote procedure call) on a device. + + Args: + device (str): Device name. + parameter (dict): parameters used for this rpc instructions. + + Returns: + Generator[None, None, None]: Generator that yields a device message. + + """ + yield self._device_msg(device=device, action="rpc", parameter=parameter, metadata=metadata) + + def scan_report_instruction(self, instructions: dict) -> Generator[None, None, None]: + """Scan report instructions + + Args: + instructions (dict): Dict containing the scan report instructions + + Returns: + Generator[None, None, None]: Generator that yields a device message. + """ + yield self._device_msg( + device=None, action="scan_report_instruction", parameter=instructions + ) + + def _check_device_and_groups(self, device, group) -> None: + if device and group: + raise DeviceMessageError("Device and device group was specified. Pick one.") + if device is None and group is None: + raise DeviceMessageError("Either devices or device groups have to be specified.") diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt new file mode 100644 index 0000000..177b228 --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt @@ -0,0 +1,513 @@ +""" +This module contains the Scans class and related classes for defining and running scans in BEC +from the client side. +""" + +from __future__ import annotations + +import builtins +import uuid +from collections.abc import Callable +from contextlib import ContextDecorator +from copy import deepcopy +from typing import TYPE_CHECKING, Dict, Literal + +from toolz import partition +from typeguard import typechecked + +from bec_lib import messages +from bec_lib.bec_errors import ScanAbortion +from bec_lib.client import SystemConfig +from bec_lib.device import DeviceBase +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger +from bec_lib.scan_report import ScanReport +from bec_lib.signature_serializer import dict_to_signature +from bec_lib.utils import scan_to_csv + +if TYPE_CHECKING: + from bec_lib.client import BECClient + from bec_lib.connector import ConsumerConnector + +logger = bec_logger.logger + + +class ScanObject: + """ScanObject is a class for scans""" + + def __init__(self, scan_name: str, scan_info: dict, client: BECClient = None) -> None: + self.scan_name = scan_name + self.scan_info = scan_info + self.client = client + + # run must be an anonymous function to allow for multiple doc strings + # pylint: disable=unnecessary-lambda + self.run = lambda *args, **kwargs: self._run(*args, **kwargs) + + def _run( + self, + *args, + callback: Callable = None, + async_callback: Callable = None, + hide_report: bool = False, + metadata: dict = None, + monitored: list[str | DeviceBase] = None, + file_suffix: str = None, + file_directory: str = None, + **kwargs, + ) -> ScanReport: + """ + Run the request with the given arguments. + + Args: + *args: Arguments for the scan + callback: Callback function + async_callback: Asynchronous callback function + hide_report: Hide the report + metadata: Metadata dictionary + monitored: List of monitored devices + **kwargs: Keyword arguments + + Returns: + ScanReport + """ + if self.client.alarm_handler.alarms_stack: + logger.info("The alarm stack is not empty but will be cleared now.") + self.client.clear_all_alarms() + scans = self.client.scans + + # pylint: disable=protected-access + hide_report = hide_report or scans._hide_report + + user_metadata = deepcopy(self.client.metadata) + + sys_config = self.client.system_config.model_copy(deep=True) + if file_suffix: + sys_config.file_suffix = file_suffix + if file_directory: + sys_config.file_directory = file_directory + + if "sample_name" not in user_metadata: + var = self.client.get_globa file_suffix: str = None, +l_var("sample_name") + if var is not None: + user_metadata["sample_name"] = var + + if metadata is not None: + user_metadata.update(metadata) + + if monitored is not None: + if not isinstance(monitored, list): + monitored = [monitored] + for mon_device in monitored: + if isinstance(mon_device, str): + mon_device = self.client.device_manager.devices.get(mon_device) + if not mon_device: + raise RuntimeError( + f"Specified monitored device {mon_device} does not exist in the current device configuration." + ) + kwargs["monitored"] = monitored + + sys_config = sys_config.model_dump() + # pylint: disable=protected-access + if scans._scan_group: + sys_config["queue_group"] = scans._scan_group + if scans._scan_def_id: + sys_config["scan_def_id"] = scans._scan_def_id + if scans._dataset_id_on_hold: + sys_config["dataset_id_on_hold"] = scans._dataset_id_on_hold + + kwargs["user_metadata"] = user_metadata + kwargs["system_config"] = sys_config + + request = Scans.prepare_scan_request(self.scan_name, self.scan_info, *args, **kwargs) + request_id = str(uuid.uuid4()) + + # pylint: disable=unsupported-assignment-operation + request.metadata["RID"] = request_id + + self._send_scan_request(request) + + report = ScanReport.from_request(request, client=self.client) + report.request.callbacks.register_many("scan_segment", callback, sync=True) + report.request.callbacks.register_many("scan_segment", async_callback, sync=False) + + if scans._scan_export and scans._scan_export.scans is not None: + scans._scan_export.scans.append(report) + + if not hide_report and self.client.live_updates: + self.client.live_updates.process_request(request, callback) + + self.client.callbacks.poll() + + return report + + def _start_register(self, request: messages.ScanQueueMessage) -> ConsumerConnector: + """Start a register for the given request""" + register = self.client.device_manager.connector.register( + [ + MessageEndpoints.device_readback(dev) + for dev in request.content["parameter"]["args"].keys() + ], + threaded=False, + cb=(lambda msg: msg), + ) + return register + + def _send_scan_request(self, request: messages.ScanQueueMessage) -> None: + """Send a scan request to the scan server""" + self.client.device_manager.connector.send(MessageEndpoints.scan_queue_request(), request) + + +class Scans: + """Scans is a class for available scans in BEC""" + + def __init__(self, parent): + self.parent = parent + self._available_scans = {} + self._import_scans() + self._scan_group = None + self._scan_def_id = None + self._scan_group_ctx = ScanGroup(parent=self) + self._scan_def_ctx = ScanDef(parent=self) + self._hide_report = None + self._hide_report_ctx = HideReport(parent=self) + self._dataset_id_on_hold = None + self._dataset_id_on_hold_ctx = DatasetIdOnHold(parent=self) + self._scan_export = None + + def _import_scans(self): + """Import scans from the scan server""" + available_scans = self.parent.connector.get(MessageEndpoints.available_scans()) + if available_scans is None: + logger.warning("No scans available. Are redis and the BEC server running?") + return + for scan_name, scan_info in available_scans.resource.items(): + self._available_scans[scan_name] = ScanObject(scan_name, scan_info, client=self.parent) + setattr(self, scan_name, self._available_scans[scan_name].run) + setattr(getattr(self, scan_name), "__doc__", scan_info.get("doc")) + setattr( + getattr(self, scan_name), + "__signature__", + dict_to_signature(scan_info.get("signature")), + ) + + @staticmethod + def get_arg_type(in_type: str): + """translate type string into python type""" + # pylint: disable=too-many-return-statements + if in_type == "float": + return (float, int) + if in_type == "int": + return int + if in_type == "list": + return list + if in_type == "boolean": + return bool + if in_type == "str": + return str + if in_type == "dict": + return dict + if in_type == "device": + return DeviceBase + raise TypeError(f"Unknown type {in_type}") + + @staticmethod + def prepare_scan_request( + scan_name: str, scan_info: dict, *args, **kwargs + ) -> messages.ScanQueueMessage: + """Prepare scan request message with given scan arguments + + Args: + scan_name (str): scan name (matching a scan name on the scan server) + scan_info (dict): dictionary describing the scan (e.g. doc string, required kwargs etc.) + + Raises: + TypeError: Raised if not all required keyword arguments have been specified. + TypeError: Raised if the number of args do fit into the required bundling pattern. + TypeError: Raised if an argument is not of the required type as specified in scan_info. + + Returns: + messages.ScanQueueMessage: scan request message + """ + arg_input = list(scan_info.get("arg_input", {}).values()) + + arg_bundle_size = scan_info.get("arg_bundle_size", {}) + bundle_size = arg_bundle_size.get("bundle") + if len(arg_input) > 0: + if len(args) % len(arg_input) != 0: + raise TypeError( + f"{scan_info.get('doc')}\n {scan_name} takes multiples of" + f" {len(arg_input)} arguments ({len(args)} given)." + ) + if not all(req_kwarg in kwargs for req_kwarg in scan_info.get("required_kwargs")): + raise TypeError( + f"{scan_info.get('doc')}\n Not all required keyword arguments have been" + f" specified. The required arguments are: {scan_info.get('required_kwargs')}" + ) + # check that all specified devices in args are different objects + for arg in args: + if not isinstance(arg, DeviceBase): + continue + if args.count(arg) > 1: + raise TypeError( + f"{scan_info.get('doc')}\n All specified devices must be different" + f" objects." + ) + + # check that all arguments are of the correct type + for ii, arg in enumerate(args): + if not isinstance(arg, Scans.get_arg_type(arg_input[ii % len(arg_input)])): + raise TypeError( + f"{scan_info.get('doc')}\n Argument {ii} must be of type" + f" {arg_input[ii%len(arg_input)]}, not {type(arg).__name__}." + ) + + metadata = {} + metadata.update(kwargs["system_config"]) + metadata["user_metadata"] = kwargs.pop("user_metadata", {}) + + params = {"args": Scans._parameter_bundler(args, bundle_size), "kwargs": kwargs} + # check the number of arg bundles against the number of required bundles + if bundle_size: + num_bundles = len(params["args"]) + min_bundles = arg_bundle_size.get("min") + max_bundles = arg_bundle_size.get("max") + if min_bundles and num_bundles < min_bundles: + raise TypeError( + f"{scan_info.get('doc')}\n {scan_name} requires at least {min_bundles} bundles" + f" of arguments ({num_bundles} given)." + ) + if max_bundles and num_bundles > max_bundles: + raise TypeError( + f"{scan_info.get('doc')}\n {scan_name} requires at most {max_bundles} bundles" + f" of arguments ({num_bundles} given)." + ) + return messages.ScanQueueMessage( + scan_type=scan_name, parameter=params, queue="primary", metadata=metadata + ) + + @staticmethod + def _parameter_bundler(args, bundle_size): + """ + + Args: + args: + bundle_size: number of parameters per bundle + + Returns: + + """ + if not bundle_size: + return tuple(cmd.name if hasattr(cmd, "name") else cmd for cmd in args) + params = {} + for cmds in partition(bundle_size, args): + cmds_serialized = [cmd.name if hasattr(cmd, "name") else cmd for cmd in cmds] + params[cmds_serialized[0]] = cmds_serialized[1:] + return params + + @property + def scan_group(self): + """Context manager / decorator for defining scan groups""" + return self._scan_group_ctx + + @property + def scan_def(self): + """Context manager / decorator for defining new scans""" + return self._scan_def_ctx + + @property + def hide_report(self): + """Context manager / decorator for hiding the report""" + return self._hide_report_ctx + + @property + def dataset_id_on_hold(self): + """Context manager / decorator for setting the dataset id on hold""" + return self._dataset_id_on_hold_ctx + + def scan_export(self, output_file: str): + """Context manager / decorator for exporting scans""" + return ScanExport(output_file) + + +class ScanGroup(ContextDecorator): + """ScanGroup is a ContextDecorator for defining a scan group""" + + def __init__(self, parent: Scans = None) -> None: + super().__init__() + self.parent = parent + + def __enter__(self): + group_id = str(uuid.uuid4()) + self.parent._scan_group = group_id + return self + + def __exit__(self, *exc): + self.parent.close_scan_group() + self.parent._scan_group = None + + +class ScanDef(ContextDecorator): + """ScanDef is a ContextDecorator for defining a new scan""" + + def __init__(self, parent: Scans = None) -> None: + super().__init__() + self.parent = parent + + def __enter__(self): + if self.parent._scan_def_id is not None: + raise ScanAbortion("Nested scan definitions currently not supported.") + scan_def_id = str(uuid.uuid4()) + self.parent._scan_def_id = scan_def_id + self.parent.open_scan_def() + return self + + def __exit__(self, *exc): + if exc[0] is None: + self.parent.close_scan_def() + self.parent._scan_def_id = None + + +class HideReport(ContextDecorator): + """HideReport is a ContextDecorator for hiding the report""" + + def __init__(self, parent: Scans = None) -> None: + super().__init__() + self.parent = parent + + def __enter__(self): + if self.parent._hide_report is None: + self.parent._hide_report = True + return self + + def __exit__(self, *exc): + self.parent._hide_report = None + + +class DatasetIdOnHold(ContextDecorator): + """DatasetIdOnHold is a ContextDecorator for setting the dataset id on hold""" + + def __init__(self, parent: Scans = None) -> None: + super().__init__() + self.parent = parent + self._call_count = 0 + + def __enter__(self): + self._call_count += 1 + if self.parent._dataset_id_on_hold is None: + self.parent._dataset_id_on_hold = True + return self + + def __exit__(self, *exc): + self._call_count -= 1 + if self._call_count: + return + self.parent._dataset_id_on_hold = None + queue = self.parent.parent.queue + queue.next_dataset_number += 1 + + +class FileWriter: + @typechecked + def __init__(self, file_suffix: str = None, file_directory: str = None) -> None: + """Context manager for updating metadata + + Args: + fw_config (dict): Dictionary with metadata for the filewriter, can only have keys "file_suffix" and "file_directory" + """ + self.client = self._get_client() + self.system_config = self.client.system_config + self._orig_system_config = None + self._orig_metadata = None + self.file_suffix = file_suffix + self.file_directory = file_directory + + def _get_client(self): + """Get BEC client""" + return builtins.__dict__["bec"] + + def __enter__(self): + """Enter the context manager""" + self._orig_metadata = deepcopy(self.client.metadata) + self._orig_system_config = self.system_config.model_copy(deep=True) + self.system_config.file_suffix = self.file_suffix + self.system_config.file_directory = self.file_directory + return self + + def __exit__(self, *exc): + """Exit the context manager""" + self.client.metadata = self._orig_metadata + self.system_config.file_suffix = self._orig_system_config.file_suffix + self.system_config.file_directory = self._orig_system_config.file_directory + + +class Metadata: + @typechecked + def __init__(self, metadata: dict) -> None: + """Context manager for updating metadata + + Args: + metadata (dict): Metadata dictionary + """ + self.client = self._get_client() + self._metadata = metadata + self._orig_metadata = None + + def _get_client(self): + """Get BEC client""" + return builtins.__dict__["bec"] + + def __enter__(self): + """Enter the context manager""" + self._orig_metadata = deepcopy(self.client.metadata) + self.client.metadata.update(self._metadata) + return self + + def __exit__(self, *exc): + """Exit the context manager""" + self.client.metadata = self._orig_metadata + + +class ScanExport: + def __init__(self, output_file: str) -> None: + """Context manager for exporting scans + + Args: + output_file (str): Output file name + """ + self.output_file = output_file + self.client = None + self.scans = None + + def _check_abort_on_ctrl_c(self): + """Check if scan should be aborted on Ctrl-C""" + # pylint: disable=protected-access + if not self.client._service_config.abort_on_ctrl_c: + raise RuntimeError( + "ScanExport context manager can only be used if abort_on_ctrl_c is set to True" + ) + + def _get_client(self): + return builtins.__dict__["bec"] + + def __enter__(self): + self.scans = [] + self.client = self._get_client() + self.client.scans._scan_export = self + self._check_abort_on_ctrl_c() + return self + + def _export_to_csv(self): + scan_to_csv(self.scans, self.output_file) + + def __exit__(self, *exc): + try: + for scan in self.scans: + scan.wait() + finally: + try: + self._export_to_csv() + self.scans = None + except Exception as exc: + logger.warning(f"Could not export scans to csv file, due to exception {exc}") diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt new file mode 100644 index 0000000..b34195f --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt @@ -0,0 +1,2061 @@ +https://bec.readthedocs.io/en/latest/api_reference/_autosummary/bec_server.scan_server.scans.ScanBase.html#bec_server.scan_server.scans.ScanBase + + +Sequence of events: + + + read_scan_motors + + prepare_positions + + _calculate_positions + + _optimize_trajectory + + _set_position_offset + + _check_limits + + open_scan + + stage + + run_baseline_reading + + pre_scan + + scan_core + + finalize + + unstage + + cleanup + + + + + + +class ScanBase(*args, device_manager: DeviceManagerBase +| None = None, parameter: dict \ +| None = None, exp_time: float = 0, readout_time: float = 0, acquisition_config: dict +| None = None, settling_time: float = 0, relative: bool = False, burst_at_each_point: int = 1, +frames_per_trigger: int = 1, optim_trajectory: Literal['corridor', None] +| None = None, monitored: list | None = None, metadata: dict | None = None, **kwargs) + +Key functions in ScanBase during step by step scanning are + + +# This walks through all positions and calls _at_each_point() + + def scan_core(self): + """perform the scan core procedure""" + for ind, pos in self._get_position(): + for self.burst_index in range(self.burst_at_each_point): + yield from self._at_each_point(ind, pos) + self.burst_index = 0 + + + + def _at_each_point(self, ind=None, pos=None): + yield from self._move_scan_motors_and_wait(pos) -------- > wait until all motors have arrived + if ind > 0: ----------> now wait for primary group + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + time.sleep(self.settling_time) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) ------ trigger after settling time + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) ---- wait for trigger finish + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="read", group="scan_motor", wait_group="readout_primary" + ) + + self.point_id += 1 + + + + +Methods + + + + + + + + + + + + + + + + + + + +sourece code in bec_server.scan_server.scans + + + +from __future__ import annotations + +import ast +import enum +import threading +import time +import uuid +from abc import ABC, abstractmethod +from typing import Any, Literal + +import numpy as np + +from bec_lib.device import DeviceBase +from bec_lib.devicemanager import DeviceManagerBase +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger + +from .errors import LimitError, ScanAbortion +from .path_optimization import PathOptimizerMixin +from .scan_stubs import ScanStubs + +logger = bec_logger.logger + + + +[docs] +class ScanArgType(str, enum.Enum): + DEVICE = "device" + FLOAT = "float" + INT = "int" + BOOL = "boolean" + STR = "str" + LIST = "list" + DICT = "dict" + + + + +[docs] +def unpack_scan_args(scan_args: dict[str, Any]) -> list: + """unpack_scan_args unpacks the scan arguments and returns them as a tuple. + + Args: + scan_args (dict[str, Any]): scan arguments + + Returns: + list: list of arguments + """ + args = [] + if not scan_args: + return args + if not isinstance(scan_args, dict): + return scan_args + for cmd_name, cmd_args in scan_args.items(): + args.append(cmd_name) + args.extend(cmd_args) + return args + + + + +[docs] +def get_2D_raster_pos(axis, snaked=True): + """get_2D_raster_post calculates and returns the positions for a 2D + + snaked==True: + ->->->->- + -<-<-<-<- + ->->->->- + snaked==False: + ->->->->- + ->->->->- + ->->->->- + + Args: + axis (list): list of positions for each axis + snaked (bool, optional): If true, the positions will be calculcated for a snake scan. Defaults to True. + + Returns: + array: calculated positions + """ + + x_grid, y_grid = np.meshgrid(axis[0], axis[1]) + if snaked: + y_grid.T[::2] = np.fliplr(y_grid.T[::2]) + x_flat = x_grid.T.ravel() + y_flat = y_grid.T.ravel() + positions = np.vstack((x_flat, y_flat)).T + return positions + + + +# pylint: disable=too-many-arguments + +[docs] +def get_fermat_spiral_pos( + m1_start, m1_stop, m2_start, m2_stop, step=1, spiral_type=0, center=False +): + """get_fermat_spiral_pos calculates and returns the positions for a Fermat spiral scan. + + Args: + m1_start (float): start position motor 1 + m1_stop (float): end position motor 1 + m2_start (float): start position motor 2 + m2_stop (float): end position motor 2 + step (float, optional): Step size. Defaults to 1. + spiral_type (float, optional): Angular offset in radians that determines the shape of the spiral. + A spiral with spiral_type=2 is the same as spiral_type=0. Defaults to 0. + center (bool, optional): Add a center point. Defaults to False. + + Returns: + array: calculated positions in the form [[m1, m2], ...] + """ + positions = [] + phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2.0) + spiral_type * np.pi + + start = int(not center) + + length_axis1 = abs(m1_stop - m1_start) + length_axis2 = abs(m2_stop - m2_start) + n_max = int(length_axis1 * length_axis2 * 3.2 / step / step) + + for ii in range(start, n_max): + radius = step * 0.57 * np.sqrt(ii) + if abs(radius * np.sin(ii * phi)) > length_axis1 / 2: + continue + if abs(radius * np.cos(ii * phi)) > length_axis2 / 2: + continue + positions.extend([(radius * np.sin(ii * phi), radius * np.cos(ii * phi))]) + return np.array(positions) + + + + +[docs] +def get_round_roi_scan_positions(lx: float, ly: float, dr: float, nth: int, cenx=0, ceny=0): + """ + get_round_roi_scan_positions calculates and returns the positions for a round scan in a rectangular region of interest. + + Args: + lx (float): length in x + ly (float): length in y + dr (float): step size + nth (int): number of angles in the inner ring + cenx (int, optional): center in x. Defaults to 0. + ceny (int, optional): center in y. Defaults to 0. + + Returns: + array: calculated positions in the form [[x, y], ...] + """ + positions = [] + nr = 1 + int(np.floor(max([lx, ly]) / dr)) + for ir in range(1, nr + 2): + rr = ir * dr + dth = 2 * np.pi / (nth * ir) + pos = [ + (rr * np.cos(ith * dth) + cenx, rr * np.sin(ith * dth) + ceny) + for ith in range(nth * ir) + if np.abs(rr * np.cos(ith * dth)) < lx / 2 and np.abs(rr * np.sin(ith * dth)) < ly / 2 + ] + positions.extend(pos) + return np.array(positions) + + + + +[docs] +def get_round_scan_positions(r_in: float, r_out: float, nr: int, nth: int, cenx=0, ceny=0): + """ + get_round_scan_positions calculates and returns the positions for a round scan. + + Args: + r_in (float): inner radius + r_out (float): outer radius + nr (int): number of radii + nth (int): number of angles in the inner ring + cenx (int, optional): center in x. Defaults to 0. + ceny (int, optional): center in y. Defaults to 0. + + Returns: + array: calculated positions in the form [[x, y], ...] + + """ + positions = [] + dr = (r_in - r_out) / nr + for ir in range(1, nr + 2): + rr = r_in + ir * dr + dth = 2 * np.pi / (nth * ir) + positions.extend( + [ + (rr * np.sin(ith * dth) + cenx, rr * np.cos(ith * dth) + ceny) + for ith in range(nth * ir) + ] + ) + return np.array(positions, dtype=float) + + + + +[docs] +class RequestBase(ABC): + """ + Base class for all scan requests. + """ + + scan_name = "" + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + gui_args = {} + required_kwargs = [] + return_to_start_after_abort = False + use_scan_progress_report = False + + def __init__( + self, + *args, + device_manager: DeviceManagerBase = None, + monitored: list = None, + parameter: dict = None, + metadata: dict = None, + **kwargs, + ) -> None: + super().__init__() + self._shutdown_event = threading.Event() + self.parameter = parameter if parameter is not None else {} + self.caller_args = self.parameter.get("args", {}) + self.caller_kwargs = self.parameter.get("kwargs", {}) + self.metadata = metadata + self.device_manager = device_manager + self.connector = device_manager.connector + self.DIID = 0 + self.scan_motors = [] + self.positions = [] + self._pre_scan_macros = [] + self._scan_report_devices = None + self._get_scan_motors() + self.readout_priority = { + "monitored": monitored if monitored is not None else [], + "baseline": [], + "on_request": [], + "async": [], + } + self.update_readout_priority() + if metadata is None: + self.metadata = {} + self.stubs = ScanStubs( + connector=self.device_manager.connector, + device_msg_callback=self.device_msg_metadata, + shutdown_event=self._shutdown_event, + ) + + @property + def scan_report_devices(self): + """devices to be included in the scan report""" + if self._scan_report_devices is None: + return self.readout_priority["monitored"] + return self._scan_report_devices + + @scan_report_devices.setter + def scan_report_devices(self, devices: list): + self._scan_report_devices = devices + + def device_msg_metadata(self): + default_metadata = {"readout_priority": "monitored", "DIID": self.DIID} + metadata = {**default_metadata, **self.metadata} + self.DIID += 1 + return metadata + + @staticmethod + def _get_func_name_from_macro(macro: str): + return ast.parse(macro).body[0].name + + +[docs] + def run_pre_scan_macros(self): + """run pre scan macros if any""" + macros = self.device_manager.connector.lrange(MessageEndpoints.pre_scan_macros(), 0, -1) + for macro in macros: + macro = macro.value.strip() + func_name = self._get_func_name_from_macro(macro) + exec(macro) + eval(func_name)(self.device_manager.devices, self) + + + def initialize(self): + self.run_pre_scan_macros() + + def _check_limits(self): + logger.debug("check limits") + for ii, dev in enumerate(self.scan_motors): + low_limit, high_limit = self.device_manager.devices[dev].limits + if low_limit >= high_limit: + # if both limits are equal or low > high, no restrictions ought to be applied + return + for pos in self.positions: + pos_axis = pos[ii] + if not low_limit <= pos_axis <= high_limit: + raise LimitError( + f"Target position {pos} for motor {dev} is outside of range: [{low_limit}," + f" {high_limit}]" + ) + + def _get_scan_motors(self): + if len(self.caller_args) == 0: + return + if self.arg_bundle_size.get("bundle"): + self.scan_motors = list(self.caller_args.keys()) + return + for motor in self.caller_args: + if motor not in self.device_manager.devices: + continue + self.scan_motors.append(motor) + + +[docs] + def update_readout_priority(self): + """update the readout priority for this request. Typically the monitored devices should also include the scan motors.""" + self.readout_priority["monitored"].extend(self.scan_motors) + self.readout_priority["monitored"] = list( + sorted( + set(self.readout_priority["monitored"]), + key=self.readout_priority["monitored"].index, + ) + ) + + + @abstractmethod + def run(self): + pass + + + + +[docs] +class ScanBase(RequestBase, PathOptimizerMixin): + """ + Base class for all scans. The following methods are called in the following order during the scan + 1. initialize + - run_pre_scan_macros + 2. read_scan_motors + 3. prepare_positions + - _calculate_positions + - _optimize_trajectory + - _set_position_offset + - _check_limits + 4. open_scan + 5. stage + 6. run_baseline_reading + 7. pre_scan + 8. scan_core + 9. finalize + 10. unstage + 11. cleanup + + A subclass of ScanBase must implement the following methods: + - _calculate_positions + + Attributes: + scan_name (str): name of the scan + scan_type (str): scan type. Can be "step" or "fly" + arg_input (list): list of scan argument types + arg_bundle_size (dict): + - bundle: number of arguments that are bundled together + - min: minimum number of bundles + - max: maximum number of bundles + required_kwargs (list): list of required kwargs + return_to_start_after_abort (bool): if True, the scan will return to the start position after an abort + """ + + scan_name = "" + scan_type = "step" + required_kwargs = ["required"] + return_to_start_after_abort = True + use_scan_progress_report = True + + # perform pre-move action before the pre_scan trigger is sent + pre_move = True + + def __init__( + self, + *args, + device_manager: DeviceManagerBase = None, + parameter: dict = None, + exp_time: float = 0, + readout_time: float = 0, + acquisition_config: dict = None, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + frames_per_trigger: int = 1, + optim_trajectory: Literal["corridor", None] = None, + monitored: list = None, + metadata: dict = None, + **kwargs, + ): + super().__init__( + *args, + device_manager=device_manager, + monitored=monitored, + parameter=parameter, + metadata=metadata, + **kwargs, + ) + self.DIID = 0 + self.point_id = 0 + self.exp_time = exp_time + self.readout_time = readout_time + self.acquisition_config = acquisition_config + self.settling_time = settling_time + self.relative = relative + self.burst_at_each_point = burst_at_each_point + self.frames_per_trigger = frames_per_trigger + self.optim_trajectory = optim_trajectory + self.burst_index = 0 + + self.start_pos = [] + self.positions = [] + self.num_pos = 0 + + if self.scan_name == "": + raise ValueError("scan_name cannot be empty") + + if acquisition_config is None or "default" not in acquisition_config: + self.acquisition_config = { + "default": {"exp_time": self.exp_time, "readout_time": self.readout_time} + } + + @property + def monitor_sync(self): + """ + monitor_sync is a property that defines how monitored devices are synchronized. + It can be either bec or the name of the device. If set to bec, the scan bundler + will synchronize scan segments based on the bec triggered readouts. If set to a device name, + the scan bundler will synchronize based on the readouts of the device, i.e. upon + receiving a new readout of the device, cached monitored readings will be added + to the scan segment. + """ + return "bec" + + +[docs] + def read_scan_motors(self): + """read the scan motors""" + yield from self.stubs.read_and_wait(device=self.scan_motors, wait_group="scan_motor") + + + @abstractmethod + def _calculate_positions(self) -> None: + """Calculate the positions""" + + def _optimize_trajectory(self): + if not self.optim_trajectory: + return + if self.optim_trajectory == "corridor": + self.positions = self.optimize_corridor(self.positions) + return + return + + +[docs] + def prepare_positions(self): + """prepare the positions for the scan""" + self._calculate_positions() + self._optimize_trajectory() + self.num_pos = len(self.positions) * self.burst_at_each_point + yield from self._set_position_offset() + self._check_limits() + + + +[docs] + def open_scan(self): + """open the scan""" + positions = self.positions if isinstance(self.positions, list) else self.positions.tolist() + yield from self.stubs.open_scan( + scan_motors=self.scan_motors, + readout_priority=self.readout_priority, + num_pos=self.num_pos, + positions=positions, + scan_name=self.scan_name, + scan_type=self.scan_type, + ) + + + +[docs] + def stage(self): + """call the stage procedure""" + yield from self.stubs.stage() + + + +[docs] + def run_baseline_reading(self): + """perform a reading of all baseline devices""" + yield from self.stubs.baseline_reading() + + + def _set_position_offset(self): + for dev in self.scan_motors: + val = yield from self.stubs.send_rpc_and_wait(dev, "read") + self.start_pos.append(val[dev].get("value")) + if self.relative: + self.positions += self.start_pos + + +[docs] + def close_scan(self): + """close the scan""" + yield from self.stubs.close_scan() + + + +[docs] + def scan_core(self): + """perform the scan core procedure""" + for ind, pos in self._get_position(): + for self.burst_index in range(self.burst_at_each_point): + yield from self._at_each_point(ind, pos) + self.burst_index = 0 + + + +[docs] + def return_to_start(self): + """return to the start position""" + yield from self._move_scan_motors_and_wait(self.start_pos) + + + +[docs] + def finalize(self): + """finalize the scan""" + yield from self.return_to_start() + yield from self.stubs.wait(wait_type="read", group="primary", wait_group="readout_primary") + yield from self.stubs.complete(device=None) + + + +[docs] + def unstage(self): + """call the unstage procedure""" + yield from self.stubs.unstage() + + + +[docs] + def cleanup(self): + """call the cleanup procedure""" + yield from self.close_scan() + + + def _at_each_point(self, ind=None, pos=None): + yield from self._move_scan_motors_and_wait(pos) + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + time.sleep(self.settling_time) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="read", group="scan_motor", wait_group="readout_primary" + ) + + self.point_id += 1 + + def _move_scan_motors_and_wait(self, pos): + if not isinstance(pos, list) and not isinstance(pos, np.ndarray): + pos = [pos] + if len(pos) == 0: + return + for ind, val in enumerate(self.scan_motors): + yield from self.stubs.set(device=val, value=pos[ind], wait_group="scan_motor") + + yield from self.stubs.wait(wait_type="move", group="scan_motor", wait_group="scan_motor") + + def _get_position(self): + for ind, pos in enumerate(self.positions): + yield (ind, pos) + + def scan_report_instructions(self): + yield None + + +[docs] + def pre_scan(self): + """ + pre scan procedure. This method is called before the scan_core method and can be used to + perform additional tasks before the scan is started. This + """ + if self.pre_move and len(self.positions) > 0: + yield from self._move_scan_motors_and_wait(self.positions[0]) + yield from self.stubs.pre_scan() + + + +[docs] + def run(self): + """run the scan. This method is called by the scan server and is the main entry point for the scan.""" + self.initialize() + yield from self.read_scan_motors() + yield from self.prepare_positions() + yield from self.scan_report_instructions() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + yield from self.pre_scan() + yield from self.scan_core() + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + + + @classmethod + def scan(cls, *args, **kwargs): + scan = cls(args, **kwargs) + yield from scan.run() + + + + +[docs] +class SyncFlyScanBase(ScanBase, ABC): + """ + Fly scan base class for all synchronous fly scans. A synchronous fly scan is a scan where the flyer is + synced with the monitored devices. + Classes inheriting from SyncFlyScanBase must at least implement the scan_core method and the monitor_sync property. + """ + + scan_type = "fly" + pre_move = False + + def _get_scan_motors(self): + # fly scans normally do not have stepper scan motors so + # the default way of retrieving scan motors is not applicable + return [] + + @property + @abstractmethod + def monitor_sync(self) -> str: + """ + monitor_sync is the flyer that will be used to synchronize the monitor readings in the scan bundler. + The return value should be the name of the flyer device. + """ + + def _calculate_positions(self) -> None: + pass + + +[docs] + def read_scan_motors(self): + yield None + + + +[docs] + def prepare_positions(self): + yield None + + + +[docs] + @abstractmethod + def scan_core(self): + """perform the scan core procedure""" + + + ############################################ + # Example of how to kickoff and wait for a flyer: + ############################################ + + # yield from self.stubs.kickoff(device=self.flyer, parameter=self.caller_kwargs) + # yield from self.stubs.complete(device=self.flyer) + # target_diid = self.DIID - 1 + + # while True: + # status = self.stubs.get_req_status( + # device=self.flyer, RID=self.metadata["RID"], DIID=target_diid + # ) + # progress = self.stubs.get_device_progress( + # device=self.flyer, RID=self.metadata["RID"] + # ) + # if progress: + # self.num_pos = progress + # if status: + # break + # time.sleep(1) + + # def _get_flyer_status(self) -> list: + # connector = self.device_manager.connector + + # pipe = connector.pipeline() + # connector.lrange( + # MessageEndpoints.device_req_status_container(self.metadata["RID"]), 0, -1, pipe + # ) + # connector.get(MessageEndpoints.device_readback(self.flyer), pipe) + # return connector.execute_pipeline(pipe) + +g + +[docs] +class AsyncFlyScanBase(SyncFlyScanBase): + """ + Fly scan base class for all asynchronous fly scans. An asynchronous fly scan is a scan where the flyer is + not synced with the monitored devices. + Classes inheriting from AsyncFlyScanBase must at least implement the scan_core method. + """ + + @property + def monitor_sync(self): + return "bec" + + + + +[docs] +class ScanStub(RequestBase): + pass + + + + +[docs] +class OpenScanDef(ScanStub): + scan_name = "open_scan_def" + + def run(self): + yield from self.stubs.open_scan_def() + + + + +[docs] +class CloseScanDef(ScanStub): + scan_name = "close_scan_def" + + def run(self): + yield from self.stubs.close_scan_def() + + + + +[docs] +class CloseScanGroup(ScanStub): + scan_name = "close_scan_group" + + def run(self): + yield from self.stubs.close_scan_group() + + + + +[docs] +class DeviceRPC(ScanStub): + scan_name = "device_rpc" + arg_input = { + "device": ScanArgType.DEVICE, + "func": ScanArgType.STR, + "args": ScanArgType.LIST, + "kwargs": ScanArgType.DICT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1} + + def _get_scan_motors(self): + pass + + def run(self): + # different to calling self.device_rpc, this procedure will not wait for a reply and therefore not check any errors. + yield from self.stubs.rpc(device=self.parameter.get("device"), parameter=self.parameter) + + + + +[docs] +class Move(RequestBase): + scan_name = "mv" + arg_input = {"device": ScanArgType.DEVICE, "target": ScanArgType.FLOAT} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + required_kwargs = ["relative"] + + def __init__(self, *args, relative=False, **kwargs): + """ + Move device(s) to an absolute position + Args: + *args (Device, float): pairs of device / position arguments + relative (bool): if True, move relative to current position + + Returns: + ScanReport + + Examples: + >>> scans.mv(dev.samx, 1, dev.samy,2) + """ + super().__init__(**kwargs) + self.relative = relative + self.start_pos = [] + + def _calculate_positions(self): + self.positions = np.asarray([[val[0] for val in self.caller_args.values()]], dtype=float) + + def _at_each_point(self, pos=None): + for ii, motor in enumerate(self.scan_motors): + yield from self.stubs.set( + device=motor, + value=self.positions[0][ii], + wait_group="scan_motor", + metadata={"response": True}, + ) + + def cleanup(self): + pass + + def _set_position_offset(self): + self.start_pos = [] + for dev in self.scan_motors: + val = yield from self.stubs.send_rpc_and_wait(dev, "read") + self.start_pos.append(val[dev].get("value")) + if not self.relative: + return + self.positions += self.start_pos + + def prepare_positions(self): + self._calculate_positions() + yield from self._set_position_offset() + self._check_limits() + + def scan_report_instructions(self): + yield None + + def run(self): + self.initialize() + yield from self.prepare_positions() + yield from self.scan_report_instructions() + yield from self._at_each_point() + + + + +[docs] +class UpdatedMove(Move): + """ + Move device(s) to an absolute position and show live updates. This is a blocking call. For non-blocking use Move. + Args: + *args (Device, float): pairs of device / position arguments + relative (bool): if True, move relative to current position + + Returns: + ScanReport + + Examples: + >>> scans.umv(dev.samx, 1, dev.samy,2) + """ + + scan_name = "umv" + + def _at_each_point(self, pos=None): + for ii, motor in enumerate(self.scan_motors): + yield from self.stubs.set( + device=motor, value=self.positions[0][ii], wait_group="scan_motor" + ) + + for motor in self.scan_motors: + yield from self.stubs.wait(wait_type="move", device=motor, wait_group="scan_motor") + + def scan_report_instructions(self): + yield from self.stubs.scan_report_instruction( + { + "readback": { + "RID": self.metadata["RID"], + "devices": self.scan_motors, + "start": self.start_pos, + "end": self.positions[0], + } + } + ) + + + + +[docs] +class Scan(ScanBase): + scan_name = "grid_scan" + arg_input = { + "device": ScanArgType.DEVICE, + "start": ScanArgType.FLOAT, + "stop": ScanArgType.FLOAT, + "steps": ScanArgType.INT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 2, "max": None} + required_kwargs = ["relative"] + gui_config = { + "Scan Parameters": ["exp_time", "settling_time", "burst_at_each_point", "relative"] + } + + def __init__( + self, + *args, + exp_time: float = 0, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + Scan two or more motors in a grid. + + Args: + *args (Device, float, float, int): pairs of device / start / stop / steps arguments + exp_time (float): exposure time in seconds. Default is 0. + settling_time (float): settling time in seconds. Default is 0. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.grid_scan(dev.motor1, -5, 5, 10, dev.motor2, -5, 5, 10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, + settling_time=settling_time, + relative=relative, + burst_at_each_point=burst_at_each_point, + **kwargs, + ) + + def _calculate_positions(self): + axis = [] + for _, val in self.caller_args.items(): + axis.append(np.linspace(val[0], val[1], val[2], dtype=float)) + if len(axis) > 1: + self.positions = get_2D_raster_pos(axis) + else: + self.positions = np.vstack(tuple(axis)).T + + + + +[docs] +class FermatSpiralScan(ScanBase): + scan_name = "fermat_scan" + required_kwargs = ["step", "relative"] + gui_config = { + "Device 1": ["motor1", "start_motor1", "stop_motor1"], + "Device 2": ["motor2", "start_motor2", "stop_motor2"], + "Movement Parameters": ["step", "relative"], + "Acquisition Parameters": ["exp_time", "settling_time", "burst_at_each_point"], + } + + def __init__( + self, + motor1: DeviceBase, + start_motor1: float, + stop_motor1: float, + motor2: DeviceBase, + start_motor2: float, + stop_motor2: float, + step: float = 0.1, + exp_time: float = 0, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + spiral_type: float = 0, + optim_trajectory: Literal["corridor", None] = None, + **kwargs, + ): + """ + A scan following Fermat's spiral. + + Args: + motor1 (DeviceBase): first motor + start_motor1 (float): start position motor 1 + stop_motor1 (float): end position motor 1 + motor2 (DeviceBase): second motor + start_motor2 (float): start position motor 2 + stop_motor2 (float): end position motor 2 + step (float): step size in motor units. Default is 0.1. + exp_time (float): exposure time in seconds. Default is 0. + settling_time (float): settling time in seconds. Default is 0. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + spiral_type (float): type of spiral to use. Default is 0. + optim_trajectory (str): trajectory optimization method. Default is None. Options are "corridor" and "none". + + Returns: + ScanReport + + Examples: + >>> scans.fermat_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, step=0.5, exp_time=0.1, relative=True, optim_trajectory="corridor") + + """ + super().__init__( + exp_time=exp_time, + settling_time=settling_time, + relative=relative, + burst_at_each_point=burst_at_each_point, + optim_trajectory=optim_trajectory, + **kwargs, + ) + self.motor1 = motor1 + self.motor2 = motor2 + self.start_motor1 = start_motor1 + self.stop_motor1 = stop_motor1 + self.start_motor2 = start_motor2 + self.stop_motor2 = stop_motor2 + self.step = step + self.spiral_type = spiral_type + + def _calculate_positions(self): + self.positions = get_fermat_spiral_pos( + self.start_motor1, + self.stop_motor1, + self.start_motor2, + self.stop_motor2, + step=self.step, + spiral_type=self.spiral_type, + center=False, + ) + + + + +[docs] +class RoundScan(ScanBase): + scan_name = "round_scan" + required_kwargs = ["relative"] + gui_config = { + "Motors": ["motor_1", "motor_2"], + "Ring Parameters": ["inner_ring", "outer_ring", "number_of_rings", "pos_in_first_ring"], + "Scan Parameters": ["relative", "burst_at_each_point"], + } + + def __init__( + self, + motor_1: DeviceBase, + motor_2: DeviceBase, + inner_ring: float, + outer_ring: float, + number_of_rings: int, + pos_in_first_ring: int, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A scan following a round shell-like pattern. + + Args: + motor_1 (DeviceBase): first motor + motor_2 (DeviceBase): second motor + inner_ring (float): inner radius + outer_ring (float): outer radius + number_of_rings (int): number of rings + pos_in_first_ring (int): number of positions in the first ring + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_scan(dev.motor1, dev.motor2, 0, 25, 5, 3, exp_time=0.1, relative=True) + + """ + super().__init__(relative=relative, burst_at_each_point=burst_at_each_point, **kwargs) + self.axis = [] + self.motor_1 = motor_1 + self.motor_2 = motor_2 + self.inner_ring = inner_ring + self.outer_ring = outer_ring + self.number_of_rings = number_of_rings + self.pos_in_first_ring = pos_in_first_ring + + def _get_scan_motors(self): + caller_args = list(self.caller_args.items())[0] + self.scan_motors = [caller_args[0], caller_args[1][0]] + + def _calculate_positions(self): + self.positions = get_round_scan_positions( + r_in=self.inner_ring, + r_out=self.outer_ring, + nr=self.number_of_rings, + nth=self.pos_in_first_ring, + ) + + + + +[docs] +class ContLineScan(ScanBase): + scan_name = "cont_line_scan" + required_kwargs = ["steps", "relative"] + scan_type = "step" + gui_config = { + "Device": ["device", "start", "stop"], + "Movement Parameters": ["steps", "relative", "offset", "atol"], + "Acquisition Parameters": ["exp_time", "burst_at_each_point"], + } + + def __init__( + self, + device: DeviceBase, + start: float, + stop: float, + offset: float = 1, + atol: float = 0.5, + exp_time: float = 0, + steps: int = 10, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A continuous line scan. Use this scan if you want to move a motor continuously from start to stop position whilst + acquiring data at predefined positions. The scan will abort if the motor moves too fast and a point is skipped. + + Args: + device (DeviceBase): motor to move continuously from start to stop position + start (float): start position + stop (float): stop position + exp_time (float): exposure time in seconds. Default is 0. + steps (int): number of steps. Default is 10. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + offset (float): offset in motor units. Default is 1. + atol (float): absolute tolerance for position check. Default is 0.5. + + Returns: + ScanReport + + Examples: + >>> scans.cont_line_scan(dev.motor1, -5, 5, steps=10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.steps = steps + self.device = device + self.offset = offset + self.start = start + self.stop = stop + self.atol = atol + self.motor_velocity = self.device_manager.devices[self.device].read()[ + f"{self.device}_velocity" + ]["value"] + + def _calculate_positions(self) -> None: + self.positions = np.linspace(self.start, self.stop, self.steps, dtype=float)[ + np.newaxis, : + ].T + # Check if the motor is moving faster than the exp_time + dist_setp = self.positions[1][0] - self.positions[0][0] + time_per_step = dist_setp / self.motor_velocity + if time_per_step < self.exp_time: + raise ScanAbortion( + f"Motor {self.device} is moving too fast. Time per step: {time_per_step:.03f} < Exp_time: {self.exp_time:.03f}." + + f" Consider reducing speed {self.motor_velocity} or reducing exp_time {self.exp_time}" + ) + + def _check_limits(self): + logger.debug("check limits") + low_limit, high_limit = self.device_manager.devices[self.device].limits + if low_limit >= high_limit: + # if both limits are equal or low > high, no restrictions ought to be applied + return + for ii, pos in enumerate(self.positions): + if ii == 0: + pos_axis = pos - self.offset + else: + pos_axis = pos + if not low_limit <= pos_axis <= high_limit: + raise LimitError( + f"Target position {pos} for motor {self.device} is outside of range: [{low_limit}," + f" {high_limit}]" + ) + + def _at_each_point(self, _pos=None): + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.read(group="primary", wait_group="primary", point_id=self.point_id) + self.point_id += 1 + + +[docs] + def scan_core(self): + yield from self._move_scan_motors_and_wait(self.positions[0] - self.offset) + # send the slow motor on its way + yield from self.stubs.set( + device=self.scan_motors[0], value=self.positions[-1][0], wait_group="scan_motor" + ) + + while self.point_id < len(self.positions[:]): + cont_motor_positions = self.device_manager.devices[self.scan_motors[0]].readback.read() + + if not cont_motor_positions: + continue + + cont_motor_positions = cont_motor_positions[self.scan_motors[0]].get("value") + logger.debug(f"Current position of {self.scan_motors[0]}: {cont_motor_positions}") + # TODO: consider the alternative, which triggers a readout for each point right after the motor passed it + # if cont_motor_positions > self.positions[self.point_id][0]: + if np.isclose(cont_motor_positions, self.positions[self.point_id][0], atol=self.atol): + logger.debug(f"reading point {self.point_id}") + yield from self._at_each_point() + continue + if cont_motor_positions > self.positions[self.point_id][0]: + raise ScanAbortion( + f"Skipped point {self.point_id + 1}:" + f"Consider reducing speed {self.device_manager.devices[self.scan_motors[0]].velocity.get()}, " + f"increasing the atol {self.atol}, or increasing the offset {self.offset}" + ) + + + + + +[docs] +class ContLineFlyScan(AsyncFlyScanBase): + scan_name = "cont_line_fly_scan" + required_kwargs = [] + use_scan_progress_report = False + gui_config = {"Device": ["motor", "start", "stop"], "Scan Parameters": ["exp_time", "relative"]} + + def __init__( + self, + motor: DeviceBase, + start: float, + stop: float, + exp_time: float = 0, + relative: bool = False, + **kwargs, + ): + """ + A continuous line fly scan. Use this scan if you want to move a motor continuously from start to stop position whilst + acquiring data as fast as possible (respecting the exposure time). The scan will stop automatically when the motor + reaches the end position. + + Args: + motor (DeviceBase): motor to move continuously from start to stop position + start (float): start position + stop (float): stop position + exp_time (float): exposure time in seconds. Default is 0. + relative (bool): if True, the motor will be moved relative to its current position. Default is False. + + Returns: + ScanReport + + Examples: + >>> scans.cont_line_fly_scan(dev.sam_rot, 0, 180, exp_time=0.1) + + """ + super().__init__(relative=relative, exp_time=exp_time, **kwargs) + self.motor = motor + self.start = start + self.stop = stop + self.device_move_request_id = str(uuid.uuid4()) + + +[docs] + def prepare_positions(self): + self.positions = np.array([[self.start], [self.stop]], dtype=float) + self.num_pos = None + yield from self._set_position_offset() + + + def scan_report_instructions(self): + yield from self.stubs.scan_report_instruction( + { + "readback": { + "RID": self.device_move_request_id, + "devices": [self.motor], + "start": [self.start], + "end": [self.stop], + } + } + ) + + +[docs] + def scan_core(self): + # move the motor to the start position + yield from self.stubs.set_and_wait(device=[self.motor], positions=self.positions[0]) + + # start the flyer + flyer_request = yield from self.stubs.set_with_response( + device=self.motor, value=self.positions[1][0], request_id=self.device_move_request_id + ) + + while True: + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="trigger", group="trigger", wait_time=self.exp_time + ) + if self.stubs.request_is_completed(flyer_request): + break + self.point_id += 1 + + + +[docs] + def finalize(self): + yield from super().finalize() + self.num_pos = self.point_id + 1 + + + + + +[docs] +class RoundScanFlySim(SyncFlyScanBase): + scan_name = "round_scan_fly" + scan_type = "fly" + pre_move = False + required_kwargs = ["relative"] + gui_config = { + "Fly Parameters": ["flyer", "relative"], + "Ring Parameters": ["inner_ring", "outer_ring", "number_of_rings", "number_pos"], + } + + def __init__( + self, + flyer: DeviceBase, + inner_ring: float, + outer_ring: float, + number_of_rings: int, + number_pos: int, + relative: bool = False, + **kwargs, + ): + """ + A fly scan following a round shell-like pattern. + + Args: + flyer (DeviceBase): flyer device + inner_ring (float): inner radius + outer_ring (float): outer radius + number_of_rings (int): number of rings + number_pos (int): number of positions in the first ring + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_scan_fly(dev.flyer_sim, 0, 50, 5, 3, exp_time=0.1, relative=True) + + """ + super().__init__(**kwargs) + self.flyer = flyer + self.inner_ring = inner_ring + self.outer_ring = outer_ring + self.number_of_rings = number_of_rings + self.number_pos = number_pos + + def _get_scan_motors(self): + self.scan_motors = [] + + @property + def monitor_sync(self): + return self.flyer + + +[docs] + def prepare_positions(self): + self._calculate_positions() + self.num_pos = len(self.positions) * self.burst_at_each_point + self._check_limits() + yield None + + + +[docs] + def finalize(self): + yield + + + def _calculate_positions(self): + self.positions = get_round_scan_positions( + r_in=self.inner_ring, + r_out=self.outer_ring, + nr=self.number_of_rings, + nth=self.number_pos, + ) + + +[docs] + def scan_core(self): + yield from self.stubs.kickoff( + device=self.flyer, + parameter={ + "num_pos": self.num_pos, + "positions": self.positions.tolist(), + "exp_time": self.exp_time, + }, + ) + target_DIID = self.DIID - 1 + + while True: + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") + status = self.device_manager.connector.get(MessageEndpoints.device_status(self.flyer)) + if status: + device_is_idle = status.content.get("status", 1) == 0 + matching_RID = self.metadata.get("RID") == status.metadata.get("RID") + matching_DIID = target_DIID == status.metadata.get("DIID") + if device_is_idle and matching_RID and matching_DIID: + break + + time.sleep(1) + logger.debug("reading monitors") + + + + + +[docs] +class RoundROIScan(ScanBase): + scan_name = "round_roi_scan" + required_kwargs = ["dr", "nth", "relative"] + gui_config = { + "Motor 1": ["motor_1", "width_1"], + "Motor 2": ["motor_2", "width_2"], + "Shell Parametes": ["dr", "nth"], + "Acquisition Parameters": ["exp_time", "relative", "burst_at_each_point"], + } + + def __init__( + self, + motor_1: DeviceBase, + width_1: float, + motor_2: DeviceBase, + width_2: float, + dr: float = 1, + nth: int = 5, + exp_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A scan following a round-roi-like pattern. + + Args: + motor_1 (DeviceBase): first motor + width_1 (float): width of region of interest for motor_1 + motor_2 (DeviceBase): second motor + width_2 (float): width of region of interest for motor_2 + dr (float): shell width. Default is 1. + nth (int): number of points in the first shell. Default is 5. + exp_time (float): exposure time in seconds. Default is 0. + relative (bool): Start from an absolute or relative position. Default is False. + burst_at_each_point (int): number of acquisition per point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_roi_scan(dev.motor1, 20, dev.motor2, 20, dr=2, nth=3, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.motor_1 = motor_1 + self.motor_2 = motor_2 + self.width_1 = width_1 + self.width_2 = width_2 + self.dr = dr + self.nth = nth + + def _calculate_positions(self) -> None: + self.positions = get_round_roi_scan_positions( + lx=self.width_1, ly=self.width_2, dr=self.dr, nth=self.nth + ) + + + + +[docs] +class ListScan(ScanBase): + scan_name = "list_scan" + required_kwargs = ["relative"] + arg_input = {"device": ScanArgType.DEVICE, "positions": ScanArgType.LIST} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """ + A scan following the positions specified in a list. + Please note that all lists must be of equal length. + + Args: + *args: pairs of motors and position lists + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.list_scan(dev.motor1, [0,1,2,3,4], dev.motor2, [4,3,2,1,0], exp_time=0.1, relative=True) + + """ + super().__init__(parameter=parameter, **kwargs) + if len(set(len(entry[0]) for entry in self.caller_args.values())) != 1: + raise ValueError("All position lists must be of equal length.") + + def _calculate_positions(self): + self.positions = np.vstack(tuple(self.caller_args.values()), dtype=float).T.tolist() + + + + +[docs] +class TimeScan(ScanBase): + scan_name = "time_scan" + required_kwargs = ["points", "interval"] + gui_config = {"Scan Parameters": ["points", "interval", "exp_time", "burst_at_each_point"]} + + def __init__( + self, + points: int, + interval: float, + exp_time: float = 0, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + Trigger and readout devices at a fixed interval. + Note that the interval time cannot be less than the exposure time. + The effective "sleep" time between points is + sleep_time = interval - exp_time + + Args: + points: number of points + interval: time interval between points + exp_time: exposure time in s + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.time_scan(points=10, interval=1.5, exp_time=0.1, relative=True) + + """ + super().__init__(exp_time=exp_time, burst_at_each_point=burst_at_each_point, **kwargs) + self.points = points + self.interval = interval + self.interval -= self.exp_time + + def _calculate_positions(self) -> None: + pass + + +[docs] + def prepare_positions(self): + self.num_pos = self.points + yield None + + + def _at_each_point(self, ind=None, pos=None): + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.interval) + self.point_id += 1 + + +[docs] + def scan_core(self): + for ind in range(self.num_pos): + yield from self._at_each_point(ind) + + + + + +[docs] +class MonitorScan(ScanBase): + scan_name = "monitor_scan" + required_kwargs = ["relative"] + scan_type = "fly" + gui_config = {"Device": ["device", "start", "stop"], "Scan Parameters": ["relative"]} + + def __init__( + self, device: DeviceBase, start: float, stop: float, relative: bool = False, **kwargs + ): + """ + Readout all primary devices at each update of the monitored device. + + Args: + device (Device): monitored device + start (float): start position of the monitored device + stop (float): stop position of the monitored device + relative (bool): if True, the motor will be moved relative to its current position. Default is False. + + Returns: + ScanReport + + Examples: + >>> scans.monitor_scan(dev.motor1, -5, 5, exp_time=0.1, relative=True) + + """ + self.device = device + super().__init__(relative=relative, **kwargs) + self.start = start + self.stop = stop + + def _get_scan_motors(self): + self.scan_motors = [self.device] + self.flyer = self.device + + @property + def monitor_sync(self): + return self.flyer + + def _calculate_positions(self) -> None: + self.positions = np.array([[self.start], [self.stop]], dtype=float) + + +[docs] + def prepare_positions(self): + self._calculate_positions() + self.num_pos = 0 + yield from self._set_position_offset() + self._check_limits() + + + def _get_flyer_status(self) -> list: + connector = self.device_manager.connector + + pipe = connector.pipeline() + connector.lrange( + MessageEndpoints.device_req_status_container(self.metadata["RID"]), 0, -1, pipe + ) + connector.get(MessageEndpoints.device_readback(self.flyer), pipe) + return connector.execute_pipeline(pipe) + + +[docs] + def scan_core(self): + yield from self.stubs.set( + device=self.flyer, value=self.positions[0][0], wait_group="scan_motor" + ) + yield from self.stubs.wait(wait_type="move", device=self.flyer, wait_group="scan_motor") + + # send the slow motor on its way + yield from self.stubs.set( + device=self.flyer, + value=self.positions[1][0], + wait_group="scan_motor", + metadata={"response": True}, + ) + + while True: + move_completed, readback = self._get_flyer_status() + + if move_completed: + break + + if not readback: + continue + readback = readback.content["signals"] + yield from self.stubs.publish_data_as_read( + device=self.flyer, data=readback, point_id=self.point_id + ) + self.point_id += 1 + self.num_pos += 1 + + + + + +[docs] +class Acquire(ScanBase): + scan_name = "acquire" + required_kwargs = [] + gui_config = {"Scan Parameters": ["exp_time", "burst_at_each_point"]} + + def __init__(self, *args, exp_time: float = 0, burst_at_each_point: int = 1, **kwargs): + """ + A simple acquisition at the current position. + + Args: + exp_time (float): exposure time in s + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.acquire(exp_time=0.1, relative=True) + + """ + super().__init__(exp_time=exp_time, burst_at_each_point=burst_at_each_point, **kwargs) + + def _calculate_positions(self) -> None: + self.num_pos = self.burst_at_each_point + + +[docs] + def prepare_positions(self): + self._calculate_positions() + + + def _at_each_point(self, ind=None, pos=None): + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + + +[docs] + def scan_core(self): + for self.burst_index in range(self.burst_at_each_point): + yield from self._at_each_point(self.burst_index) + self.burst_index = 0 + + + +[docs] + def run(self): + self.initialize() + self.prepare_positions() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + yield from self.pre_scan() + yield from self.scan_core() + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + + + + + +[docs] +class LineScan(ScanBase): + scan_name = "line_scan" + required_kwargs = ["steps", "relative"] + arg_input = { + "device": ScanArgType.DEVICE, + "start": ScanArgType.FLOAT, + "stop": ScanArgType.FLOAT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + gui_config = { + "Movement Parameters": ["steps", "relative"], + "Acquisition Parameters": ["exp_time", "burst_at_each_point"], + } + + def __init__( + self, + *args, + exp_time: float = 0, + steps: int = None, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A line scan for one or more motors. + + Args: + *args (Device, float, float): pairs of device / start position / end position + exp_time (float): exposure time in s. Default: 0 + steps (int): number of steps. Default: 10 + relative (bool): if True, the start and end positions are relative to the current position. Default: False + burst_at_each_point (int): number of acquisition per point. Default: 1 + + Returns: + ScanReport + + Examples: + >>> scans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.steps = steps + + def _calculate_positions(self) -> None: + axis = [] + for _, val in self.caller_args.items(): + ax_pos = np.linspace(val[0], val[1], self.steps, dtype=float) + axis.append(ax_pos) + self.positions = np.array(list(zip(*axis)), dtype=float) + + + + +[docs] +class ScanComponent(ScanBase): + pass + + + + +[docs] +class OpenInteractiveScan(ScanComponent): + scan_name = "open_interactive_scan" + required_kwargs = [] + arg_input = {"device": ScanArgType.DEVICE} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.open_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1) + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + def _get_scan_motors(self): + caller_args = list(self.caller_args.keys()) + self.scan_motors = caller_args + + +[docs] + def run(self): + yield from self.stubs.open_scan_def() + self.initialize() + yield from self.read_scan_motors() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + + + + + +[docs] +class AddInteractiveScanPoint(ScanComponent): + scan_name = "interactive_scan_trigger" + arg_input = {"device": ScanArgType.DEVICE} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.interactive_scan_trigger() + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + def _get_scan_motors(self): + self.scan_motors = list(self.caller_args.keys()) + + def _at_each_point(self, ind=None, pos=None): + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + + +[docs] + def run(self): + yield from self.open_scan() + yield from self._at_each_point() + yield from self.close_scan() + + + + + +[docs] +class CloseInteractiveScan(ScanComponent): + scan_name = "close_interactive_scan" + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + Parameters +Next, we need to define the scan parameters. In exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.close_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1) + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + +[docs] + def run(self): + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + yield from self.stubs.close_scan_def() + diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels_details.py b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels_details.py new file mode 100644 index 0000000..aa40aaa --- /dev/null +++ b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels_details.py @@ -0,0 +1,120 @@ +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd import Component as Cpt + + +from phoenix_bec.local_scripts.Examples.my_ophyd import Device,EpicsMotor, EpicsSignal, EpicsSignalRO +from phoenix_bec.local_scripts.Examples.my_ophyd import Component as Cpt + +############################################ +# +# KEEP my_ophyd zipped to avoid chaos local version does not run +# so this is rather useless +# +########################################## + + + +#option I via direct acces to classes + +def print_dic(clname,cl): + print('') + print('-------- ',clname) + for ii in cl.__dict__: + if '_' not in ii: + + try: + print(ii,' ---- ',cl.__getattribute__(ii)) + except: + print(ii) + + + + +ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') +ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') +DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') +SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') +CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + + +#prefix='XXXX:' +y_cpt = Cpt(EpicsMotor, 'ScanX') +# Option 2 using component + +device_ins=Device('X07MB-ES-MA1:',name=('device_name')) +print(' initialzation of device_in=Device(X07MB-ES-MA1:,name=(device_name)') +print('device_ins.__init__') +print(device_ins.__init__) +print_dic('class Device',Device) +print_dic('instance of device device_ins',device_ins) + + +print(' ') +print('DEFINE class StageXY... prefix variable not defined ') + +EpicsMotor, 'ScanY' +""" +class MyCpt(typing.Generic[K]): + + def __init__( + self, + cls: Type[K], + suffix: Optional[str] = None, + *, + lazy: Optional[bool] = None, + trigger_value: Optional[Any] = None, + add_prefix: Optional[Sequence[str]] = None, + doc: Optional[str] = None, + kind: Union[str, Kind] = Kind.normal, + **kwargs, + ): + self.attr = None # attr is set later by the device when known + self.cls = cls + self.kwargs = kwargs + #self.lazy = lazy if lazy is not None else self.lazy_default + self.suffix = suffix + self.doc = doc + self.trigger_value = trigger_value # TODO discuss + self.kind = Kind[kind.lower()] if isinstance(kind, str) else Kind(kind) + if add_prefix is None: + add_prefix = ("suffix", "write_pv") + self.add_prefix = tuple(add_prefix) + self._subscriptions = collections.defaultdict(list) + print(' ') +""" + + +in Device we have this class method +Class device(..): +.... + @classmethod + def _initialize_device(cls): +.... + + +class StageXY(Device): + # Here the whole namespace and finctionality + # of Device(Blueskyinterface,Pphydobject) is inherited + # into class StageXY + + x = Cpt(EpicsMotor, 'ScanX') + y = Cpt(EpicsMotor, 'ScanY') + +# end class + + + + +print() +print('init xy_stage, use input parameter from Device and prefix is defined in call ') +xy = StageXY('X07MB-ES-MA1:', name='xy_name') + +print_dic('class StageXY',StageXY) +print_dic('instance of StageXY',xy) + + + + +#print('xy.x.prefix') +#print(xy.x.prefix) +xy.__dict__ diff --git a/phoenix_bec/local_scripts/README.md b/phoenix_bec/local_scripts/README.md new file mode 100644 index 0000000..3f9b602 --- /dev/null +++ b/phoenix_bec/local_scripts/README.md @@ -0,0 +1,9 @@ +# purpose of directory + +This diretory is for scripts, test etc. which are not loaded into the bec-server. + +For now we keep it in the phoenix_bec structure, but for operation, such files should be located out side of the +bec_phoenix plugin. + + +TO avoid poading of these files, there should be no files called __init__.py anywhere in the directory tree -- 2.49.1 From 943aa44abe1a4ff87546c3a42e1fa37eaf6c4247 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Wed, 21 Aug 2024 17:54:57 +0200 Subject: [PATCH 04/14] next some base classed copied and commented to local scripts --- .../startup/post_startup.py | 2 +- .../device_configs/phoenix_devices.yaml | 24 +++++++++++++++ phoenix_bec/devices/__init__.py | 1 + phoenix_bec/devices/falcon_phoenix_no_hdf5.py | 20 ++++--------- phoenix_bec/devices/phoenix_trigger.py | 15 ++++------ .../DefiningEpics_Channels.py | 29 +++++++++---------- phoenix_bec/local_scripts/README.txt | 8 ----- phoenix_bec/scripts/phoenix.py | 12 +------- 8 files changed, 52 insertions(+), 59 deletions(-) delete mode 100644 phoenix_bec/local_scripts/README.txt diff --git a/phoenix_bec/bec_ipython_client/startup/post_startup.py b/phoenix_bec/bec_ipython_client/startup/post_startup.py index ffa0da2..385e9e2 100644 --- a/phoenix_bec/bec_ipython_client/startup/post_startup.py +++ b/phoenix_bec/bec_ipython_client/startup/post_startup.py @@ -154,7 +154,7 @@ print(' 1) phoenix_server = PhoenixBL() ... takes code from server version ' print('SERBVR VERSION DOES NOT WORK ANYMORE ') print('FOLDER SCRUIPT SEEMS TO BE NON_STANDARD!!!!!!! ') -#phoenix_server=PhoenixBL() +phoenix_server=PhoenixBL() print(' 2) phoenix=PH.PhoenixBL() ... on inpython shell only! (for debugging)') diff --git a/phoenix_bec/device_configs/phoenix_devices.yaml b/phoenix_bec/device_configs/phoenix_devices.yaml index 241f85a..5218499 100644 --- a/phoenix_bec/device_configs/phoenix_devices.yaml +++ b/phoenix_bec/device_configs/phoenix_devices.yaml @@ -5,9 +5,33 @@ # ##################################################### + +#################### +# +# TRIGGER/Delay +# +################### + +phoenix_trigger: + description: Trigger + deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger + deviceConfig: + prefix: 'X07MB-OP2:' + deviceTags: + - phoenix + - trigger + - phoenix_devices.yaml + onFailure: buffer + enabled: true + readoutPriority: async + softwareTrigger: false + +############################ # # MOTORS ES1 # +############################ + ScanX: readoutPriority: baseline diff --git a/phoenix_bec/devices/__init__.py b/phoenix_bec/devices/__init__.py index e69de29..9ba919d 100644 --- a/phoenix_bec/devices/__init__.py +++ b/phoenix_bec/devices/__init__.py @@ -0,0 +1 @@ +from PhoenixTrigger import PhoenixTrigger \ No newline at end of file diff --git a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py index f4f5cbd..371abcd 100644 --- a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py @@ -1,6 +1,6 @@ # # -# changes version for PHOENIX WITHOUT HDF5 plugin to be revised/renamed... +# changes version for PHOENIX WITHOUT HDF5 plugin to be revised/renamed... # # @@ -82,7 +82,7 @@ class FalconHDF5Plugins(Device): Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. """ - """ ---------------------------------------------------------------------------- + """ ---------------------------------------------------------------------------- capture = Cpt(EpicsSignalWithRBV, "Capture") enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config") xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config") @@ -186,7 +186,7 @@ class FalconSetup(CustomDetectorMixin): def prepare_data_backend(self) -> None: """Prepare data backend for acquisition""" - w=9 + w=9 """ -------------------------------------------------------------- self.parent.filepath.set( self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") @@ -302,17 +302,8 @@ class FalconSetup(CustomDetectorMixin): """ mapping = int(mapping_mode) trigger = trigger_source - self.parent.collect_mode.put(mapping) - self.parent.pixel_advance_mode.put(trigger) - self.parent.ignore_gate.put(ignore_gate) - - -class FalconcSAXS(PSIDetectorBase): - """ - Falcon Sitoro detector for CSAXS - - Parent class: PSIDetectorBase - + self.parent.collect_mode.put(m +prefix ---- X07MB-ES-MA1: class attributes: custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, inherits from CustomDetectorMixin @@ -325,6 +316,7 @@ class FalconcSAXS(PSIDetectorBase): # Specify which functions are revealed to the user in BEC client USER_ACCESS = ["describe"] +prefix ---- X07MB-ES-MA1: # specify Setup class custom_prepare_cls = FalconSetup diff --git a/phoenix_bec/devices/phoenix_trigger.py b/phoenix_bec/devices/phoenix_trigger.py index d457eb6..0b83026 100644 --- a/phoenix_bec/devices/phoenix_trigger.py +++ b/phoenix_bec/devices/phoenix_trigger.py @@ -12,8 +12,6 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorB from bec_lib import bec_logger, messages from bec_lib.endpoints import MessageEndpoints -import time - logger = bec_logger.logger DETECTOR_TIMEOUT = 5 @@ -35,7 +33,7 @@ class PhoenixTriggerSetup(CustomDetectorMixin): super().__init__(*args, parent=parent, **kwargs) self._counter = 0 - WW + WW def on_stage(self): # is this called on each point in scan or just before scan ??? print('on stage') @@ -48,13 +46,12 @@ class PhoenixTriggerSetup(CustomDetectorMixin): cycles=self.parent.smpl.put(2) time.sleep(0.5) cycles=self.parent.total_cycles.put(cycles) - + logger.success('PhoenixTrigger on stage') def on_trigger(self): - + print('on_trigger') self.parent.start_smpl.put(1) - time.sleep(0.05) # use blocking logger.success('PhoenixTrigger on_trigger') return self.wait_with_status( @@ -62,7 +59,7 @@ class PhoenixTriggerSetup(CustomDetectorMixin): - + # logger.success(' PhoenixTrigger on_trigger complete ') # if success: @@ -74,7 +71,7 @@ class PhoenixTriggerSetup(CustomDetectorMixin): def on_complete(self): - + print('on_complete') timeout =10 @@ -99,7 +96,7 @@ class PhoenixTriggerSetup(CustomDetectorMixin): - + def on_stop(self): logger.success(' PhoenixTrigger on_stop ') diff --git a/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py index 840cffb..75eba1f 100644 --- a/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py +++ b/phoenix_bec/local_scripts/Examples/Learn_about_ophyd/DefiningEpics_Channels.py @@ -38,35 +38,32 @@ print_dic('instance of device device_ins',device_ins) print(' ') print('DEFINE class StageXY... prefix variable not defined ') + class StageXY(Device): - - print('in StageXY') - try: - print('prefix',prefix) - except: - print('prefix not defined') + # Here the whole namespace and finctionality + # of Device(Blueskyinterface,Pphydobject) is inherited + # into class StageXY using Device + # device requires as input parameters the prefix + # in the initialization of Cpt there is some magic + # hard to understand, moist likely using calss methods.. + # x = Cpt(EpicsMotor, 'ScanX') y = Cpt(EpicsMotor, 'ScanY') +# end class -class StageXY2(Device): - #def __init__(self,prefix,name=''): - # print('in StageXY') - # print(prefix) - ##print('prefix',prefix) - - x = Cpt(EpicsMotor, 'ScanX') - y = Cpt(EpicsMotor, 'ScanY') print() -print('init xy_stage, use input parameter from Device and prefix is defined') -xy_stage = StageXY('X07MB-ES-MA1:', name='stage') +print('init xy_stage, use input parameter from Device and prefix is defined in call ') +xy_stage = StageXY('X07MB-ES-MA1:', name='stageXXX') print_dic('class StageXY',StageXY) print_dic('instance of StageXY',xy_stage) + + ############################################# # This is basic bluesky # Epics motor def seems to use init params in device, whcih are diff --git a/phoenix_bec/local_scripts/README.txt b/phoenix_bec/local_scripts/README.txt deleted file mode 100644 index 6d32ff0..0000000 --- a/phoenix_bec/local_scripts/README.txt +++ /dev/null @@ -1,8 +0,0 @@ -This diretory is for scripts, test etc. which are not loaded into the server. - -Hence no directory should contain a file named -__init__.py - - -For now we keep it in the phoenix_bec structure, but for operation, such files should be located out side of the -bec_phoenix plugin. diff --git a/phoenix_bec/scripts/phoenix.py b/phoenix_bec/scripts/phoenix.py index 4d3b4f2..ab1f1cd 100644 --- a/phoenix_bec/scripts/phoenix.py +++ b/phoenix_bec/scripts/phoenix.py @@ -39,21 +39,10 @@ class PhoenixBL(): init PhoenixBL() in phoenix_bec/scripts """ - - import os print('..... init PhoenixBL from phoenix_bec/scripts/phoenix.py') - #from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO - #from ophyd import Component as Cpt - #self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') - #self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') - #self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') - #self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") - #self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') - #self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') - # load local configuration self.path_scripts_local = '/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/phoenix_bec/local_scripts/' @@ -90,3 +79,4 @@ class PhoenixBL(): def show_phoenix_setup(self): print(self.path_phoenix_bec) os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') + -- 2.49.1 From 6572a71f3b7c876ff26105cb17ad5da502cd4cc6 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Wed, 21 Aug 2024 17:58:19 +0200 Subject: [PATCH 05/14] minor change --- .../Documentation/Base_Classes/BAse_CLASS_ScanBase.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt index b34195f..f4a3372 100644 --- a/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt @@ -76,6 +76,7 @@ Key functions in ScanBase during step by step scanning are self.point_id += 1 +QUESTION: WHAT ARE THE GROUPS AND WHERE ARE THEY DEFINED ?????? -- 2.49.1 From c77f3594519b26e418be9827260351bdabdeff6e Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 13:37:46 +0200 Subject: [PATCH 06/14] Add magic to post_startup.py to restart bec server from command line, first version of TTL Trigger device --- .../startup/post_startup.py | 5 + .../device_configs/phoenix_devices.yaml | 16 +- phoenix_bec/devices/__init__.py | 2 +- phoenix_bec/devices/falcon_phoenix_no_hdf5.py | 2 +- phoenix_bec/devices/phoenix_trigger.py | 189 +- phoenix_bec/devices/phoenix_trigger.py~ | 182 ++ phoenix_bec/devices/xmap_phoenix_no_hdf5.py | 17 +- .../BASE_CLASS_PSI_detector_base.txt | 435 ++++ ...E_CLASS_scans.txt => BASE_CLASS_Scans.txt} | 3 +- .../Base_Classes/BAse_CLASS_ScanBase.txt~ | 2025 +++++++++++++++++ phoenix_bec/local_scripts/PhoenixTemplate.py | 18 +- phoenix_bec/local_scripts/README.md~ | 8 + phoenix_bec/scans/__init__.py | 2 +- ...{phoenix_line_scan.py => phoenix_scans.py} | 111 +- phoenix_bec/scripts/phoenix.py | 5 +- 15 files changed, 2895 insertions(+), 125 deletions(-) create mode 100644 phoenix_bec/devices/phoenix_trigger.py~ create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_PSI_detector_base.txt rename phoenix_bec/local_scripts/Documentation/Base_Classes/{BASE_CLASS_scans.txt => BASE_CLASS_Scans.txt} (99%) create mode 100644 phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt~ create mode 100644 phoenix_bec/local_scripts/README.md~ rename phoenix_bec/scans/{phoenix_line_scan.py => phoenix_scans.py} (71%) diff --git a/phoenix_bec/bec_ipython_client/startup/post_startup.py b/phoenix_bec/bec_ipython_client/startup/post_startup.py index 385e9e2..6ad6153 100644 --- a/phoenix_bec/bec_ipython_client/startup/post_startup.py +++ b/phoenix_bec/bec_ipython_client/startup/post_startup.py @@ -36,6 +36,7 @@ to setup the prompts. # pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module import time as tt import sys +import os from IPython.core.magic import register_line_magic @@ -132,6 +133,10 @@ def ph_load_config(line): print('elapsed time:', tt.time()-t0) #enddef +@register_line_magic +def ph_restart_bec_server(line): + os.system('bec-server restart') + os.system('gnome-terminal --geometry 120X50 -- bash -c "bec-server attach; exec bash"') ##@register_line_magic #def ph_post_startup(line): diff --git a/phoenix_bec/device_configs/phoenix_devices.yaml b/phoenix_bec/device_configs/phoenix_devices.yaml index 5218499..c9c57fc 100644 --- a/phoenix_bec/device_configs/phoenix_devices.yaml +++ b/phoenix_bec/device_configs/phoenix_devices.yaml @@ -3,23 +3,15 @@ # phoenix standard devices (motors) # # -##################################################### - - -#################### -# -# TRIGGER/Delay -# -################### - -phoenix_trigger: - description: Trigger +####################################################: +TTL: + description: PHOENIX TTL trigger deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger deviceConfig: prefix: 'X07MB-OP2:' deviceTags: - phoenix - - trigger + - TTL Trigger - phoenix_devices.yaml onFailure: buffer enabled: true diff --git a/phoenix_bec/devices/__init__.py b/phoenix_bec/devices/__init__.py index 9ba919d..7322b2b 100644 --- a/phoenix_bec/devices/__init__.py +++ b/phoenix_bec/devices/__init__.py @@ -1 +1 @@ -from PhoenixTrigger import PhoenixTrigger \ No newline at end of file +from .phoenix_trigger import PhoenixTrigger \ No newline at end of file diff --git a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py index 371abcd..443e4e5 100644 --- a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py @@ -256,7 +256,7 @@ class FalconSetup(CustomDetectorMixin): #): # # Retry stop detector and wait for remaining time # raise FalconTimeoutError( - # f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + # f"Failed to stop detector, timeou t with state {signal_conditions[0][0]}" # ) def stop_detector_backend(self) -> None: diff --git a/phoenix_bec/devices/phoenix_trigger.py b/phoenix_bec/devices/phoenix_trigger.py index 0b83026..8d1ea1d 100644 --- a/phoenix_bec/devices/phoenix_trigger.py +++ b/phoenix_bec/devices/phoenix_trigger.py @@ -1,3 +1,5 @@ +import time + from ophyd import ( ADComponent as ADCpt, Device, @@ -18,10 +20,6 @@ DETECTOR_TIMEOUT = 5 #class PhoenixTriggerError(Exce start_csmpl=Cpt(EPicsSignal,'START-CSMPL') # cont on / off - - - - class PhoenixTriggerSetup(CustomDetectorMixin): """ This defines the PHOENIX trigger setup. @@ -29,11 +27,38 @@ class PhoenixTriggerSetup(CustomDetectorMixin): """ + #self.acquire = self.parent.smpl.put(1) + #self.continuous_sampling_on = parent.start_cmpl.put(1) + #self.continuous_sampling_off = self.parent.start_cmpl.put(0) + def __init__(self, *args, parent:Device = None, **kwargs): super().__init__(*args, parent=parent, **kwargs) self._counter = 0 - WW + + def on_acquire(self): + self.parent.smpl.put(1) + print('on_aquire') + + + def on_cont_sample_on(self): + self.parent.start_csmpl.put(1) + print('on_cont_sample_on') + + def on_cont_sample_off(self): + self.parent.start_csmpl.put(0) + print('on_cont_sample_off') + + + def on_done(self): + done = self.parent.smpl_done.get() + return done + + + def on_dwell(self,t): + " calculate cycles from time in sec " + cycles=self.parent.total_cycles.put(0)*5 + def on_stage(self): # is this called on each point in scan or just before scan ??? print('on stage') @@ -43,23 +68,26 @@ class PhoenixTriggerSetup(CustomDetectorMixin): time.sleep(0.05) cycles=self.parent.total_cycles.put(0) time.sleep(0.05) - cycles=self.parent.smpl.put(2) + cycles=self.parent.smpl.put(1) time.sleep(0.5) cycles=self.parent.total_cycles.put(cycles) - logger.success('PhoenixTrigger on stage') - def on_trigger(self): - print('on_trigger') - self.parent.start_smpl.put(1) - logger.success('PhoenixTrigger on_trigger') - - return self.wait_with_status( - [(self.parent.smpl_done.get, 1)]) - + + #def on_trigger(self): + # print('on_trigger') + # self.parent.start_smpl.put(1) + # logger.success('PhoenixTrigger on_trigger') + # + # return self.wait_with_status( + # [(self.parent.smpl_done.get, 1)]) + + + + # logger.success(' PhoenixTrigger on_trigger complete ') # if success: @@ -70,67 +98,57 @@ class PhoenixTriggerSetup(CustomDetectorMixin): - def on_complete(self): - print('on_complete') - timeout =10 + #def on_complete(self): + # print('on_complete') + # timeout =10 - logger.success('XXXX complete %d XXXX' % success) + # logger.success('XXXX complete %d XXXX' % success) - success = self.wait_for_signals( - [ - (self.parent.smpl_done.get, 0)) - ], - timeout, - check_stopped=True, - all_signals=True - ) + # success = self.wait_for_signals( + # [ + # (self.parent.smpl_done.get, 0) + # ], + # timeout, + # check_stopped=True, + # all_signals=True + # ) - if success: - status.set_finished() - else: - status.set_exception(TimeoutError()) - return status + # if success: + # status.set_finished() + # else: + # status.set_exception(TimeoutError()) + # return status - - def on_stop(self): - logger.success(' PhoenixTrigger on_stop ') - - self.parent.csmpl.put(1) - logger.success(' PhoenixTrigger on_stop finished ') - - def on_unstage(self): - logger.success(' PhoenixTrigger on_unstage ') - self.parent.csmpl.put(1) - self.parent.smpl.put(1) - logger.success(' PhoenixTrigger on_unstage finished ') - + # hoenixTrigger on_unstage ') + # self.parent.csmpl.put(1) + # self.parent.smpl.put(1) + # logger.success(' PhoenixTrigger on_unstage finished ') + #def on_trigger(): + # print('on_trigger') class PhoenixTrigger(PSIDetectorBase): """ + Docstring: + + Class for PHOENIX TTL hardware trigger + Parent class: PSIDetectorBase class attributes: - custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + custom_prepare_cls (PhoenixTriggerSetup) : Custom setup for TTL trigger at PHOENIX inherits from CustomDetectorMixin in __init__ of PSIDetecor bases class is initialized self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) - PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector - dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector - mca (EpicsMCARecord) : MCA parameters for XMAP detector - hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector - MIN_READOUT (float) : Minimum readout time for the detector - - The class PhoenixTrigger is the class to be called via yaml configuration file the input arguments are defined by PSIDetectorBase, and need to be given in the yaml configuration file. @@ -138,6 +156,8 @@ class PhoenixTrigger(PSIDetectorBase): use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. + + PSIDetectorBase( prefix='', *,Q @@ -147,12 +167,6 @@ class PhoenixTrigger(PSIDetectorBase): device_manager=None, **kwargs, ) - Docstring: - Abstract base class for SLS detectors - - Class attributes: - custom_prepare_cls (object): class for custom prepare logic (BL specific) - Args: prefix (str): EPICS PV prefix for component (optional) name (str): name of the device, as will be reported via read() @@ -167,13 +181,66 @@ class PhoenixTrigger(PSIDetectorBase): File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py Type: type Subclasses: EpicsSignal + """ + ################################################################## + # Specify which functions are revealed to the user in BEC client + # only a set of predefined functions will be visible in dev.TTL + # The Variable USER_ACCESS contains an ascii list of functions which will be + # visible in dev.TTL as well + # Alternatively one couls also create 2nd instance of PhoenixTrigger, + # which is probably not ideal + + USER_ACCESS = ["a_acquire" + ,"a_cont_sample_on" + ,"a_cont_sample_off" + ,"prefix" + ,"a_done"] + + ##################################################################### + # specify Setup class into variable custom_prepare_cls + # in __init__ of PSIDetectorBase will the initialzed by + # self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + # making the instance of PSIDetectorBase availble in functions + custom_prepare_cls = PhoenixTriggerSetup - - start_csmpl = Cpt(EpicsSignal,'START-CSMPL') # cont on / off + #############################################################3 + # Now use component to provide channel access + # when PhoenixTrigger is initialized, the parameters of the base class are + # inherided, most notable prefix, which is here X07MB-OP2: + # The input of Component=Cpt is Cpt(deviceClass,suffix) + # if Cpt is used in a class, which has interited Device, here via: + # (Here PhoenixTrigger <-- PSIDetectorBase <- Device + # the Cpt will construct - magically- the Epics channel name + # EpicsPV = prefix+suffix, + # for example + # 'X07MB-OP2:' + 'START-CSMPL' -> 'X07MB-OP2:' + 'START-CSMPL' + # + start_csmpl = Cpt(EpicsSignal, 'START-CSMPL') # cont on / off intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set + smpl = Cpt(EpicsSignal,'SMPL') # start sampling --> aquire smpl_done = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done + + # link to reasonable names + # start with a_ to see functions quicklz in listing + # + # + def a_acquire(self): + self.custom_prepare.on_acquire() + + def a_cont_sample_on(self): + self.custom_prepare.on_cont_sample_on() + + def a_cont_sample_off(self): + self.custom_prepare.on_cont_sample_off() + + def a_done(self): + done=self.custom_prepare.on_done() + return done + + def a_dwell(self): + self.custom_prepare.on_dwell() \ No newline at end of file diff --git a/phoenix_bec/devices/phoenix_trigger.py~ b/phoenix_bec/devices/phoenix_trigger.py~ new file mode 100644 index 0000000..d457eb6 --- /dev/null +++ b/phoenix_bec/devices/phoenix_trigger.py~ @@ -0,0 +1,182 @@ +from ophyd import ( + ADComponent as ADCpt, + Device, + DeviceStatus, +) + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO + +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from bec_lib import bec_logger, messages +from bec_lib.endpoints import MessageEndpoints + +import time + +logger = bec_logger.logger + +DETECTOR_TIMEOUT = 5 + +#class PhoenixTriggerError(Exce start_csmpl=Cpt(EPicsSignal,'START-CSMPL') # cont on / off + + + + + +class PhoenixTriggerSetup(CustomDetectorMixin): + """ + This defines the PHOENIX trigger setup. + + + """ + + def __init__(self, *args, parent:Device = None, **kwargs): + super().__init__(*args, parent=parent, **kwargs) + self._counter = 0 + + WW + def on_stage(self): + # is this called on each point in scan or just before scan ??? + print('on stage') + self.parent.start_csmpl.put(0) + time.sleep(0.05) + cycles=self.parent.total_cycles.get() + time.sleep(0.05) + cycles=self.parent.total_cycles.put(0) + time.sleep(0.05) + cycles=self.parent.smpl.put(2) + time.sleep(0.5) + cycles=self.parent.total_cycles.put(cycles) + + logger.success('PhoenixTrigger on stage') + + def on_trigger(self): + + self.parent.start_smpl.put(1) + time.sleep(0.05) # use blocking + logger.success('PhoenixTrigger on_trigger') + + return self.wait_with_status( + [(self.parent.smpl_done.get, 1)]) + + + + +# logger.success(' PhoenixTrigger on_trigger complete ') + +# if success: +# status.set_finished() +# else: +# status.set_exception(TimeoutError()) +# return status + + + + def on_complete(self): + + timeout =10 + + + logger.success('XXXX complete %d XXXX' % success) + + success = self.wait_for_signals( + [ + (self.parent.smpl_done.get, 0)) + ], + timeout, + check_stopped=True, + all_signals=True + ) + + + + if success: + status.set_finished() + else: + status.set_exception(TimeoutError()) + return status + + + + + def on_stop(self): + logger.success(' PhoenixTrigger on_stop ') + + self.parent.csmpl.put(1) + logger.success(' PhoenixTrigger on_stop finished ') + + def on_unstage(self): + logger.success(' PhoenixTrigger on_unstage ') + self.parent.csmpl.put(1) + self.parent.smpl.put(1) + logger.success(' PhoenixTrigger on_unstage finished ') + + + + + +class PhoenixTrigger(PSIDetectorBase): + + """ + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + in __init__ of PSIDetecor bases + class is initialized + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector + mca (EpicsMCARecord) : MCA parameters for XMAP detector + hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector + MIN_READOUT (float) : Minimum readout time for the detector + + + The class PhoenixTrigger is the class to be called via yaml configuration file + the input arguments are defined by PSIDetectorBase, + and need to be given in the yaml configuration file. + To adress chanels such as 'X07MB-OP2:SMPL-DONE': + + use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. + + PSIDetectorBase( + prefix='', + *,Q + name, + kind=None, + parent=None, + device_manager=None, + **kwargs, + ) + Docstring: + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + parent (object): instance of the parent device + device_manager (object): bec device manager + **kwargs: keyword arguments + File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py + Type: type + Subclasses: EpicsSignal + """ + + custom_prepare_cls = PhoenixTriggerSetup + + + start_csmpl = Cpt(EpicsSignal,'START-CSMPL') # cont on / off + intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up + total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set + smpl_done = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done + diff --git a/phoenix_bec/devices/xmap_phoenix_no_hdf5.py b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py index 57b0d19..87bb9f1 100644 --- a/phoenix_bec/devices/xmap_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py @@ -310,10 +310,7 @@ class XMAPSetup(CustomDetectorMixin): class XMAPphoenix(PSIDetectorBase): """MCA XMAP detector for phoenix - - Parent class: PSIDetectorBase - - class attributes: + custom_prepare_cls (XMAPSetu custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, inherits from CustomDetectorMixin in __init__ of PSIDetecor base @@ -337,9 +334,9 @@ class XMAPphoenix(PSIDetectorBase): dxp = Cpt(EpicsDXPXMAP, "dxp1:") mca1 = Cpt(EpicsMCARecord, "mca1") - mca2 = Cpt(EpicsMCARecord, "mca2") - mca3 = Cpt(EpicsMCARecord, "mca3") - mca4 = Cpt(EpicsMCARecord, "mca4") + #mca2 = Cpt(EpicsMCARecord, "mca2") + #mca3 = Cpt(EpicsMCARecord, "mca3") + #mca4 = Cpt(EpicsMCARecord, "mca4") print('load hdf5') #hdf5 = Cpt(XMAPHDF5Plugins, "HDF1:") @@ -362,5 +359,11 @@ class XMAPphoenix(PSIDetectorBase): auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer") pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer") pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") + + #nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") print('DONE connecton chanels in XMAPphoenix') + + + def aaaa(self): + print('aaaa') diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_PSI_detector_base.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_PSI_detector_base.txt new file mode 100644 index 0000000..fe67b35 --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_PSI_detector_base.txt @@ -0,0 +1,435 @@ +FILE ophyd_devices/ophy_devices/devices/interfaces/base_classes + + +"""This module contains the base class for SLS detectors. We follow the approach to integrate +PSI detectors into the BEC system based on this base class. The base class is used to implement +certain methods that are expected by BEC, such as stage, unstage, trigger, stop, etc... +We use composition with a custom prepare class to implement BL specific logic for the detector. +The beamlines need to inherit from the Custoon_ +import threading +import time +import traceback + +from bec_lib import messages +from bec_lib.endpoints import MessageEndpoints +from bec_lib.file_utils import FileWriter +from bec_lib.logger import bec_logger +from ophyd import Component, Device, DeviceStatus, Kind +from ophyd.device import Staged + +from ophyd_devices.sim.sim_signals import SetableSignal +from ophyd_devices.utils import bec_utils +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError + +logger = bec_logger.logger + + +class DetectorInitError(Exception): + """Raised when initiation of the device class fails, + due to missing device manager or not started in sim_mode.""" + + +class CustomDetectorMixin: + """ + Mixin class for custom detector logic + + This class is used to implement BL specific logic for the detector. + It is used in the PSIDetectorBase class. + + For the integration of a new detector, the following functions should + help with integrating functionality, but additional ones can be added. + + Check PSIDetectorBase for the functions that are called during relevant function calls of + stage, unstage, trigger, stop and _init. + """ + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + self.parent = parent + + def on_init(self) -> None: + """ + Init sequence for the detector + """ + + def on_stage(self) -> None: + """ + Specify actions to be executed during stage in preparation for a scan. + self.parent.scaninfo already has all current parameters for the upcoming scan. + + In case the backend service is writing data on disk, this step should include publishing + a file_event and file_message to BEC to inform the system where the data is written to. + + IMPORTANT: + It must be safe to assume that the device is ready for the scan + to start immediately once this function is finished. + """ + + def on_unstage(self) -> None: + """ + Specify actions to be executed during unstage. + + This step should include checking if the acqusition was successful, + and publishing the file location and file event message, + with flagged done to BEC. + """ + + def on_stop(self) -> None: + """ + Specify actions to be executed during stop. + This must also set self.parent.stopped to True. + + This step should include stopping the detector and backend service. + """ + + def on_trigger(self) -> None | DeviceStatus: + """ + Specify actions to be executed upon receiving trigger signal. + Return a DeviceStatus object or None + """ + + def on_pre_scan(self) -> None: + """ + Specify actions to be executed right before a scan starts. + + Only use if needed, and it is recommended to keep this function as short/fast as possible. + """ + + def on_complete(self) -> None | DeviceStatus: + """ + Specify actions to be executed when the scan is complete. + + This can for instance be to check with the detector and backend if all data is written succsessfully. + """ + + def publish_file_location(self, done: bool, successful: bool, metadata: dict = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + metadata (dict): additional metadata to publish + """ + if metadata is None: + metadata = {} + + msg = messages.FileMessage( + file_path=self.parent.filepath.get(), + done=done, + successful=successful, + metadata=metadata, + ) + pipe = self.parent.connector.pipeline() + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + def wait_for_signals( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + ) -> bool: + """ + Convenience wrapper to allow waiting for signals to reach a certain condition. + For EPICs PVs, an example usage is pasted at the bottom. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + + Returns: + bool: True if all signals are in the desired state, False if timeout is reached + + >>> Example usage for EPICS PVs: + >>> self.wait_for_signals(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True) + """ + + timer = 0 + while True: + checks = [ + get_current_state() == condition + for get_current_state, condition in signal_conditions + ] + if check_stopped is True and self.parent.stopped is True: + return False + if (all_signals and all(checks)) or (not all_signals and any(checks)): + return True + if timer > timeout: + return False + time.sleep(interval) + timer += interval + + def wait_with_status( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + exception_on_timeout: Exception = None, + ) -> DeviceStatus: + """Utility function to wait for signals in a thread. + Returns a DevicesStatus object that resolves either to set_finished or set_exception. + The DeviceStatus is attached to the parent device, i.e. the detector object inheriting from PSIDetectorBase. + + Usage: + This function should be used to wait for signals to reach a certain condition, especially in the context of + on_trigger and on_complete. If it is not used, functions may block and slow down the performance of BEC. + It will return a DeviceStatus object that is to be returned from the function. Once the conditions are met, + the DeviceStatus will be set to set_finished in case of success or set_exception in case of a timeout or exception. + The exception can be specified with the exception_on_timeout argument. The default exception is a TimeoutError. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + + Returns: + DeviceStatus: DeviceStatus object that resolves either to set_finished or set_exception + """ + if exception_on_timeout is None: + exception_on_timeout = DeviceTimeoutError( + f"Timeout error for {self.parent.name} while waiting for signals {signal_conditions}" + ) + + status = DeviceStatus(self.parent) + + # utility function to wrap the wait_for_signals function + def wait_for_signals_wrapper( + status: DeviceStatus, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool, + interval: float, + all_signals: bool, + exception_on_timeout: Exception, + ): + """Convenient wrapper around wait_for_signals to set status based on the result. + + Args: + status (DeviceStatus): DeviceStatus object to be set + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + """ + try: + result = self.wait_for_signals( + signal_conditions, timeout, check_stopped, interval, all_signals + ) + if result: + status.set_finished() + else: + if self.parent.stopped: + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=DeviceStopError(f"{self.parent.name} was stopped")) + else: + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=exception_on_timeout) + # pylint: disable=broad-except + except Exception as exc: + content = traceback.format_exc() + logger.warning( + f"Error in wait_for_signals in {self.parent.name}; Traceback: {content}" + ) + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=exc) + + thread = threading.Thread( + target=wait_for_signals_wrapper, + args=( + status, + signal_conditions, + timeout, + check_stopped, + interval, + all_signals, + exception_on_timeout, + ), + daemon=True, + ) + thread.start() + return status + + +class PSIDetectorBase(Device): + """ + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> readout ignored for read 'ophydobj.read()' + normal -> readout for read + config -> config parameter for 'ophydobj.read_configuration()' + hinted -> which attribute is readout for read + parent (object): instance of the parent device + device_manager (object): bec device manager + **kwargs: keyword arguments + """ + + filepath = Component(SetableSignal, value="", kind=Kind.config) + + custom_prepare_cls = CustomDetectorMixin + + def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, parent=parent, **kwargs) + self.stopped = False + self.name = name + self.service_cfg = None + self.scaninfo = None + self.filewriter = None + + if not issubclass(self.custom_prepare_cls, CustomDetectorMixin): + raise DetectorInitError("Custom prepare class must be subclass of CustomDetectorMixin") + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + + if device_manager: + self._update_service_config() + self.device_manager = device_manager + else: + self.device_manager = bec_utils.DMMock() + base_path = kwargs["basepath"] if "basepath" in kwargs else "." + self.service_cfg = {"base_path": os.path.abspath(base_path)} + + self.connector = self.device_manager.connector + self._update_scaninfo() + self._update_filewriter() + self._init() + + def _update_filewriter(self) -> None: + """Update filewriter with service config""" + self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector) + + def _update_scaninfo(self) -> None: + """Update scaninfo from BecScaninfoMixing + This depends on device manager and operation/sim_mode + """ + self.scaninfo = BecScaninfoMixin(self.device_manager) + self.scaninfo.load_scan_metadata() + + def _update_service_config(self) -> None: + """Update service config from BEC service config + + If bec services are not running and SERVICE_CONFIG is NONE, we fall back to the current directory. + """ + # pylint: disable=import-outside-toplevel + from bec_lib.bec_service import SERVICE_CONFIG + + if SERVICE_CONFIG: + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] + return + self.service_cfg = {"base_path": os.path.abspath(".")} + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and set stopped flagged to True if it has.""" + old_scan_id = self.scaninfo.scan_id + self.scaninfo.load_scan_metadata() + if self.scaninfo.scan_id != old_scan_id: + self.stopped = True + + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters""" + self.custom_prepare.on_init() + + def stage(self) -> list[object]: + """ + Stage device in preparation for a scan. + First we check if the device is already staged. Stage is idempotent, + if staged twice it should raise (we let ophyd.Device handle the raise here). + We reset the stopped flag and get the scaninfo from BEC, before calling custom_prepare.on_stage. + + Returns: + list(object): list of objects that were staged + + """ + if self._staged != Staged.no: + return super().stage() + self.stopped = False + self.scaninfo.load_scan_metadata() + self.custom_prepare.on_stage() + return super().stage() + + def pre_scan(self) -> None: + """Pre-scan logic. + + This function will be called from BEC directly before the scan core starts, and should only implement + time-critical actions. Therefore, it should also be kept as short/fast as possible. + I.e. Arming a detector in case there is a risk of timing out. + """ + self.custom_prepare.on_pre_scan() + + def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + # pylint: disable=assignment-from-no-return + status = self.custom_prepare.on_trigger() + if isinstance(status, DeviceStatus): + return status + return super().trigger() + + def complete(self) -> None: + """Complete the acquisition, called from BEC. + + This function is called after the scan is complete, just before unstage. + We can check here with the data backend and detector if the acquisition successfully finished. + + Actions are implemented in custom_prepare.on_complete since they are beamline specific. + """ + # pylint: disable=assignment-from-no-return + status = self.custom_prepare.on_complete() + if isinstance(status, DeviceStatus): + return status + status = DeviceStatus(self) + status.set_finished() + return status + + def unstage(self) -> list[object]: + """ + Unstage device after a scan. + + We first check if the scanID has changed, thus, the scan was unexpectedly interrupted but the device was not stopped. + If that is the case, the stopped flag is set to True, which will immediately unstage the device. + + Custom_prepare.on_unstage is called to allow for BL specific logic to be executed. + + Returns: + list(object): list of objects that were unstaged + """ + self.check_scan_id() + self.custom_prepare.on_unstage() + self.stopped = False + return super().unstage() + + def stop(self, *, success=False) -> None: + """ + Stop the scan, with camera and file writer + + """ + self.custom_prepare.on_stop() + super().stop(success=success) + self.stopped = True diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_Scans.txt similarity index 99% rename from phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt rename to phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_Scans.txt index 177b228..ed0ced0 100644 --- a/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_scans.txt +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BASE_CLASS_Scans.txt @@ -13,8 +13,7 @@ from copy import deepcopy from typing import TYPE_CHECKING, Dict, Literal from toolz import partition -from typeguard import typechecked - +from typeguard import typecheck from bec_lib import messages from bec_lib.bec_errors import ScanAbortion from bec_lib.client import SystemConfig diff --git a/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt~ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt~ new file mode 100644 index 0000000..c841ee1 --- /dev/null +++ b/phoenix_bec/local_scripts/Documentation/Base_Classes/BAse_CLASS_ScanBase.txt~ @@ -0,0 +1,2025 @@ +https://bec.readthedocs.io/en/latest/api_reference/_autosummary/bec_server.scan_server.scans.ScanBase.html#bec_server.scan_server.scans.ScanBase + + +Sequence of events: + + + read_scan_motors + + prepare_positions + + _calculate_positions + + _optimize_trajectory + + _set_position_offset + + _check_limits + + open_scan + + stage + + run_baseline_reading + + pre_scan + + scan_core + + finalize + + unstage + + cleanup + + + +class ScanBase(*args, device_manager: DeviceManagerBase +| None = None, parameter: dict \ +| None = None, exp_time: float = 0, readout_time: float = 0, acquisition_config: dict +| None = None, settling_time: float = 0, relative: bool = False, burst_at_each_point: int = 1, +frames_per_trigger: int = 1, optim_trajectory: Literal['corridor', None] +| None = None, monitored: list | None = None, metadata: dict | None = None, **kwargs) + + + +Methods + + + + + + + + + + + + + + + + + + + +sourece code in bec_server.scan_server.scans + + + +from __future__ import annotations + +import ast +import enum +import threading +import time +import uuid +from abc import ABC, abstractmethod +from typing import Any, Literal + +import numpy as np + +from bec_lib.device import DeviceBase +from bec_lib.devicemanager import DeviceManagerBase +from bec_lib.endpoints import MessageEndpoints +from bec_lib.logger import bec_logger + +from .errors import LimitError, ScanAbortion +from .path_optimization import PathOptimizerMixin +from .scan_stubs import ScanStubs + +logger = bec_logger.logger + + + +[docs] +class ScanArgType(str, enum.Enum): + DEVICE = "device" + FLOAT = "float" + INT = "int" + BOOL = "boolean" + STR = "str" + LIST = "list" + DICT = "dict" + + + + +[docs] +def unpack_scan_args(scan_args: dict[str, Any]) -> list: + """unpack_scan_args unpacks the scan arguments and returns them as a tuple. + + Args: + scan_args (dict[str, Any]): scan arguments + + Returns: + list: list of arguments + """ + args = [] + if not scan_args: + return args + if not isinstance(scan_args, dict): + return scan_args + for cmd_name, cmd_args in scan_args.items(): + args.append(cmd_name) + args.extend(cmd_args) + return args + + + + +[docs] +def get_2D_raster_pos(axis, snaked=True): + """get_2D_raster_post calculates and returns the positions for a 2D + + snaked==True: + ->->->->- + -<-<-<-<- + ->->->->- + snaked==False: + ->->->->- + ->->->->- + ->->->->- + + Args: + axis (list): list of positions for each axis + snaked (bool, optional): If true, the positions will be calculcated for a snake scan. Defaults to True. + + Returns: + array: calculated positions + """ + + x_grid, y_grid = np.meshgrid(axis[0], axis[1]) + if snaked: + y_grid.T[::2] = np.fliplr(y_grid.T[::2]) + x_flat = x_grid.T.ravel() + y_flat = y_grid.T.ravel() + positions = np.vstack((x_flat, y_flat)).T + return positions + + + +# pylint: disable=too-many-arguments + +[docs] +def get_fermat_spiral_pos( + m1_start, m1_stop, m2_start, m2_stop, step=1, spiral_type=0, center=False +): + """get_fermat_spiral_pos calculates and returns the positions for a Fermat spiral scan. + + Args: + m1_start (float): start position motor 1 + m1_stop (float): end position motor 1 + m2_start (float): start position motor 2 + m2_stop (float): end position motor 2 + step (float, optional): Step size. Defaults to 1. + spiral_type (float, optional): Angular offset in radians that determines the shape of the spiral. + A spiral with spiral_type=2 is the same as spiral_type=0. Defaults to 0. + center (bool, optional): Add a center point. Defaults to False. + + Returns: + array: calculated positions in the form [[m1, m2], ...] + """ + positions = [] + phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2.0) + spiral_type * np.pi + + start = int(not center) + + length_axis1 = abs(m1_stop - m1_start) + length_axis2 = abs(m2_stop - m2_start) + n_max = int(length_axis1 * length_axis2 * 3.2 / step / step) + + for ii in range(start, n_max): + radius = step * 0.57 * np.sqrt(ii) + if abs(radius * np.sin(ii * phi)) > length_axis1 / 2: + continue + if abs(radius * np.cos(ii * phi)) > length_axis2 / 2: + continue + positions.extend([(radius * np.sin(ii * phi), radius * np.cos(ii * phi))]) + return np.array(positions) + + + + +[docs] +def get_round_roi_scan_positions(lx: float, ly: float, dr: float, nth: int, cenx=0, ceny=0): + """ + get_round_roi_scan_positions calculates and returns the positions for a round scan in a rectangular region of interest. + + Args: + lx (float): length in x + ly (float): length in y + dr (float): step size + nth (int): number of angles in the inner ring + cenx (int, optional): center in x. Defaults to 0. + ceny (int, optional): center in y. Defaults to 0. + + Returns: + array: calculated positions in the form [[x, y], ...] + """ + positions = [] + nr = 1 + int(np.floor(max([lx, ly]) / dr)) + for ir in range(1, nr + 2): + rr = ir * dr + dth = 2 * np.pi / (nth * ir) + pos = [ + (rr * np.cos(ith * dth) + cenx, rr * np.sin(ith * dth) + ceny) + for ith in range(nth * ir) + if np.abs(rr * np.cos(ith * dth)) < lx / 2 and np.abs(rr * np.sin(ith * dth)) < ly / 2 + ] + positions.extend(pos) + return np.array(positions) + + + + +[docs] +def get_round_scan_positions(r_in: float, r_out: float, nr: int, nth: int, cenx=0, ceny=0): + """ + get_round_scan_positions calculates and returns the positions for a round scan. + + Args: + r_in (float): inner radius + r_out (float): outer radius + nr (int): number of radii + nth (int): number of angles in the inner ring + cenx (int, optional): center in x. Defaults to 0. + ceny (int, optional): center in y. Defaults to 0. + + Returns: + array: calculated positions in the form [[x, y], ...] + + """ + positions = [] + dr = (r_in - r_out) / nr + for ir in range(1, nr + 2): + rr = r_in + ir * dr + dth = 2 * np.pi / (nth * ir) + positions.extend( + [ + (rr * np.sin(ith * dth) + cenx, rr * np.cos(ith * dth) + ceny) + for ith in range(nth * ir) + ] + ) + return np.array(positions, dtype=float) + + + + +[docs] +class RequestBase(ABC): + """ + Base class for all scan requests. + """ + + scan_name = "" + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + gui_args = {} + required_kwargs = [] + return_to_start_after_abort = False + use_scan_progress_report = False + + def __init__( + self, + *args, + device_manager: DeviceManagerBase = None, + monitored: list = None, + parameter: dict = None, + metadata: dict = None, + **kwargs, + ) -> None: + super().__init__() + self._shutdown_event = threading.Event() + self.parameter = parameter if parameter is not None else {} + self.caller_args = self.parameter.get("args", {}) + self.caller_kwargs = self.parameter.get("kwargs", {}) + self.metadata = metadata + self.device_manager = device_manager + self.connector = device_manager.connector + self.DIID = 0 + self.scan_motors = [] + self.positions = [] + self._pre_scan_macros = [] + self._scan_report_devices = None + self._get_scan_motors() + self.readout_priority = { + "monitored": monitored if monitored is not None else [], + "baseline": [], + "on_request": [], + "async": [], + } + self.update_readout_priority() + if metadata is None: + self.metadata = {} + self.stubs = ScanStubs( + connector=self.device_manager.connector, + device_msg_callback=self.device_msg_metadata, + shutdown_event=self._shutdown_event, + ) + + @property + def scan_report_devices(self): + """devices to be included in the scan report""" + if self._scan_report_devices is None: + return self.readout_priority["monitored"] + return self._scan_report_devices + + @scan_report_devices.setter + def scan_report_devices(self, devices: list): + self._scan_report_devices = devices + + def device_msg_metadata(self): + default_metadata = {"readout_priority": "monitored", "DIID": self.DIID} + metadata = {**default_metadata, **self.metadata} + self.DIID += 1 + return metadata + + @staticmethod + def _get_func_name_from_macro(macro: str): + return ast.parse(macro).body[0].name + + +[docs] + def run_pre_scan_macros(self): + """run pre scan macros if any""" + macros = self.device_manager.connector.lrange(MessageEndpoints.pre_scan_macros(), 0, -1) + for macro in macros: + macro = macro.value.strip() + func_name = self._get_func_name_from_macro(macro) + exec(macro) + eval(func_name)(self.device_manager.devices, self) + + + def initialize(self): + self.run_pre_scan_macros() + + def _check_limits(self): + logger.debug("check limits") + for ii, dev in enumerate(self.scan_motors): + low_limit, high_limit = self.device_manager.devices[dev].limits + if low_limit >= high_limit: + # if both limits are equal or low > high, no restrictions ought to be applied + return + for pos in self.positions: + pos_axis = pos[ii] + if not low_limit <= pos_axis <= high_limit: + raise LimitError( + f"Target position {pos} for motor {dev} is outside of range: [{low_limit}," + f" {high_limit}]" + ) + + def _get_scan_motors(self): + if len(self.caller_args) == 0: + return + if self.arg_bundle_size.get("bundle"): + self.scan_motors = list(self.caller_args.keys()) + return + for motor in self.caller_args: + if motor not in self.device_manager.devices: + continue + self.scan_motors.append(motor) + + +[docs] + def update_readout_priority(self): + """update the readout priority for this request. Typically the monitored devices should also include the scan motors.""" + self.readout_priority["monitored"].extend(self.scan_motors) + self.readout_priority["monitored"] = list( + sorted( + set(self.readout_priority["monitored"]), + key=self.readout_priority["monitored"].index, + ) + ) + + + @abstractmethod + def run(self): + pass + + + + +[docs] +class ScanBase(RequestBase, PathOptimizerMixin): + """ + Base class for all scans. The following methods are called in the following order during the scan + 1. initialize + - run_pre_scan_macros + 2. read_scan_motors + 3. prepare_positions + - _calculate_positions + - _optimize_trajectory + - _set_position_offset + - _check_limits + 4. open_scan + 5. stage + 6. run_baseline_reading + 7. pre_scan + 8. scan_core + 9. finalize + 10. unstage + 11. cleanup + + A subclass of ScanBase must implement the following methods: + - _calculate_positions + + Attributes: + scan_name (str): name of the scan + scan_type (str): scan type. Can be "step" or "fly" + arg_input (list): list of scan argument types + arg_bundle_size (dict): + - bundle: number of arguments that are bundled together + - min: minimum number of bundles + - max: maximum number of bundles + required_kwargs (list): list of required kwargs + return_to_start_after_abort (bool): if True, the scan will return to the start position after an abort + """ + + scan_name = "" + scan_type = "step" + required_kwargs = ["required"] + return_to_start_after_abort = True + use_scan_progress_report = True + + # perform pre-move action before the pre_scan trigger is sent + pre_move = True + + def __init__( + self, + *args, + device_manager: DeviceManagerBase = None, + parameter: dict = None, + exp_time: float = 0, + readout_time: float = 0, + acquisition_config: dict = None, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + frames_per_trigger: int = 1, + optim_trajectory: Literal["corridor", None] = None, + monitored: list = None, + metadata: dict = None, + **kwargs, + ): + super().__init__( + *args, + device_manager=device_manager, + monitored=monitored, + parameter=parameter, + metadata=metadata, + **kwargs, + ) + self.DIID = 0 + self.point_id = 0 + self.exp_time = exp_time + self.readout_time = readout_time + self.acquisition_config = acquisition_config + self.settling_time = settling_time + self.relative = relative + self.burst_at_each_point = burst_at_each_point + self.frames_per_trigger = frames_per_trigger + self.optim_trajectory = optim_trajectory + self.burst_index = 0 + + self.start_pos = [] + self.positions = [] + self.num_pos = 0 + + if self.scan_name == "": + raise ValueError("scan_name cannot be empty") + + if acquisition_config is None or "default" not in acquisition_config: + self.acquisition_config = { + "default": {"exp_time": self.exp_time, "readout_time": self.readout_time} + } + + @property + def monitor_sync(self): + """ + monitor_sync is a property that defines how monitored devices are synchronized. + It can be either bec or the name of the device. If set to bec, the scan bundler + will synchronize scan segments based on the bec triggered readouts. If set to a device name, + the scan bundler will synchronize based on the readouts of the device, i.e. upon + receiving a new readout of the device, cached monitored readings will be added + to the scan segment. + """ + return "bec" + + +[docs] + def read_scan_motors(self): + """read the scan motors""" + yield from self.stubs.read_and_wait(device=self.scan_motors, wait_group="scan_motor") + + + @abstractmethod + def _calculate_positions(self) -> None: + """Calculate the positions""" + + def _optimize_trajectory(self): + if not self.optim_trajectory: + return + if self.optim_trajectory == "corridor": + self.positions = self.optimize_corridor(self.positions) + return + return + + +[docs] + def prepare_positions(self): + """prepare the positions for the scan""" + self._calculate_positions() + self._optimize_trajectory() + self.num_pos = len(self.positions) * self.burst_at_each_point + yield from self._set_position_offset() + self._check_limits() + + + +[docs] + def open_scan(self): + """open the scan""" + positions = self.positions if isinstance(self.positions, list) else self.positions.tolist() + yield from self.stubs.open_scan( + scan_motors=self.scan_motors, + readout_priority=self.readout_priority, + num_pos=self.num_pos, + positions=positions, + scan_name=self.scan_name, + scan_type=self.scan_type, + ) + + + +[docs] + def stage(self): + """call the stage procedure""" + yield from self.stubs.stage() + + + +[docs] + def run_baseline_reading(self): + """perform a reading of all baseline devices""" + yield from self.stubs.baseline_reading() + + + def _set_position_offset(self): + for dev in self.scan_motors: + val = yield from self.stubs.send_rpc_and_wait(dev, "read") + self.start_pos.append(val[dev].get("value")) + if self.relative: + self.positions += self.start_pos + + +[docs] + def close_scan(self): + """close the scan""" + yield from self.stubs.close_scan() + + + +[docs] + def scan_core(self): + """perform the scan core procedure""" + for ind, pos in self._get_position(): + for self.burst_index in range(self.burst_at_each_point): + yield from self._at_each_point(ind, pos) + self.burst_index = 0 + + + +[docs] + def return_to_start(self): + """return to the start position""" + yield from self._move_scan_motors_and_wait(self.start_pos) + + + +[docs] + def finalize(self): + """finalize the scan""" + yield from self.return_to_start() + yield from self.stubs.wait(wait_type="read", group="primary", wait_group="readout_primary") + yield from self.stubs.complete(device=None) + + + +[docs] + def unstage(self): + """call the unstage procedure""" + yield from self.stubs.unstage() + + + +[docs] + def cleanup(self): + """call the cleanup procedure""" + yield from self.close_scan() + + + def _at_each_point(self, ind=None, pos=None): + yield from self._move_scan_motors_and_wait(pos) + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + time.sleep(self.settling_time) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="read", group="scan_motor", wait_group="readout_primary" + ) + + self.point_id += 1 + + def _move_scan_motors_and_wait(self, pos): + if not isinstance(pos, list) and not isinstance(pos, np.ndarray): + pos = [pos] + if len(pos) == 0: + return + for ind, val in enumerate(self.scan_motors): + yield from self.stubs.set(device=val, value=pos[ind], wait_group="scan_motor") + + yield from self.stubs.wait(wait_type="move", group="scan_motor", wait_group="scan_motor") + + def _get_position(self): + for ind, pos in enumerate(self.positions): + yield (ind, pos) + + def scan_report_instructions(self): + yield None + + +[docs] + def pre_scan(self): + """ + pre scan procedure. This method is called before the scan_core method and can be used to + perform additional tasks before the scan is started. This + """ + if self.pre_move and len(self.positions) > 0: + yield from self._move_scan_motors_and_wait(self.positions[0]) + yield from self.stubs.pre_scan() + + + +[docs] + def run(self): + """run the scan. This method is called by the scan server and is the main entry point for the scan.""" + self.initialize() + yield from self.read_scan_motors() + yield from self.prepare_positions() + yield from self.scan_report_instructions() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + yield from self.pre_scan() + yield from self.scan_core() + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + + + @classmethod + def scan(cls, *args, **kwargs): + scan = cls(args, **kwargs) + yield from scan.run() + + + + +[docs] +class SyncFlyScanBase(ScanBase, ABC): + """ + Fly scan base class for all synchronous fly scans. A synchronous fly scan is a scan where the flyer is + synced with the monitored devices. + Classes inheriting from SyncFlyScanBase must at least implement the scan_core method and the monitor_sync property. + """ + + scan_type = "fly" + pre_move = False + + def _get_scan_motors(self): + # fly scans normally do not have stepper scan motors so + # the default way of retrieving scan motors is not applicable + return [] + + @property + @abstractmethod + def monitor_sync(self) -> str: + """ + monitor_sync is the flyer that will be used to synchronize the monitor readings in the scan bundler. + The return value should be the name of the flyer device. + """ + + def _calculate_positions(self) -> None: + pass + + +[docs] + def read_scan_motors(self): + yield None + + + +[docs] + def prepare_positions(self): + yield None + + + +[docs] + @abstractmethod + def scan_core(self): + """perform the scan core procedure""" + + + ############################################ + # Example of how to kickoff and wait for a flyer: + ############################################ + + # yield from self.stubs.kickoff(device=self.flyer, parameter=self.caller_kwargs) + # yield from self.stubs.complete(device=self.flyer) + # target_diid = self.DIID - 1 + + # while True: + # status = self.stubs.get_req_status( + # device=self.flyer, RID=self.metadata["RID"], DIID=target_diid + # ) + # progress = self.stubs.get_device_progress( + # device=self.flyer, RID=self.metadata["RID"] + # ) + # if progress: + # self.num_pos = progress + # if status: + # break + # time.sleep(1) + + # def _get_flyer_status(self) -> list: + # connector = self.device_manager.connector + + # pipe = connector.pipeline() + # connector.lrange( + # MessageEndpoints.device_req_status_container(self.metadata["RID"]), 0, -1, pipe + # ) + # connector.get(MessageEndpoints.device_readback(self.flyer), pipe) + # return connector.execute_pipeline(pipe) + + + +[docs] +class AsyncFlyScanBase(SyncFlyScanBase): + """ + Fly scan base class for all asynchronous fly scans. An asynchronous fly scan is a scan where the flyer is + not synced with the monitored devices. + Classes inheriting from AsyncFlyScanBase must at least implement the scan_core method. + """ + + @property + def monitor_sync(self): + return "bec" + + + + +[docs] +class ScanStub(RequestBase): + pass + + + + +[docs] +class OpenScanDef(ScanStub): + scan_name = "open_scan_def" + + def run(self): + yield from self.stubs.open_scan_def() + + + + +[docs] +class CloseScanDef(ScanStub): + scan_name = "close_scan_def" + + def run(self): + yield from self.stubs.close_scan_def() + + + + +[docs] +class CloseScanGroup(ScanStub): + scan_name = "close_scan_group" + + def run(self): + yield from self.stubs.close_scan_group() + + + + +[docs] +class DeviceRPC(ScanStub): + scan_name = "device_rpc" + arg_input = { + "device": ScanArgType.DEVICE, + "func": ScanArgType.STR, + "args": ScanArgType.LIST, + "kwargs": ScanArgType.DICT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1} + + def _get_scan_motors(self): + pass + + def run(self): + # different to calling self.device_rpc, this procedure will not wait for a reply and therefore not check any errors. + yield from self.stubs.rpc(device=self.parameter.get("device"), parameter=self.parameter) + + + + +[docs] +class Move(RequestBase): + scan_name = "mv" + arg_input = {"device": ScanArgType.DEVICE, "target": ScanArgType.FLOAT} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + required_kwargs = ["relative"] + + def __init__(self, *args, relative=False, **kwargs): + """ + Move device(s) to an absolute position + Args: + *args (Device, float): pairs of device / position arguments + relative (bool): if True, move relative to current position + + Returns: + ScanReport + + Examples: + >>> scans.mv(dev.samx, 1, dev.samy,2) + """ + super().__init__(**kwargs) + self.relative = relative + self.start_pos = [] + + def _calculate_positions(self): + self.positions = np.asarray([[val[0] for val in self.caller_args.values()]], dtype=float) + + def _at_each_point(self, pos=None): + for ii, motor in enumerate(self.scan_motors): + yield from self.stubs.set( + device=motor, + value=self.positions[0][ii], + wait_group="scan_motor", + metadata={"response": True}, + ) + + def cleanup(self): + pass + + def _set_position_offset(self): + self.start_pos = [] + for dev in self.scan_motors: + val = yield from self.stubs.send_rpc_and_wait(dev, "read") + self.start_pos.append(val[dev].get("value")) + if not self.relative: + return + self.positions += self.start_pos + + def prepare_positions(self): + self._calculate_positions() + yield from self._set_position_offset() + self._check_limits() + + def scan_report_instructions(self): + yield None + + def run(self): + self.initialize() + yield from self.prepare_positions() + yield from self.scan_report_instructions() + yield from self._at_each_point() + + + + +[docs] +class UpdatedMove(Move): + """ + Move device(s) to an absolute position and show live updates. This is a blocking call. For non-blocking use Move. + Args: + *args (Device, float): pairs of device / position arguments + relative (bool): if True, move relative to current position + + Returns: + ScanReport + + Examples: + >>> scans.umv(dev.samx, 1, dev.samy,2) + """ + + scan_name = "umv" + + def _at_each_point(self, pos=None): + for ii, motor in enumerate(self.scan_motors): + yield from self.stubs.set( + device=motor, value=self.positions[0][ii], wait_group="scan_motor" + ) + + for motor in self.scan_motors: + yield from self.stubs.wait(wait_type="move", device=motor, wait_group="scan_motor") + + def scan_report_instructions(self): + yield from self.stubs.scan_report_instruction( + { + "readback": { + "RID": self.metadata["RID"], + "devices": self.scan_motors, + "start": self.start_pos, + "end": self.positions[0], + } + } + ) + + + + +[docs] +class Scan(ScanBase): + scan_name = "grid_scan" + arg_input = { + "device": ScanArgType.DEVICE, + "start": ScanArgType.FLOAT, + "stop": ScanArgType.FLOAT, + "steps": ScanArgType.INT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 2, "max": None} + required_kwargs = ["relative"] + gui_config = { + "Scan Parameters": ["exp_time", "settling_time", "burst_at_each_point", "relative"] + } + + def __init__( + self, + *args, + exp_time: float = 0, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + Scan two or more motors in a grid. + + Args: + *args (Device, float, float, int): pairs of device / start / stop / steps arguments + exp_time (float): exposure time in seconds. Default is 0. + settling_time (float): settling time in seconds. Default is 0. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.grid_scan(dev.motor1, -5, 5, 10, dev.motor2, -5, 5, 10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, + settling_time=settling_time, + relative=relative, + burst_at_each_point=burst_at_each_point, + **kwargs, + ) + + def _calculate_positions(self): + axis = [] + for _, val in self.caller_args.items(): + axis.append(np.linspace(val[0], val[1], val[2], dtype=float)) + if len(axis) > 1: + self.positions = get_2D_raster_pos(axis) + else: + self.positions = np.vstack(tuple(axis)).T + + + + +[docs] +class FermatSpiralScan(ScanBase): + scan_name = "fermat_scan" + required_kwargs = ["step", "relative"] + gui_config = { + "Device 1": ["motor1", "start_motor1", "stop_motor1"], + "Device 2": ["motor2", "start_motor2", "stop_motor2"], + "Movement Parameters": ["step", "relative"], + "Acquisition Parameters": ["exp_time", "settling_time", "burst_at_each_point"], + } + + def __init__( + self, + motor1: DeviceBase, + start_motor1: float, + stop_motor1: float, + motor2: DeviceBase, + start_motor2: float, + stop_motor2: float, + step: float = 0.1, + exp_time: float = 0, + settling_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + spiral_type: float = 0, + optim_trajectory: Literal["corridor", None] = None, + **kwargs, + ): + """ + A scan following Fermat's spiral. + + Args: + motor1 (DeviceBase): first motor + start_motor1 (float): start position motor 1 + stop_motor1 (float): end position motor 1 + motor2 (DeviceBase): second motor + start_motor2 (float): start position motor 2 + stop_motor2 (float): end position motor 2 + step (float): step size in motor units. Default is 0.1. + exp_time (float): exposure time in seconds. Default is 0. + settling_time (float): settling time in seconds. Default is 0. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + spiral_type (float): type of spiral to use. Default is 0. + optim_trajectory (str): trajectory optimization method. Default is None. Options are "corridor" and "none". + + Returns: + ScanReport + + Examples: + >>> scans.fermat_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, step=0.5, exp_time=0.1, relative=True, optim_trajectory="corridor") + + """ + super().__init__( + exp_time=exp_time, + settling_time=settling_time, + relative=relative, + burst_at_each_point=burst_at_each_point, + optim_trajectory=optim_trajectory, + **kwargs, + ) + self.motor1 = motor1 + self.motor2 = motor2 + self.start_motor1 = start_motor1 + self.stop_motor1 = stop_motor1 + self.start_motor2 = start_motor2 + self.stop_motor2 = stop_motor2 + self.step = step + self.spiral_type = spiral_type + + def _calculate_positions(self): + self.positions = get_fermat_spiral_pos( + self.start_motor1, + self.stop_motor1, + self.start_motor2, + self.stop_motor2, + step=self.step, + spiral_type=self.spiral_type, + center=False, + ) + + + + +[docs] +class RoundScan(ScanBase): + scan_name = "round_scan" + required_kwargs = ["relative"] + gui_config = { + "Motors": ["motor_1", "motor_2"], + "Ring Parameters": ["inner_ring", "outer_ring", "number_of_rings", "pos_in_first_ring"], + "Scan Parameters": ["relative", "burst_at_each_point"], + } + + def __init__( + self, + motor_1: DeviceBase, + motor_2: DeviceBase, + inner_ring: float, + outer_ring: float, + number_of_rings: int, + pos_in_first_ring: int, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A scan following a round shell-like pattern. + + Args: + motor_1 (DeviceBase): first motor + motor_2 (DeviceBase): second motor + inner_ring (float): inner radius + outer_ring (float): outer radius + number_of_rings (int): number of rings + pos_in_first_ring (int): number of positions in the first ring + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_scan(dev.motor1, dev.motor2, 0, 25, 5, 3, exp_time=0.1, relative=True) + + """ + super().__init__(relative=relative, burst_at_each_point=burst_at_each_point, **kwargs) + self.axis = [] + self.motor_1 = motor_1 + self.motor_2 = motor_2 + self.inner_ring = inner_ring + self.outer_ring = outer_ring + self.number_of_rings = number_of_rings + self.pos_in_first_ring = pos_in_first_ring + + def _get_scan_motors(self): + caller_args = list(self.caller_args.items())[0] + self.scan_motors = [caller_args[0], caller_args[1][0]] + + def _calculate_positions(self): + self.positions = get_round_scan_positions( + r_in=self.inner_ring, + r_out=self.outer_ring, + nr=self.number_of_rings, + nth=self.pos_in_first_ring, + ) + + + + +[docs] +class ContLineScan(ScanBase): + scan_name = "cont_line_scan" + required_kwargs = ["steps", "relative"] + scan_type = "step" + gui_config = { + "Device": ["device", "start", "stop"], + "Movement Parameters": ["steps", "relative", "offset", "atol"], + "Acquisition Parameters": ["exp_time", "burst_at_each_point"], + } + + def __init__( + self, + device: DeviceBase, + start: float, + stop: float, + offset: float = 1, + atol: float = 0.5, + exp_time: float = 0, + steps: int = 10, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A continuous line scan. Use this scan if you want to move a motor continuously from start to stop position whilst + acquiring data at predefined positions. The scan will abort if the motor moves too fast and a point is skipped. + + Args: + device (DeviceBase): motor to move continuously from start to stop position + start (float): start position + stop (float): stop position + exp_time (float): exposure time in seconds. Default is 0. + steps (int): number of steps. Default is 10. + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + offset (float): offset in motor units. Default is 1. + atol (float): absolute tolerance for position check. Default is 0.5. + + Returns: + ScanReport + + Examples: + >>> scans.cont_line_scan(dev.motor1, -5, 5, steps=10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.steps = steps + self.device = device + self.offset = offset + self.start = start + self.stop = stop + self.atol = atol + self.motor_velocity = self.device_manager.devices[self.device].read()[ + f"{self.device}_velocity" + ]["value"] + + def _calculate_positions(self) -> None: + self.positions = np.linspace(self.start, self.stop, self.steps, dtype=float)[ + np.newaxis, : + ].T + # Check if the motor is moving faster than the exp_time + dist_setp = self.positions[1][0] - self.positions[0][0] + time_per_step = dist_setp / self.motor_velocity + if time_per_step < self.exp_time: + raise ScanAbortion( + f"Motor {self.device} is moving too fast. Time per step: {time_per_step:.03f} < Exp_time: {self.exp_time:.03f}." + + f" Consider reducing speed {self.motor_velocity} or reducing exp_time {self.exp_time}" + ) + + def _check_limits(self): + logger.debug("check limits") + low_limit, high_limit = self.device_manager.devices[self.device].limits + if low_limit >= high_limit: + # if both limits are equal or low > high, no restrictions ought to be applied + return + for ii, pos in enumerate(self.positions): + if ii == 0: + pos_axis = pos - self.offset + else: + pos_axis = pos + if not low_limit <= pos_axis <= high_limit: + raise LimitError( + f"Target position {pos} for motor {self.device} is outside of range: [{low_limit}," + f" {high_limit}]" + ) + + def _at_each_point(self, _pos=None): + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.read(group="primary", wait_group="primary", point_id=self.point_id) + self.point_id += 1 + + +[docs] + def scan_core(self): + yield from self._move_scan_motors_and_wait(self.positions[0] - self.offset) + # send the slow motor on its way + yield from self.stubs.set( + device=self.scan_motors[0], value=self.positions[-1][0], wait_group="scan_motor" + ) + + while self.point_id < len(self.positions[:]): + cont_motor_positions = self.device_manager.devices[self.scan_motors[0]].readback.read() + + if not cont_motor_positions: + continue + + cont_motor_positions = cont_motor_positions[self.scan_motors[0]].get("value") + logger.debug(f"Current position of {self.scan_motors[0]}: {cont_motor_positions}") + # TODO: consider the alternative, which triggers a readout for each point right after the motor passed it + # if cont_motor_positions > self.positions[self.point_id][0]: + if np.isclose(cont_motor_positions, self.positions[self.point_id][0], atol=self.atol): + logger.debug(f"reading point {self.point_id}") + yield from self._at_each_point() + continue + if cont_motor_positions > self.positions[self.point_id][0]: + raise ScanAbortion( + f"Skipped point {self.point_id + 1}:" + f"Consider reducing speed {self.device_manager.devices[self.scan_motors[0]].velocity.get()}, " + f"increasing the atol {self.atol}, or increasing the offset {self.offset}" + ) + + + + + +[docs] +class ContLineFlyScan(AsyncFlyScanBase): + scan_name = "cont_line_fly_scan" + required_kwargs = [] + use_scan_progress_report = False + gui_config = {"Device": ["motor", "start", "stop"], "Scan Parameters": ["exp_time", "relative"]} + + def __init__( + self, + motor: DeviceBase, + start: float, + stop: float, + exp_time: float = 0, + relative: bool = False, + **kwargs, + ): + """ + A continuous line fly scan. Use this scan if you want to move a motor continuously from start to stop position whilst + acquiring data as fast as possible (respecting the exposure time). The scan will stop automatically when the motor + reaches the end position. + + Args: + motor (DeviceBase): motor to move continuously from start to stop position + start (float): start position + stop (float): stop position + exp_time (float): exposure time in seconds. Default is 0. + relative (bool): if True, the motor will be moved relative to its current position. Default is False. + + Returns: + ScanReport + + Examples: + >>> scans.cont_line_fly_scan(dev.sam_rot, 0, 180, exp_time=0.1) + + """ + super().__init__(relative=relative, exp_time=exp_time, **kwargs) + self.motor = motor + self.start = start + self.stop = stop + self.device_move_request_id = str(uuid.uuid4()) + + +[docs] + def prepare_positions(self): + self.positions = np.array([[self.start], [self.stop]], dtype=float) + self.num_pos = None + yield from self._set_position_offset() + + + def scan_report_instructions(self): + yield from self.stubs.scan_report_instruction( + { + "readback": { + "RID": self.device_move_request_id, + "devices": [self.motor], + "start": [self.start], + "end": [self.stop], + } + } + ) + + +[docs] + def scan_core(self): + # move the motor to the start position + yield from self.stubs.set_and_wait(device=[self.motor], positions=self.positions[0]) + + # start the flyer + flyer_request = yield from self.stubs.set_with_response( + device=self.motor, value=self.positions[1][0], request_id=self.device_move_request_id + ) + + while True: + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="trigger", group="trigger", wait_time=self.exp_time + ) + if self.stubs.request_is_completed(flyer_request): + break + self.point_id += 1 + + + +[docs] + def finalize(self): + yield from super().finalize() + self.num_pos = self.point_id + 1 + + + + + +[docs] +class RoundScanFlySim(SyncFlyScanBase): + scan_name = "round_scan_fly" + scan_type = "fly" + pre_move = False + required_kwargs = ["relative"] + gui_config = { + "Fly Parameters": ["flyer", "relative"], + "Ring Parameters": ["inner_ring", "outer_ring", "number_of_rings", "number_pos"], + } + + def __init__( + self, + flyer: DeviceBase, + inner_ring: float, + outer_ring: float, + number_of_rings: int, + number_pos: int, + relative: bool = False, + **kwargs, + ): + """ + A fly scan following a round shell-like pattern. + + Args: + flyer (DeviceBase): flyer device + inner_ring (float): inner radius + outer_ring (float): outer radius + number_of_rings (int): number of rings + number_pos (int): number of positions in the first ring + relative (bool): if True, the motors will be moved relative to their current position. Default is False. + burst_at_each_point (int): number of exposures at each point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_scan_fly(dev.flyer_sim, 0, 50, 5, 3, exp_time=0.1, relative=True) + + """ + super().__init__(**kwargs) + self.flyer = flyer + self.inner_ring = inner_ring + self.outer_ring = outer_ring + self.number_of_rings = number_of_rings + self.number_pos = number_pos + + def _get_scan_motors(self): + self.scan_motors = [] + + @property + def monitor_sync(self): + return self.flyer + + +[docs] + def prepare_positions(self): + self._calculate_positions() + self.num_pos = len(self.positions) * self.burst_at_each_point + self._check_limits() + yield None + + + +[docs] + def finalize(self): + yield + + + def _calculate_positions(self): + self.positions = get_round_scan_positions( + r_in=self.inner_ring, + r_out=self.outer_ring, + nr=self.number_of_rings, + nth=self.number_pos, + ) + + +[docs] + def scan_core(self): + yield from self.stubs.kickoff( + device=self.flyer, + parameter={ + "num_pos": self.num_pos, + "positions": self.positions.tolist(), + "exp_time": self.exp_time, + }, + ) + target_DIID = self.DIID - 1 + + while True: + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") + status = self.device_manager.connector.get(MessageEndpoints.device_status(self.flyer)) + if status: + device_is_idle = status.content.get("status", 1) == 0 + matching_RID = self.metadata.get("RID") == status.metadata.get("RID") + matching_DIID = target_DIID == status.metadata.get("DIID") + if device_is_idle and matching_RID and matching_DIID: + break + + time.sleep(1) + logger.debug("reading monitors") + + + + + +[docs] +class RoundROIScan(ScanBase): + scan_name = "round_roi_scan" + required_kwargs = ["dr", "nth", "relative"] + gui_config = { + "Motor 1": ["motor_1", "width_1"], + "Motor 2": ["motor_2", "width_2"], + "Shell Parametes": ["dr", "nth"], + "Acquisition Parameters": ["exp_time", "relative", "burst_at_each_point"], + } + + def __init__( + self, + motor_1: DeviceBase, + width_1: float, + motor_2: DeviceBase, + width_2: float, + dr: float = 1, + nth: int = 5, + exp_time: float = 0, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A scan following a round-roi-like pattern. + + Args: + motor_1 (DeviceBase): first motor + width_1 (float): width of region of interest for motor_1 + motor_2 (DeviceBase): second motor + width_2 (float): width of region of interest for motor_2 + dr (float): shell width. Default is 1. + nth (int): number of points in the first shell. Default is 5. + exp_time (float): exposure time in seconds. Default is 0. + relative (bool): Start from an absolute or relative position. Default is False. + burst_at_each_point (int): number of acquisition per point. Default is 1. + + Returns: + ScanReport + + Examples: + >>> scans.round_roi_scan(dev.motor1, 20, dev.motor2, 20, dr=2, nth=3, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.motor_1 = motor_1 + self.motor_2 = motor_2 + self.width_1 = width_1 + self.width_2 = width_2 + self.dr = dr + self.nth = nth + + def _calculate_positions(self) -> None: + self.positions = get_round_roi_scan_positions( + lx=self.width_1, ly=self.width_2, dr=self.dr, nth=self.nth + ) + + + + +[docs] +class ListScan(ScanBase): + scan_name = "list_scan" + required_kwargs = ["relative"] + arg_input = {"device": ScanArgType.DEVICE, "positions": ScanArgType.LIST} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """ + A scan following the positions specified in a list. + Please note that all lists must be of equal length. + + Args: + *args: pairs of motors and position lists + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.list_scan(dev.motor1, [0,1,2,3,4], dev.motor2, [4,3,2,1,0], exp_time=0.1, relative=True) + + """ + super().__init__(parameter=parameter, **kwargs) + if len(set(len(entry[0]) for entry in self.caller_args.values())) != 1: + raise ValueError("All position lists must be of equal length.") + + def _calculate_positions(self): + self.positions = np.vstack(tuple(self.caller_args.values()), dtype=float).T.tolist() + + + + +[docs] +class TimeScan(ScanBase): + scan_name = "time_scan" + required_kwargs = ["points", "interval"] + gui_config = {"Scan Parameters": ["points", "interval", "exp_time", "burst_at_each_point"]} + + def __init__( + self, + points: int, + interval: float, + exp_time: float = 0, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + Trigger and readout devices at a fixed interval. + Note that the interval time cannot be less than the exposure time. + The effective "sleep" time between points is + sleep_time = interval - exp_time + + Args: + points: number of points + interval: time interval between points + exp_time: exposure time in s + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.time_scan(points=10, interval=1.5, exp_time=0.1, relative=True) + + """ + super().__init__(exp_time=exp_time, burst_at_each_point=burst_at_each_point, **kwargs) + self.points = points + self.interval = interval + self.interval -= self.exp_time + + def _calculate_positions(self) -> None: + pass + + +[docs] + def prepare_positions(self): + self.num_pos = self.points + yield None + + + def _at_each_point(self, ind=None, pos=None): + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.interval) + self.point_id += 1 + + +[docs] + def scan_core(self): + for ind in range(self.num_pos): + yield from self._at_each_point(ind) + + + + + +[docs] +class MonitorScan(ScanBase): + scan_name = "monitor_scan" + required_kwargs = ["relative"] + scan_type = "fly" + gui_config = {"Device": ["device", "start", "stop"], "Scan Parameters": ["relative"]} + + def __init__( + self, device: DeviceBase, start: float, stop: float, relative: bool = False, **kwargs + ): + """ + Readout all primary devices at each update of the monitored device. + + Args: + device (Device): monitored device + start (float): start position of the monitored device + stop (float): stop position of the monitored device + relative (bool): if True, the motor will be moved relative to its current position. Default is False. + + Returns: + ScanReport + + Examples: + >>> scans.monitor_scan(dev.motor1, -5, 5, exp_time=0.1, relative=True) + + """ + self.device = device + super().__init__(relative=relative, **kwargs) + self.start = start + self.stop = stop + + def _get_scan_motors(self): + self.scan_motors = [self.device] + self.flyer = self.device + + @property + def monitor_sync(self): + return self.flyer + + def _calculate_positions(self) -> None: + self.positions = np.array([[self.start], [self.stop]], dtype=float) + + +[docs] + def prepare_positions(self): + self._calculate_positions() + self.num_pos = 0 + yield from self._set_position_offset() + self._check_limits() + + + def _get_flyer_status(self) -> list: + connector = self.device_manager.connector + + pipe = connector.pipeline() + connector.lrange( + MessageEndpoints.device_req_status_container(self.metadata["RID"]), 0, -1, pipe + ) + connector.get(MessageEndpoints.device_readback(self.flyer), pipe) + return connector.execute_pipeline(pipe) + + +[docs] + def scan_core(self): + yield from self.stubs.set( + device=self.flyer, value=self.positions[0][0], wait_group="scan_motor" + ) + yield from self.stubs.wait(wait_type="move", device=self.flyer, wait_group="scan_motor") + + # send the slow motor on its way + yield from self.stubs.set( + device=self.flyer, + value=self.positions[1][0], + wait_group="scan_motor", + metadata={"response": True}, + ) + + while True: + move_completed, readback = self._get_flyer_status() + + if move_completed: + break + + if not readback: + continue + readback = readback.content["signals"] + yield from self.stubs.publish_data_as_read( + device=self.flyer, data=readback, point_id=self.point_id + ) + self.point_id += 1 + self.num_pos += 1 + + + + + +[docs] +class Acquire(ScanBase): + scan_name = "acquire" + required_kwargs = [] + gui_config = {"Scan Parameters": ["exp_time", "burst_at_each_point"]} + + def __init__(self, *args, exp_time: float = 0, burst_at_each_point: int = 1, **kwargs): + """ + A simple acquisition at the current position. + + Args: + exp_time (float): exposure time in s + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.acquire(exp_time=0.1, relative=True) + + """ + super().__init__(exp_time=exp_time, burst_at_each_point=burst_at_each_point, **kwargs) + + def _calculate_positions(self) -> None: + self.num_pos = self.burst_at_each_point + + +[docs] + def prepare_positions(self): + self._calculate_positions() + + + def _at_each_point(self, ind=None, pos=None): + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + + +[docs] + def scan_core(self): + for self.burst_index in range(self.burst_at_each_point): + yield from self._at_each_point(self.burst_index) + self.burst_index = 0 + + + +[docs] + def run(self): + self.initialize() + self.prepare_positions() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + yield from self.pre_scan() + yield from self.scan_core() + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + + + + + +[docs] +class LineScan(ScanBase): + scan_name = "line_scan" + required_kwargs = ["steps", "relative"] + arg_input = { + "device": ScanArgType.DEVICE, + "start": ScanArgType.FLOAT, + "stop": ScanArgType.FLOAT, + } + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + gui_config = { + "Movement Parameters": ["steps", "relative"], + "Acquisition Parameters": ["exp_time", "burst_at_each_point"], + } + + def __init__( + self, + *args, + exp_time: float = 0, + steps: int = None, + relative: bool = False, + burst_at_each_point: int = 1, + **kwargs, + ): + """ + A line scan for one or more motors. + + Args: + *args (Device, float, float): pairs of device / start position / end position + exp_time (float): exposure time in s. Default: 0 + steps (int): number of steps. Default: 10 + relative (bool): if True, the start and end positions are relative to the current position. Default: False + burst_at_each_point (int): number of acquisition per point. Default: 1 + + Returns: + ScanReport + + Examples: + >>> scans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True) + + """ + super().__init__( + exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs + ) + self.steps = steps + + def _calculate_positions(self) -> None: + axis = [] + for _, val in self.caller_args.items(): + ax_pos = np.linspace(val[0], val[1], self.steps, dtype=float) + axis.append(ax_pos) + self.positions = np.array(list(zip(*axis)), dtype=float) + + + + +[docs] +class ScanComponent(ScanBase): + pass + + + + +[docs] +class OpenInteractiveScan(ScanComponent): + scan_name = "open_interactive_scan" + required_kwargs = [] + arg_input = {"device": ScanArgType.DEVICE} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.open_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1) + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + def _get_scan_motors(self): + caller_args = list(self.caller_args.keys()) + self.scan_motors = caller_args + + +[docs] + def run(self): + yield from self.stubs.open_scan_def() + self.initialize() + yield from self.read_scan_motors() + yield from self.open_scan() + yield from self.stage() + yield from self.run_baseline_reading() + + + + + +[docs] +class AddInteractiveScanPoint(ScanComponent): + scan_name = "interactive_scan_trigger" + arg_input = {"device": ScanArgType.DEVICE} + arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": None} + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.interactive_scan_trigger() + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + def _get_scan_motors(self): + self.scan_motors = list(self.caller_args.keys()) + + def _at_each_point(self, ind=None, pos=None): + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read_and_wait( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + self.point_id += 1 + + +[docs] + def run(self): + yield from self.open_scan() + yield from self._at_each_point() + yield from self.close_scan() + + + + + +[docs] +class CloseInteractiveScan(ScanComponent): + scan_name = "close_interactive_scan" + + def __init__(self, *args, **kwargs): + """ + An interactive scan for one or more motors. + + Args: + *args: devices + Parameters +Next, we need to define the scan parameters. In exp_time: exposure time in s + steps: number of steps (please note: 5 steps == 6 positions) + relative: Start from an absolute or relative position + burst: number of acquisition per point + + Returns: + ScanReport + + Examples: + >>> scans.close_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1) + + """ + super().__init__(**kwargs) + + def _calculate_positions(self): + pass + + +[docs] + def run(self): + yield from self.finalize() + yield from self.unstage() + yield from self.cleanup() + yield from self.stubs.close_scan_def() + diff --git a/phoenix_bec/local_scripts/PhoenixTemplate.py b/phoenix_bec/local_scripts/PhoenixTemplate.py index af47e90..71c13dd 100644 --- a/phoenix_bec/local_scripts/PhoenixTemplate.py +++ b/phoenix_bec/local_scripts/PhoenixTemplate.py @@ -19,25 +19,15 @@ import importlib import ophyd -#logger = bec_logger.logger - # load local configuration -#bec.config.load_demo_config() - -# .. define base path for directory with scripts - -PhoenixBL=0 -from ConfigPHOENIX.config.phoenix import PhoenixBL -#from ConfigPHOENIX.devices.falcon_csaxs import FalconSetup -# initialize general parameter -ph=PhoenixBL() - -bec.config.update_session_with_file('./ConfigPHOENIX/device_config/phoenix_devices.yaml') +# +phoenix.add_phoenix_config() +#bec.config.update_session_with_file('./ConfigPHOENIX/device_config/phoenix_devices.yaml') time.sleep(1) -s1=scans.line_scan(dev.ScanX,0,0.002,steps=4,exp_time=1,relative=False,delay=2) +#s1=scans.line_scan(dev.ScanX,0,0.1,steps=4,exp_time=1,relative=False,delay=2) s2=scans.phoenix_line_scan(dev.ScanX,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) diff --git a/phoenix_bec/local_scripts/README.md~ b/phoenix_bec/local_scripts/README.md~ new file mode 100644 index 0000000..6d32ff0 --- /dev/null +++ b/phoenix_bec/local_scripts/README.md~ @@ -0,0 +1,8 @@ +This diretory is for scripts, test etc. which are not loaded into the server. + +Hence no directory should contain a file named +__init__.py + + +For now we keep it in the phoenix_bec structure, but for operation, such files should be located out side of the +bec_phoenix plugin. diff --git a/phoenix_bec/scans/__init__.py b/phoenix_bec/scans/__init__.py index 2cead0a..6e9a21e 100644 --- a/phoenix_bec/scans/__init__.py +++ b/phoenix_bec/scans/__init__.py @@ -1 +1 @@ -from .phoenix_line_scan import PhoenixLineScan \ No newline at end of file +from .phoenix_scans import PhoenixLineScan \ No newline at end of file diff --git a/phoenix_bec/scans/phoenix_line_scan.py b/phoenix_bec/scans/phoenix_scans.py similarity index 71% rename from phoenix_bec/scans/phoenix_line_scan.py rename to phoenix_bec/scans/phoenix_scans.py index 1ac0ff8..cce9c87 100644 --- a/phoenix_bec/scans/phoenix_line_scan.py +++ b/phoenix_bec/scans/phoenix_scans.py @@ -22,19 +22,41 @@ but they are executed in a specific order: - self.cleanup # send a close scan message and perform additional cleanups if needed """ +# imports in ScanBase +#from __future__ import annotations + +#import ast +#import enum +#import threading +#import time +#import uuid +#from abc import ABC, abstractmethod +#from typing import Any, Literal + +#import numpy as np + +#from bec_lib.device import DeviceBase +#from bec_lib.devicemanager import DeviceManagerBase +#from bec_lib.endpoints import MessageEndpoints +#from bec_lib.logger import bec_logger + +#from .errors import LimitError, ScanAbortion +#from .path_optimization import PathOptimizerMixin +#from .scan_stubs import ScanStubs +# end imports in ScanBase + # import time # import numpy as np # from bec_lib.endpoints import MessageEndpoints -# from bec_lib.logger import bec_logger +from bec_lib.logger import bec_logger # from bec_lib import messages # from bec_server.scan_server.errors import ScanAbortion # from bec_server.scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase # logger = bec_logger.logger - from bec_server.scan_server.scans import ScanBase, ScanArgType import numpy as np import time @@ -42,8 +64,63 @@ from bec_lib.logger import bec_logger logger = bec_logger.logger -class PhoenixLineScan(ScanBase): - scan_name = "phoenix_line_scanZZZ" + +class LogTime(): + + def __init__(self): + self.t0=time.process_time() + + def p_s(self,x): + now=time.process_time() + delta=now-self.t0 + m=str(delta)+' sec '+x + logger.success(m) + self.t0=now + +ll=LogTime() + + +class PhoenixScanBaseTTL(ScanBase): + """ + Base scan cl p_s('init scrips.phoenix.scans.PhoenixLineScan') + """ + + + ll.p_s('enter scripts.phoenix.scans.PhoenixScanBaseTTL') + def scan_core(self): + """perform the scan core procedure""" + ll.p_s('PhoenixScanBaseTT.scan_core') + for ind, pos in self._get_position(): + for self.burst_index in range(self.burst_at_each_point): + ll.p_s('PhoenixScanBaseTT.scan_core in loop ') + + yield from self._at_each_point(ind, pos) + self.burst_index = 0 + + def _at_each_point(self, ind=None, pos=None): + ll.p_s('PhoenixScanBaseTT._at_each_point') + yield from self._move_scan_motors_and_wait(pos) + if ind > 0: + yield from self.stubs.wait( + wait_type="read", group="primary", wait_group="readout_primary" + ) + time.sleep(self.settling_time) + yield from self.stubs.trigger(group="trigger", point_id=self.point_id) + yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) + yield from self.stubs.read( + group="primary", wait_group="readout_primary", point_id=self.point_id + ) + yield from self.stubs.wait( + wait_type="read", group="scan_motor", wait_group="readout_primary" + ) + + self.point_id += 1 + ll.p_s('done') + +class PhoenixLineScan(PhoenixScanBaseTTL): + + ll.p_s('enter scripts.phoenix.scans.PhoenixLineScan') + scan_name = "phoenix_line_scan" required_kwargs = ["steps", "relative"] arg_input = { "device": ScanArgType.DEVICE, @@ -78,38 +155,22 @@ class PhoenixLineScan(ScanBase): ans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True) """ + ll.p_s('init scripts.phoenix.scans.PhoenixLineScan') super().__init__( exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs ) self.steps = steps self.setup_device = setup_device - print('INIT CLASS PhoenixLineScan') - time.sleep(1) + time.sleep(1) + ll.p_s('done') def _calculate_positions(self) -> None: + ll.p_s('PhoenixLineScan._calculate_positions') axis = [] for _, val in self.caller_args.items(): ax_pos = np.linspace(val[0], val[1], self.steps, dtype=float) axis.append(ax_pos) self.positions = np.array(list(zip(*axis)), dtype=float) + ll.p_s('done') - def _at_each_point(self, ind=None, pos=None): - yield from self._move_scan_motors_and_wait(pos) - if ind > 0: - yield from self.stubs.wait( - wait_type="read", group="primary", wait_group="readout_primary" - ) - time.sleep(self.settling_time) - if self.setup_device: - yield from self.stubs.send_rpc_and_wait(self.setup_device, "velocity.set", 1) - yield from self.stubs.trigger(group="trigger", point_id=self.point_id) - yield from self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time) - yield from self.stubs.read( - group="primary", wait_group="readout_primary", point_id=self.point_id - ) - yield from self.stubs.wait( - wait_type="read", group="scan_motor", wait_group="readout_primary" - ) - - self.point_id += 1 \ No newline at end of file diff --git a/phoenix_bec/scripts/phoenix.py b/phoenix_bec/scripts/phoenix.py index ab1f1cd..ff0975d 100644 --- a/phoenix_bec/scripts/phoenix.py +++ b/phoenix_bec/scripts/phoenix.py @@ -28,6 +28,9 @@ logger = bec_logger.logger # .. define base path for directory with scripts + + + class PhoenixBL(): """ # @@ -68,7 +71,7 @@ class PhoenixBL(): print('add xmap ') print(self.path_devices+'phoenix_xmap.yaml') - bec.config.update_session_with_file(self.path_devices+'phoenix_xmap.yaml',timeout=100) + bec.config.update_session_with_file(self.path_devices+'phoenix_xmap.yaml')#,timeout=100) def add_falcon(self): print('add_xmap') -- 2.49.1 From 9abbcd4d487612097bc5f6c2f6d22aed1e5d716b Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 13:44:47 +0200 Subject: [PATCH 07/14] correct syntax error --- phoenix_bec/device_configs/phoenix_falcon.yaml | 2 +- phoenix_bec/devices/falcon_phoenix_no_hdf5.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/phoenix_bec/device_configs/phoenix_falcon.yaml b/phoenix_bec/device_configs/phoenix_falcon.yaml index d59f886..2f24a63 100644 --- a/phoenix_bec/device_configs/phoenix_falcon.yaml +++ b/phoenix_bec/device_configs/phoenix_falcon.yaml @@ -11,4 +11,4 @@ falcon_nohdf5: onFailure: buffer enabled: true readoutPriority: async - softwareTrigger: false + softwareTrigger: false \ No newline at end of file diff --git a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py index 443e4e5..22c3a9f 100644 --- a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py @@ -312,11 +312,10 @@ prefix ---- X07MB-ES-MA1: mca (EpicsMCARecord) : MCA parameters for Falcon detector hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector MIN_READOUT (float) : Minimum readout time for the detector - """ + # Specify which functions are revealed to the user in BEC client USER_ACCESS = ["describe"] -prefix ---- X07MB-ES-MA1: # specify Setup class custom_prepare_cls = FalconSetup @@ -353,3 +352,5 @@ prefix ---- X07MB-ES-MA1: if __name__ == "__main__": falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) + + -- 2.49.1 From 0a62c9aac6f74e7f7c65f6b0bf51bc70d201aefa Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 14:00:56 +0200 Subject: [PATCH 08/14] correct syntax error --- .../device_configs/phoenix_falcon.yaml | 2 +- phoenix_bec/devices/falcon_phoenix_no_hdf5.py | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/phoenix_bec/device_configs/phoenix_falcon.yaml b/phoenix_bec/device_configs/phoenix_falcon.yaml index 2f24a63..5884a17 100644 --- a/phoenix_bec/device_configs/phoenix_falcon.yaml +++ b/phoenix_bec/device_configs/phoenix_falcon.yaml @@ -1,6 +1,6 @@ falcon_nohdf5: description: Falcon detector x-ray fluoresence II - deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconcSAXS + deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconPHOENIX deviceConfig: prefix: 'X07MB-SITORO:' deviceTags: diff --git a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py index 22c3a9f..a1dcff2 100644 --- a/phoenix_bec/devices/falcon_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/falcon_phoenix_no_hdf5.py @@ -256,7 +256,7 @@ class FalconSetup(CustomDetectorMixin): #): # # Retry stop detector and wait for remaining time # raise FalconTimeoutError( - # f"Failed to stop detector, timeou t with state {signal_conditions[0][0]}" + # f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" # ) def stop_detector_backend(self) -> None: @@ -288,6 +288,10 @@ class FalconSetup(CustomDetectorMixin): self.stop_detector() self.stop_detector_backend() + + + + def set_trigger( self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 ) -> None: @@ -302,18 +306,25 @@ class FalconSetup(CustomDetectorMixin): """ mapping = int(mapping_mode) trigger = trigger_source - self.parent.collect_mode.put(m -prefix ---- X07MB-ES-MA1: - class attributes: - custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, - inherits from CustomDetectorMixin - PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector - dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector - mca (EpicsMCARecord) : MCA parameters for Falcon detector - hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector - MIN_READOUT (float) : Minimum readout time for the detector + self.parent.collect_mode.put(mapping) + self.parent.pixel_advance_mode.put(trigger) + self.parent.ignore_gate.put(ignore_gate) +class FalconPhoenix(PSIDetectorBase): + """ + Falcon detector for phoenix + custom_prepare_cls (XMAPSetu + custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + in __init__ of PSIDetecor base + PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector + dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector + mca (EpicsMCARecord) : MCA parameters for XMAP detector + hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + # Specify which functions are revealed to the user in BEC client USER_ACCESS = ["describe"] @@ -351,6 +362,4 @@ prefix ---- X07MB-ES-MA1: if __name__ == "__main__": - falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) - - + falcon = FalconPhoenix(name="falcon", prefix="X07MB-SITORO:", sim_mode=True) -- 2.49.1 From 9f34dc3b22ffbb6214032df724db4e29761e0a38 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 14:14:39 +0200 Subject: [PATCH 09/14] correct syntax error --- phoenix_bec/device_configs/phoenix_falcon.yaml | 2 +- phoenix_bec/device_configs/phoenix_xmap.yaml | 2 +- phoenix_bec/devices/xmap_phoenix_no_hdf5.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/phoenix_bec/device_configs/phoenix_falcon.yaml b/phoenix_bec/device_configs/phoenix_falcon.yaml index 5884a17..5d3a51c 100644 --- a/phoenix_bec/device_configs/phoenix_falcon.yaml +++ b/phoenix_bec/device_configs/phoenix_falcon.yaml @@ -1,6 +1,6 @@ falcon_nohdf5: description: Falcon detector x-ray fluoresence II - deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconPHOENIX + deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconPhoenix deviceConfig: prefix: 'X07MB-SITORO:' deviceTags: diff --git a/phoenix_bec/device_configs/phoenix_xmap.yaml b/phoenix_bec/device_configs/phoenix_xmap.yaml index 406949f..151a022 100644 --- a/phoenix_bec/device_configs/phoenix_xmap.yaml +++ b/phoenix_bec/device_configs/phoenix_xmap.yaml @@ -1,6 +1,6 @@ xmap_nohdf5: description: XMAP detector x-ray fluoresence II - deviceClass: phoenix_bec.devices.xmap_phoenix_no_hdf5.XMAPphoenix + deviceClass: phoenix_bec.devices.xmap_phoenix_no_hdf5.XMAPPhoenix deviceConfig: prefix: 'X07MB-XMAP:' deviceTags: diff --git a/phoenix_bec/devices/xmap_phoenix_no_hdf5.py b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py index 87bb9f1..0c2f280 100644 --- a/phoenix_bec/devices/xmap_phoenix_no_hdf5.py +++ b/phoenix_bec/devices/xmap_phoenix_no_hdf5.py @@ -307,7 +307,7 @@ class XMAPSetup(CustomDetectorMixin): self.parent.ignore_gate.put(ignore_gate) -class XMAPphoenix(PSIDetectorBase): +class XMAPPhoenix(PSIDetectorBase): """MCA XMAP detector for phoenix custom_prepare_cls (XMAPSetu -- 2.49.1 From 69097bc9aafc70feacce5072d0d4aff9c798f118 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 15:38:17 +0200 Subject: [PATCH 10/14] next version phoenix trigger --- phoenix_bec/devices/phoenix_trigger.py | 39 +++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/phoenix_bec/devices/phoenix_trigger.py b/phoenix_bec/devices/phoenix_trigger.py index 8d1ea1d..2d6426c 100644 --- a/phoenix_bec/devices/phoenix_trigger.py +++ b/phoenix_bec/devices/phoenix_trigger.py @@ -7,6 +7,7 @@ from ophyd import ( ) from ophyd import Component as Cpt +from ophyd import FormattedComponent as FCpt from ophyd import Device, EpicsSignal, EpicsSignalRO from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin @@ -54,6 +55,9 @@ class PhoenixTriggerSetup(CustomDetectorMixin): done = self.parent.smpl_done.get() return done + def on_done_cpt(self): + done = self.parent.smpl_done_cpt.get() + return done def on_dwell(self,t): " calculate cycles from time in sec " @@ -66,14 +70,29 @@ class PhoenixTriggerSetup(CustomDetectorMixin): time.sleep(0.05) cycles=self.parent.total_cycles.get() time.sleep(0.05) - cycles=self.parent.total_cycles.put(0) + self.parent.total_cycles.put(0) time.sleep(0.05) - cycles=self.parent.smpl.put(1) + self.parent.smpl.put(1) time.sleep(0.5) + print(cycles) cycles=self.parent.total_cycles.put(cycles) logger.success('PhoenixTrigger on stage') + def on_unstage(self): + # is this called on each point in scan or just before scan ??? + print('on unstage') + #while self.parent.smpl_done.get() + self.parent.total_cycles.put(5) + time.sleep(0.3) + self.parent.start_csmpl.put(1) + time.sleep(0.3) + self.parent.smpl.put(1) + time.sleep(2) + + self.parent.smpl.put(1) + time.sleep(.5) + logger.success('PhoenixTrigger.on_unstage') @@ -196,7 +215,9 @@ class PhoenixTrigger(PSIDetectorBase): ,"a_cont_sample_on" ,"a_cont_sample_off" ,"prefix" - ,"a_done"] + ,"a_done" + ,"a_done_cpt" + ,"SMPL"] ##################################################################### # specify Setup class into variable custom_prepare_cls @@ -222,9 +243,15 @@ class PhoenixTrigger(PSIDetectorBase): intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set smpl = Cpt(EpicsSignal,'SMPL') # start sampling --> aquire - smpl_done = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done + # Done field is of type bi + smpl_done_cpt = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done + smpl_done = EpicsSignal(name='SMPL-DONE',read_pv='X07MB-OP2:SMPL') + def SMPL(): + s= EpicsSignal(name='SMPL-DONE',read_pv='X07MB-OP2:SMPL') + return s + # link to reasonable names # start with a_ to see functions quicklz in listing # @@ -242,5 +269,9 @@ class PhoenixTrigger(PSIDetectorBase): done=self.custom_prepare.on_done() return done + def a_done_cpt(self): + done=self.custom_prepare.on_done_cpt() + return done + def a_dwell(self): self.custom_prepare.on_dwell() \ No newline at end of file -- 2.49.1 From dac949679df281b875c7ce7e9c2d97c17ea52fe0 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Fri, 23 Aug 2024 17:48:52 +0200 Subject: [PATCH 11/14] finally figured out how to handle EpicsSignals for string type.... --- phoenix_bec/devices/phoenix_trigger.py | 27 +++++++------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/phoenix_bec/devices/phoenix_trigger.py b/phoenix_bec/devices/phoenix_trigger.py index 2d6426c..232a709 100644 --- a/phoenix_bec/devices/phoenix_trigger.py +++ b/phoenix_bec/devices/phoenix_trigger.py @@ -52,11 +52,11 @@ class PhoenixTriggerSetup(CustomDetectorMixin): def on_done(self): - done = self.parent.smpl_done.get() - return done - - def on_done_cpt(self): - done = self.parent.smpl_done_cpt.get() + done_out = self.parent.smpl_done.get() + if done_out =='1': + done=True + if done_out == '0': + done=False return done def on_dwell(self,t): @@ -243,19 +243,10 @@ class PhoenixTrigger(PSIDetectorBase): intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set smpl = Cpt(EpicsSignal,'SMPL') # start sampling --> aquire - # Done field is of type bi - smpl_done_cpt = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done - smpl_done = EpicsSignal(name='SMPL-DONE',read_pv='X07MB-OP2:SMPL') + # Signal is of type string + smpl_done = Cpt(EpicsSignal,'SMPL-DONE',string=True) # show trigger is done - def SMPL(): - s= EpicsSignal(name='SMPL-DONE',read_pv='X07MB-OP2:SMPL') - return s - - # link to reasonable names - # start with a_ to see functions quicklz in listing - # - # def a_acquire(self): self.custom_prepare.on_acquire() @@ -269,9 +260,5 @@ class PhoenixTrigger(PSIDetectorBase): done=self.custom_prepare.on_done() return done - def a_done_cpt(self): - done=self.custom_prepare.on_done_cpt() - return done - def a_dwell(self): self.custom_prepare.on_dwell() \ No newline at end of file -- 2.49.1 From d815c24e2726ff1f1557a7268a9a5fec80a39dbc Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Mon, 26 Aug 2024 14:25:42 +0200 Subject: [PATCH 12/14] Add method to create larch type data group to phoenix_bec.scripts.phoenix, and data conversion to group for linescan data --- .../startup/post_startup.py | 1 + .../device_configs/phoenix_devices.yaml | 54 +- phoenix_bec/devices/__init__.py | 3 +- phoenix_bec/devices/dummy_devices.py | 490 ++++++++++++++++++ phoenix_bec/local_scripts/PhoenixTemplate.py | 33 +- phoenix_bec/local_scripts/test.py | 3 + phoenix_bec/scans/__init__.py | 2 +- phoenix_bec/scripts/phoenix.py | 118 +++++ 8 files changed, 693 insertions(+), 11 deletions(-) create mode 100644 phoenix_bec/devices/dummy_devices.py create mode 100644 phoenix_bec/local_scripts/test.py diff --git a/phoenix_bec/bec_ipython_client/startup/post_startup.py b/phoenix_bec/bec_ipython_client/startup/post_startup.py index 6ad6153..17bba61 100644 --- a/phoenix_bec/bec_ipython_client/startup/post_startup.py +++ b/phoenix_bec/bec_ipython_client/startup/post_startup.py @@ -93,6 +93,7 @@ def ph_reload(line): print('from phoenix_bec.scripts import phoenix as PH') print('phoenix = PH.PhoenixBL()') phoenix = PH.PhoenixBL() + #ph_config=PH.PhoenixConfighelper() #enddef diff --git a/phoenix_bec/device_configs/phoenix_devices.yaml b/phoenix_bec/device_configs/phoenix_devices.yaml index c9c57fc..f5af665 100644 --- a/phoenix_bec/device_configs/phoenix_devices.yaml +++ b/phoenix_bec/device_configs/phoenix_devices.yaml @@ -4,7 +4,7 @@ # # ####################################################: -TTL: +PH_TTL: description: PHOENIX TTL trigger deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger deviceConfig: @@ -15,9 +15,59 @@ TTL: - phoenix_devices.yaml onFailure: buffer enabled: true - readoutPriority: async + readoutPriority: monitored softwareTrigger: false + +PH_Dummy: + description: PHOENIX DUMMY DET + deviceClass: phoenix_bec.devices.dummy_devices.Dummy_PSIDetector + deviceConfig: + prefix: 'X07MB-PC-PSCAN:' + name: 'Dummy_Detector_PSI_Detector' + deviceTags: + - phoenix + - TTL Trigger + - phoenix_devices.yaml + - reads channel X07MB-PC-PSCAN.P-P0D0 from DAQ GUI + onFailure: buffer + enabled: true + readoutPriority: monitored + softwareTrigger: false + + + + +#Dummy_DET: +# description: PHOENIX TTL trigger +# deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger +# deviceConfig: +# prefix: 'X07MB-PC-PSCAN:' +# deviceTags: +# - phoenix +# - TTL Trigger +# - phoenixdevices +# onFailure: buffer +# enabled: true +# readoutPriority: monitored +# softwareTrigger: false + +#Dummy_DET2: +# description: Dummy for psi detector fo testing of algorithm only +# deviceClass: phoenix_bec.devices.dummy_devices.Dummy_PSIDetector +# deviceConfig: +# prefix: 'X07MB-PC-PSCAN:' +# deviceTags: +# - phoenix +# - Dummy_Dummy_PSIDetector +# - phoenix_devices.yaml +# onFailure: buffer +# enabled: true +# readoutPriority: monitored +# softwareTrigger: false + + + ############################ # # MOTORS ES1 diff --git a/phoenix_bec/devices/__init__.py b/phoenix_bec/devices/__init__.py index 7322b2b..0893a2c 100644 --- a/phoenix_bec/devices/__init__.py +++ b/phoenix_bec/devices/__init__.py @@ -1 +1,2 @@ -from .phoenix_trigger import PhoenixTrigger \ No newline at end of file +from .phoenix_trigger import PhoenixTrigger +from .dummy_devices import Dummy_PSIDetector diff --git a/phoenix_bec/devices/dummy_devices.py b/phoenix_bec/devices/dummy_devices.py new file mode 100644 index 0000000..6c67754 --- /dev/null +++ b/phoenix_bec/devices/dummy_devices.py @@ -0,0 +1,490 @@ +""" +This is a copy of psi_detecto_base.py +with added print sigbnals to understand how it functions + +""" + +import os +import threading +import time +import traceback + +from bec_lib import messages +from bec_lib.endpoints import MessageEndpoints +from bec_lib.file_utils import FileWriter +from bec_lib.logger import bec_logger +from ophyd import Component, Device, DeviceStatus, Kind +from ophyd.device import Staged + +from ophyd_devices.sim.sim_signals import SetableSignal +from ophyd_devices.utils import bec_utils +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +from ophyd import Component as Cpt +from ophyd import FormattedComponent as FCpt +from ophyd import Device, EpicsSignal, EpicsSignalRO + + +logger = bec_logger.logger + + + +class LogTime(): + + def __init__(self): + self.t0=time.process_time() + + def p_s(self,x): + now=time.process_time() + delta=now-self.t0 + m=str(delta)+' sec '+x + logger.success(m) + self.t0=now + +ll=LogTime() + + +class DetectorInitError(Exception): + """Raised when initiation of the device class fails, + due to missing device manager or not started in sim_mode.""" + + +class SetupDummy(CustomDetectorMixin): + """ + Mixin class for custom detector logic + + This class is used to implement BL specific logic for the detector. + It is used in the PSIDetectorBase class. + + For the integration of a new detector, the following functions should + help with integrating functionality, but additional ones can be added. + + Check PSIDetectorBase for the functions that are called during relevant function calls of + stage, unstage, trigger, stop and _init. + """ + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + self.parent = parent + + def on_init(self) -> None: + """ + def on_stage(self) -> None:e is writing data on disk, this step should include publishing + a file_event and file_message to BEC to inform the system where the data is written to. + + IMPORTANT: + It must be safe to assume that the device is ready for the scan + to start immediately once this function is finished. + """ + + def on_unstage(self) -> None: + """ + Specify actions to be executed during unstage. + + This step should include checking if the acqusition was successful, + and publishing the file location and file event message, + with flagged done to BEC. + """ + + def on_stop(self) -> None: + """ + Specify actions to be executed during stop. + This must also set self.parent.stopped to True. + + This step should include stopping the detector and backend service. + """ + + def on_trigger(self) -> None | DeviceStatus: + """ + Specify actions to be executed upon receiving trigger signal. + Return a DeviceStatus object or None + """ + + def on_pre_scan(self) -> None: + """ + Specify actions to be executed right before a scan starts. + + Only use if needed, and it is recommended to keep this function as short/fast as possible. + """ + + def on_complete(self) -> None | DeviceStatus: + """ + Specify actions to be executed when the scan is complete. + + This can for instance be to check with the detector and backend if all data is written succsessfully. + """ + + def publish_file_location(self, done: bool, successful: bool, metadata: dict = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + metadata (dict): additional metadata to publish + """ + if metadata is None: + metadata = {} + + msg = messages.FileMessage( + file_path=self.parent.filepath.get(), + done=done, + successful=successful, + metadata=metadata, + ) + pipe = self.parent.connector.pipeline() + self.parent.connector.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name), + msg, + pipe=pipe, + ) + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + ) + pipe.execute() + + def wait_for_signals( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + ) -> bool: + """ + Convenience wrapper to allow waiting for signals to reach a certain condition. + For EPICs PVs, an example usage is pasted at the bottom. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + + Returns: + bool: True if all signals are in the desired state, False if timeout is reached + + >>> Example usage for EPICS PVs: + >>> self.wait_for_signals(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True) + """ + + timer = 0 + while True: + checks = [ + get_current_state() == condition + for get_current_state, condition in signal_conditions + ] + if check_stopped is True and self.parent.stopped is True: + return False + if (all_signals and all(checks)) or (not all_signals and any(checks)): + return True + if timer > timeout: + return False + time.sleep(interval) + timer += interval + + def wait_with_status( + self, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool = False, + interval: float = 0.05, + all_signals: bool = False, + exception_on_timeout: Exception = None, + ) -> DeviceStatus: + """Utility function to wait for signals in a thread. + Returns a DevicesStatus object that resolves either to set_finished or set_exception. + The DeviceStatus is attached to the parent device, i.e. the detector object inheriting from PSIDetectorBase. + + Usage: + This function should be used to wait for signals to reach a certain condition, especially in the context of + on_trigger and on_complete. If it is not used, functions may block and slow down the performance of BEC. + It will return a DeviceStatus object that is to be returned from the function. Once the conditions are met, + the DeviceStatus will be set to set_finished in case of success or set_exception in case of a timeout or exception. + The exception can be specified with the exception_on_timeout argument. The default exception is a TimeoutError. + + Args: + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + + Returns: + DeviceStatus: DeviceStatus object that resolves either to set_finished or set_exception + """ + if exception_on_timeout is None: + exception_on_timeout = DeviceTimeoutError( + f"Timeout error for {self.parent.name} while waiting for signals {signal_conditions}" + ) + + status = DeviceStatus(self.parent) + + # utility function to wrap the wait_for_signals function + def wait_for_signals_wrapper( + status: DeviceStatus, + signal_conditions: list[tuple], + timeout: float, + check_stopped: bool, + interval: float, + all_signals: bool, + exception_on_timeout: Exception, + ): + """Convenient wrapper around wait_for_signals to set status based on the result. + + Args: + status (DeviceStatus): DeviceStatus object to be set + signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check + timeout (float): timeout in seconds + check_stopped (bool): True if stopped flag should be checked + interval (float): interval in seconds + all_signals (bool): True if all signals should be True, False if any signal should be True + exception_on_timeout (Exception): Exception to raise on timeout + """ + try: + result = self.wait_for_signals( + signal_conditions, timeout, check_stopped, interval, all_signals + ) + if result: + status.set_finished() + else: + if self.parent.stopped: + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=DeviceStopError(f"{self.parent.name} was stopped")) + else: + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=exception_on_timeout) + # pylint: disable=broad-except + except Exception as exc: + content = traceback.format_exc() + logger.warning( + f"Error in wait_for_signals in {self.parent.name}; Traceback: {content}" + ) + # INFO This will execute a callback to the parent device.stop() method + status.set_exception(exc=exc) + + thread = threading.Thread( + target=wait_for_signals_wrapper, + args=( + status, + signal_conditions, + timeout, + check_stopped, + interval, + all_signals, + exception_on_timeout, + ), + daemon=True, + ) + thread.start() + return status + + +class Dummy_PSIDetector(PSIDetectorBase): + """ + Abstract base class for SLS detectors + + Class attributes: + custom_prepare_cls (object): class for custom prepare logic (BL specific) + + Args: + prefix (str): EPICS PV prefix for component (optional) + name (str): name of the device, as will be reported via read() + kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal + omitted -> reado_PSIDetectorBase + """ + + filepath = Component(SetableSignal, value="", kind=Kind.config) + + custom_prepare_cls = SetupDummy + + #prefix=X07MB-PC-PSCAN + + + D = Cpt(EpicsSignal, 'P-P0D0') # cont on / off + + + def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, parent=parent, **kwargs) + ll.p_s('Dummy_device Dummy_PSIDetector.__init__ ') + self.stopped = False + self.name = name + self.service_cfg = None + self.scaninfo = None + self.filewriter = None + if not issubclass(self.custom_prepare_cls, CustomDetectorMixin): + raise DetectorInitError("Custom prepare class must be subclass of CustomDetectorMixin") + self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) + + if device_manager: + self._update_service_config() + self.device_manager = device_manager + else: + self.device_manager = bec_utils.DMMock() + base_path = kwargs["basepath"] if "basepath" in kwargs else "." + self.service_cfg = {"base_path": os.path.abspath(base_path)} + + self.connector = self.device_manager.connector + self._update_scaninfo() + self._update_filewriter() + self._init() + ll.p_s('Dummy_device Dummy_PSIDetector.__init__ .. done ') + + + def _update_filewriter(self) -> None: + """Update filewriter with service config""" + ll.p_s('Dummy_device Dummy_PSIDetector._update_filewriter') + self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector) + ll.p_s('Dummy_device Dummy_PSIDetector._update_filewriter .. done ') + + def _update_scaninfo(self) -> None: + """Update scaninfo from BecScaninfoMixing + This depends on device manager and operation/sim_mode + """ + ll.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo') + + self.scaninfo = BecScaninfoMixin(self.device_manager) + self.scaninfo.load_scan_metadata() + ll.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo .. done ') + + def _update_service_config(self) -> None: + """Update service config from BEC service config + + If bec services are not running and SERVICE_CONFIG is NONE, we fall back to the current directory. + """ + # pylint: disable=import-outside-toplevel + + from bec_lib.bec_service import SERVICE_CONFIG + ll.p_s('Dummy_device Dummy_PSIDetector._update_service_config') + + if SERVICE_CONFIG: + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] + return + self.service_cfg = {"base_path": os.path.abspath(".")} + ll.p_s('Dummy_device Dummy_PSIDetector._update_service_config .. done') + + def check_scan_id(self) -> None: + """Checks if scan_id has changed and set stopped flagged to True if it has.""" + ll.p_s('Dummy_device Dummy_PSIDetector.check_scan_id') + + old_scan_id = self.scaninfo.scan_id + self.scaninfo.load_scan_metadata() + if self.scaninfo.scan_id != old_scan_id: + self.stopped = True + ll.p_s('Dummy_device Dummy_PSIDetector.check_scan_id .. done ') + + + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters""" + ll.p_s('Dummy_device Dummy_PSIDetector._init') + + self.custom_prepare.on_init() + ll.p_s('Dummy_device Dummy_PSIDetector._init ... done ') + + + def stage(self) -> list[object]: + """ + Stage device in preparation for a scan. + First we check if the device is already staged. Stage is idempotent, + if staged twice it should raise (we let ophyd.Device handle the raise here). + We reset the stopped flag and get the scaninfo from BEC, before calling custom_prepare.on_stage. + + Returns: + list(object): list of objects that were staged + + """ + ll.p_s('Dummy_device Dummy_PSIDetector.stage') + + if self._staged != Staged.no: + return super().stage() + self.stopped = False + self.scaninfo.load_scan_metadata() + self.custom_prepare.on_stage() + ll.p_s('Dummy_device Dummy_PSIDetector.stage done ') + + + return super().stage() + + def pre_scan(self) -> None: + """Pre-scan logic. + + This function will be called from BEC directly before the scan core starts, and should only implement + time-critical actions. Therefore, it should also be kept as short/fast as possible. + I.e. Arming a detector in case there is a risk of timing out. + """ + ll.p_s('Dummy_device Dummy_PSIDetector.pre_scan') + + self.custom_prepare.on_pre_scan() + ll.p_s('Dummy_device Dummy_PSIDetector.pre_scan .. done ') + + + def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + + # pylint: disable=assignment-from-no-return + ll.p_s('Dummy_device Dummy_PSIDetector.trigger') + + status = self.custom_prepare.on_trigger() + if isinstance(status, DeviceStatus): + return status + ll.p_s('Dummy_device Dummy_PSIDetector.trigger.. done ') + + return super().trigger() + + def complete(self) -> None: + """Complete the acquisition, called from BEC. + + This function is called after the scan is complete, just before unstage. + We can check here with the data backend and detector if the acquisition successfully finished. + + Actions are implemented in custom_prepare.on_complete since they are beamline specific. + """ + # pylint: disable=assignment-from-no-return + ll.p_s('Dummy_device Dummy_PSIDetector.complete') + + status = self.custom_prepare.on_complete() + if isinstance(status, DeviceStatus): + return status + status = DeviceStatus(self) + status.set_finished() + ll.p_s('Dummy_device Dummy_PSIDetector.complete ... done ') + + return status + + def unstage(self) -> list[object]: + """ + Unstage device after a scan. + + We first check if the scanID has changed, thus, the scan was unexpectedly interrupted but the device was not stopped. + If that is the case, the stopped flag is set to True, which will immediately unstage the device. + + Custom_prepare.on_unstage is called to allow for BL specific logic to be executed. + + Returns: + list(object): list of objects that were unstaged + """ + ll.p_s('Dummy_device Dummy_PSIDetector.unstage') + self.check_scan_id() + self.custom_prepare.on_unstage() + self.stopped = False + ll.p_s('Dummy_device Dummy_PSIDetector.unstage .. done') + + return super().unstage() + + def stop(self, *, success=False) -> None: + """ + Stop the scan, with camera and file writer + + """ + ll.p_s('Dummy_device Dummy_PSIDetector.stop') + self.custom_prepare.on_stop() + super().stop(success=success) + self.stopped = True + ll.p_s('Dummy_device Dummy_PSIDetector.stop ... done') + diff --git a/phoenix_bec/local_scripts/PhoenixTemplate.py b/phoenix_bec/local_scripts/PhoenixTemplate.py index 71c13dd..42c4ed9 100644 --- a/phoenix_bec/local_scripts/PhoenixTemplate.py +++ b/phoenix_bec/local_scripts/PhoenixTemplate.py @@ -26,17 +26,36 @@ phoenix.add_phoenix_config() time.sleep(1) +s1=scans.line_scan(dev.ScanX,0,0.1,steps=4,exp_time=.2,relative=False,delay=2) -#s1=scans.line_scan(dev.ScanX,0,0.1,steps=4,exp_time=1,relative=False,delay=2) +s2=scans.phoenix_line_scan(dev.ScanX,0,0.002,steps=4,exp_time=.2,relative=False,delay=2) -s2=scans.phoenix_line_scan(dev.ScanX,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) +res1 = s1.scan.to_pandas() +re1 = res1.to_numpy() +w1=PH.PhGroup('Bec Linescan') +w1.linescan2group(s1) + +print('res1') +print(res1) +print('as numpy') +print('re1') +res2 = s2.scan.to_pandas() +re2 = res2.to_numpy() +w2=PH.PhGroup('PHOENIX Linescan') +w2.linescan2group(s2) + +print('res2') +print(res2) +print('as numpy') +print('re2') -""" +print (s1) print('---------------------------------') +""" # scan will not diode print(' SCAN DO NOT READ DIODE ') dev.PH_curr_conf.readout_priority='baseline' # do not read detector @@ -49,9 +68,8 @@ print('elapsed time',(tf-ti)/1e9) print(' SCAN READ DIODE ')s is not installed on test system ScanX_conf,0,0.002,steps=11,exp_time=.3,relative=False,delay=2) -""" -""" -next lines do not work as pandas is not installed on test system + +#next lines do not work as pandas is not installed on test system res1 = s1.scan.to_pandas() re1 = res1.to_numpy() @@ -62,4 +80,5 @@ print('Scan2 at pandas ') print(res2) print('Scan2 as numpy ') print(res2) -""" + +""" \ No newline at end of file diff --git a/phoenix_bec/local_scripts/test.py b/phoenix_bec/local_scripts/test.py new file mode 100644 index 0000000..64ba4f2 --- /dev/null +++ b/phoenix_bec/local_scripts/test.py @@ -0,0 +1,3 @@ +import phoenix_bec.scripts.phoenix as PH +w=PH.PhGroup('labelName') +w.linescan2group(s1) \ No newline at end of file diff --git a/phoenix_bec/scans/__init__.py b/phoenix_bec/scans/__init__.py index 6e9a21e..4e042e0 100644 --- a/phoenix_bec/scans/__init__.py +++ b/phoenix_bec/scans/__init__.py @@ -1 +1 @@ -from .phoenix_scans import PhoenixLineScan \ No newline at end of file +from .phoenix_scans import PhoenixLineScan diff --git a/phoenix_bec/scripts/phoenix.py b/phoenix_bec/scripts/phoenix.py index ff0975d..e698fdf 100644 --- a/phoenix_bec/scripts/phoenix.py +++ b/phoenix_bec/scripts/phoenix.py @@ -83,3 +83,121 @@ class PhoenixBL(): print(self.path_phoenix_bec) os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') + + + + +class PhGroup(): + """ + Class to create data groups + with attributes prvidws as string + + call by + + ww=MakeGroup('YourName') + + it creates a group + with default attributes + + ww.GroupName='YourName' + + To add further data use for example by + + ww.newtag=67 + + or use meth + + + """ + + def __init__(self,description): + + + setattr(self,'description',description) + # atribute 'label' for compatibility woith La groups... + setattr(self,'label',description) + #if type(NameTag)==list: + # for i in NameTag: + # setattr(self,i,None) + # #endfor + #else: + # setattr(self,NameTag,None) + #endif + + def add(self,NameTag,content): + """ + Add tags to group... + + Parameters + ---------- + NameTag : TYPE + DESCRIPTION. + content : TYPE + DESCRIPTION. + + Returns + ------- + None. + + """ + + setattr(self,NameTag,content) + + def keys(self): + + """ + Method gets all atributes, which are not methods + and which do not start with __ + + + Returns + ------- + box : TYPE + DESCRIPTION. + + """ + box=[] + + for i in self.__dir__(): + if '__' not in i: + #print(i) + if str(type(self.__getattribute__(i))) != "": + box.append(i) + #endif + #endfor + return box + + + def linescan2group(self,this_scan): + print('keys') + print(this_scan.scan.data.keys()) + for outer_key in this_scan.scan.data.keys(): + print('outer_key',outer_key) + n_outer = len(this_scan.scan.data.keys()) + for inner_key in this_scan.scan.data[outer_key].keys(): + print('inner_key',inner_key) + # calculate nunber of points + n_inner = len(this_scan.scan.data[outer_key][inner_key].keys()) + value = np.zeros(n_inner) + timestamp = np.zeros(n_inner) + for i in range(n_inner): + try: + value[i] = this_scan.scan.data[outer_key][inner_key][i]['value'] + except: + value=None + try: + timestamp[i] = this_scan.scan.data[outer_key][inner_key][i]['timestamp'] + except: + timestamp[i]=None + #endfor + self.add(inner_key+'_'+ outer_key+'_val',value) + self.add(inner_key+'_'+ outer_key+'_ts',timestamp) + #endfor + #enddef + + + + + + + -- 2.49.1 From f3f51d0a05e8deee87075a2033c683fef08c21d2 Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Tue, 27 Aug 2024 17:49:27 +0200 Subject: [PATCH 13/14] Add new logging method which writes to special data file to phoenix.PhoenixBL --- phoenix_bec/devices/delay_generator_csaxs.py | 345 ------------------- phoenix_bec/devices/dummy_devices.py | 85 +++-- phoenix_bec/devices/phoenix_trigger.py~ | 182 ---------- phoenix_bec/local_scripts/Linescan_1.py | 145 ++++---- phoenix_bec/local_scripts/PhoenixTemplate.py | 2 +- phoenix_bec/local_scripts/test.py | 2 +- phoenix_bec/scans/phoenix_scans.py | 44 ++- phoenix_bec/scripts/phoenix.py | 53 ++- 8 files changed, 197 insertions(+), 661 deletions(-) delete mode 100644 phoenix_bec/devices/delay_generator_csaxs.py delete mode 100644 phoenix_bec/devices/phoenix_trigger.py~ diff --git a/phoenix_bec/devices/delay_generator_csaxs.py b/phoenix_bec/devices/delay_generator_csaxs.py deleted file mode 100644 index c0d521b..0000000 --- a/phoenix_bec/devices/delay_generator_csaxs.py +++ /dev/null @@ -1,345 +0,0 @@ -from bec_lib import bec_logger -from ophyd import Component -from ophyd_devices.interfaces.base_classes.psi_delay_generator_base import ( - DDGCustomMixin, - PSIDelayGeneratorBase, - TriggerSource, -) -from ophyd_devices.utils import bec_utils - -logger = bec_logger.logger - - -class DelayGeneratorError(Exception): - """Exception raised for errors.""" - - -class DDGSetup(DDGCustomMixin): - """ - Mixin class for DelayGenerator logic at cSAXS. - - At cSAXS, multiple DDGs were operated at the same time. There different behaviour is - implemented in the ddg_config signals that are passed via the device config. - """ - - def initialize_default_parameter(self) -> None: - """Method to initialize default parameters.""" - for ii, channel in enumerate(self.parent.all_channels): - self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel]) - - self.parent.set_channels("amplitude", self.parent.amplitude.get()) - self.parent.set_channels("offset", self.parent.offset.get()) - # Setup reference - self.parent.set_channels( - "reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs] - ) - self.parent.set_channels( - "reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs] - ) - self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) - # Set threshold level for ext. pulses - self.parent.level.put(self.parent.thres_trig_level.get()) - - def prepare_ddg(self) -> None: - """ - Method to prepare scan logic of cSAXS - - Two scantypes are supported: "step" and "fly": - - step: Scan is performed by stepping the motor and acquiring data at each step - - fly: Scan is performed by moving the motor with a constant velocity and acquiring data - - Custom logic for different DDG behaviour during scans. - - - set_high_on_exposure : If True, then TTL signal is high during - the full exposure time of the scan (all frames). - E.g. Keep shutter open for the full scan. - - fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel. - If the value is 0, then the width of the TTL pulse is determined, - no matter which parameters are passed from the scaninfo for exposure time - - set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones - were: SINGLE_SHOT, EXT_RISING_EDGE - """ - self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get())) - # scantype "step" - if self.parent.scaninfo.scan_type == "step": - # High on exposure means that the signal - if self.parent.set_high_on_exposure.get(): - # caluculate parameters - num_burst_cycle = 1 + self.parent.additional_triggers.get() - - exp_time = ( - self.parent.delta_width.get() - + self.parent.scaninfo.frames_per_trigger - * (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time) - ) - total_exposure = exp_time - delay_burst = self.parent.delay_burst.get() - - # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too - if not self.parent.trigger_width.get(): - self.parent.set_channels("width", exp_time) - else: - self.parent.set_channels("width", self.parent.trigger_width.get()) - for value, channel in zip( - self.parent.fixed_ttl_width.get(), self.parent.all_channels - ): - logger.debug(f"Trying to set DDG {channel} to {value}") - if value != 0: - self.parent.set_channels("width", value, channels=[channel]) - else: - # caluculate parameters - exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time - total_exposure = exp_time + self.parent.scaninfo.readout_time - delay_burst = self.parent.delay_burst.get() - num_burst_cycle = ( - self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get() - ) - - # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too - if not self.parent.trigger_width.get(): - self.parent.set_channels("width", exp_time) - else: - self.parent.set_channels("width", self.parent.trigger_width.get()) - # scantype "fly" - elif self.parent.scaninfo.scan_type == "fly": - if self.parent.set_high_on_exposure.get(): - # caluculate parameters - exp_time = ( - self.parent.delta_width.get() - + self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points - + self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1) - ) - total_exposure = exp_time - delay_burst = self.parent.delay_burst.get() - num_burst_cycle = 1 + self.parent.additional_triggers.get() - - # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too - if not self.parent.trigger_width.get(): - self.parent.set_channels("width", exp_time) - else: - self.parent.set_channels("width", self.parent.trigger_width.get()) - for value, channel in zip( - self.parent.fixed_ttl_width.get(), self.parent.all_channels - ): - logger.debug(f"Trying to set DDG {channel} to {value}") - if value != 0: - self.parent.set_channels("width", value, channels=[channel]) - else: - # caluculate parameters - exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time - total_exposure = exp_time + self.parent.scaninfo.readout_time - delay_burst = self.parent.delay_burst.get() - num_burst_cycle = ( - self.parent.scaninfo.num_points + self.parent.additional_triggers.get() - ) - - # Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too - if not self.parent.trigger_width.get(): - self.parent.set_channels("width", exp_time) - else: - self.parent.set_channels("width", self.parent.trigger_width.get()) - - else: - raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}") - # Set common DDG parameters - self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first") - self.parent.set_channels("delay", 0.0) - - def on_trigger(self) -> None: - """Method to be executed upon trigger""" - if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT: - self.parent.trigger_shot.put(1) - - def check_scan_id(self) -> None: - """ - Method to check if scan_id has changed. - - If yes, then it changes parent.stopped to True, which will stop further actions. - """ - old_scan_id = self.parent.scaninfo.scan_id - self.parent.scaninfo.load_scan_metadata() - if self.parent.scaninfo.scan_id != old_scan_id: - self.parent.stopped = True - - def finished(self) -> None: - """Method checks if DDG finished acquisition""" - - def on_pre_scan(self) -> None: - """ - Method called by pre_scan hook in parent class. - - Executes trigger if premove_trigger is Trus. - """ - if self.parent.premove_trigger.get() is True: - self.parent.trigger_shot.put(1) - - -class DelayGeneratorcSAXS(PSIDelayGeneratorBase): - """ - DG645 delay generator at cSAXS (multiple can be in use depending on the setup) - - Default values for setting up DDG. - Note: checks of set calues are not (only partially) included, check manual for details on possible settings. - https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf - - - delay_burst : (float >=0) Delay between trigger and first pulse in burst mode - - delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition - - additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line) - - polarity : (list of 0/1) polarity for different channels - - amplitude : (float) amplitude voltage of TTLs - - offset : (float) offset for ampltitude - - thres_trig_level : (float) threshold of trigger amplitude - - Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg): - - - set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan. - # TODO trigger_width and fixed_ttl could be combined into single list. - - fixed_ttl_width : (list of either 1 or 0), one for each channel. - - trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value. - - set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG. - - premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan). - - set_high_on_stage : (bool) if True, then TTL signal should go high already on stage. - """ - - custom_prepare_cls = DDGSetup - - delay_burst = Component( - 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_config" - ) - - additional_triggers = Component( - bec_utils.ConfigSignal, - name="additional_triggers", - kind="config", - config_storage_name="ddg_config", - ) - - polarity = Component( - bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config" - ) - - fixed_ttl_width = Component( - bec_utils.ConfigSignal, - name="fixed_ttl_width", - kind="config", - config_storage_name="ddg_config", - ) - - amplitude = Component( - 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_config" - ) - - thres_trig_level = Component( - bec_utils.ConfigSignal, - name="thres_trig_level", - kind="config", - config_storage_name="ddg_config", - ) - - set_high_on_exposure = Component( - bec_utils.ConfigSignal, - name="set_high_on_exposure", - kind="config", - config_storage_name="ddg_config", - ) - - set_high_on_stage = Component( - bec_utils.ConfigSignal, - name="set_high_on_stage", - kind="config", - config_storage_name="ddg_config", - ) - - set_trigger_source = Component( - bec_utils.ConfigSignal, - name="set_trigger_source", - kind="config", - config_storage_name="ddg_config", - ) - - trigger_width = Component( - bec_utils.ConfigSignal, - name="trigger_width", - kind="config", - config_storage_name="ddg_config", - ) - premove_trigger = Component( - bec_utils.ConfigSignal, - name="premove_trigger", - kind="config", - config_storage_name="ddg_config", - ) - - def __init__( - self, - prefix="", - *, - name, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, - device_manager=None, - sim_mode=False, - ddg_config=None, - **kwargs, - ): - """ - Args: - prefix (str, optional): Prefix of the device. Defaults to "". - name (str): Name of the device. - kind (str, optional): Kind of the device. Defaults to None. - read_attrs (list, optional): List of attributes to read. Defaults to None. - configuration_attrs (list, optional): List of attributes to configure. Defaults to None. - parent (Device, optional): Parent device. Defaults to None. - device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None. - sim_mode (bool, optional): Simulation mode flag. Defaults to False. - ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None. - - """ - # Default values for ddg_config signals - self.ddg_config = { - # Setup default values - f"{name}_delay_burst": 0, - f"{name}_delta_width": 0, - f"{name}_additional_triggers": 0, - f"{name}_polarity": [1, 1, 1, 1, 1], - f"{name}_amplitude": 4.5, - f"{name}_offset": 0, - f"{name}_thres_trig_level": 2.5, - # Values for different behaviour during scans - f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0], - f"{name}_trigger_width": None, - f"{name}_set_high_on_exposure": False, - f"{name}_set_high_on_stage": False, - f"{name}_set_trigger_source": "SINGLE_SHOT", - f"{name}_premove_trigger": False, - } - if ddg_config is not None: - # pylint: disable=expression-not-assigned - [self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()] - super().__init__( - prefix=prefix, - name=name, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, - device_manager=device_manager, - sim_mode=sim_mode, - **kwargs, - ) - - -if __name__ == "__main__": - # Start delay generator in simulation mode. - # Note: To run, access to Epics must be available. - dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True) diff --git a/phoenix_bec/devices/dummy_devices.py b/phoenix_bec/devices/dummy_devices.py index 6c67754..fc952cf 100644 --- a/phoenix_bec/devices/dummy_devices.py +++ b/phoenix_bec/devices/dummy_devices.py @@ -26,26 +26,31 @@ from ophyd import Component as Cpt from ophyd import FormattedComponent as FCpt from ophyd import Device, EpicsSignal, EpicsSignalRO +from phoenix_bec.scripts.phoenix import PhoenixBL logger = bec_logger.logger -class LogTime(): +#class LogTime(): - def __init__(self): - self.t0=time.process_time() +# def __init__(self): +# self.t0=time.time() - def p_s(self,x): - now=time.process_time() - delta=now-self.t0 - m=str(delta)+' sec '+x - logger.success(m) - self.t0=now +# def p_s(self,x): +# now=time.time() +# #delta=now-self.t0 +# m=str(now)+' sec '+x +# logger.success(m) +# #self.t0=now +# file=open('MyLogfile.txt','a') +# file.write(m+'\n') +# file.close -ll=LogTime() +p_s=PhoenixBL.my_log + class DetectorInitError(Exception): """Raised when initiation of the device class fails, due to missing device manager or not started in sim_mode.""" @@ -211,7 +216,8 @@ class SetupDummy(CustomDetectorMixin): Args: signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check timeout (float): timeout in seconds - check_stopped (bool): True if stopped flag should be checked + check_stopped (bool): T t_offset = 1724683600 # subtract some arbtrary offset from the time value +rue if stopped flag should be checked interval (float): interval in seconds all_signals (bool): True if all signals should be True, False if any signal should be True exception_on_timeout (Exception): Exception to raise on timeout @@ -311,8 +317,13 @@ class Dummy_PSIDetector(PSIDetectorBase): def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs): + + self.p_s=PhoenixBL.my_log #must be before super!!! + + self.p_s('Dummy_device Dummy_PSIDetector.__init__ ') + super().__init__(prefix=prefix, name=name, kind=kind, parent=parent, **kwargs) - ll.p_s('Dummy_device Dummy_PSIDetector.__init__ ') + self.stopped = False self.name = name self.service_cfg = None @@ -334,24 +345,28 @@ class Dummy_PSIDetector(PSIDetectorBase): self._update_scaninfo() self._update_filewriter() self._init() - ll.p_s('Dummy_device Dummy_PSIDetector.__init__ .. done ') + #.. prepare my own log file + + self.p_s('Dummy_device Dummy_PSIDetector.__init__ .. done ') def _update_filewriter(self) -> None: """Update filewriter with service config""" - ll.p_s('Dummy_device Dummy_PSIDetector._update_filewriter') + self.p_s('Dummy_device Dummy_PSIDetector._update_filewriter') self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector) - ll.p_s('Dummy_device Dummy_PSIDetector._update_filewriter .. done ') + self.p_s('Dummy_device Dummy_PSIDetector._update_filewriter .. done ') + + def _update_scaninfo(self) -> None: """Update scaninfo from BecScaninfoMixing This depends on device manager and operation/sim_mode """ - ll.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo') + self.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo') self.scaninfo = BecScaninfoMixin(self.device_manager) self.scaninfo.load_scan_metadata() - ll.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo .. done ') + self.p_s('Dummy_device Dummy_PSIDetector._update_scaninfo .. done ') def _update_service_config(self) -> None: """Update service config from BEC service config @@ -361,31 +376,31 @@ class Dummy_PSIDetector(PSIDetectorBase): # pylint: disable=import-outside-toplevel from bec_lib.bec_service import SERVICE_CONFIG - ll.p_s('Dummy_device Dummy_PSIDetector._update_service_config') + self.p_s('Dummy_device Dummy_PSIDetector._update_service_config') if SERVICE_CONFIG: self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] return self.service_cfg = {"base_path": os.path.abspath(".")} - ll.p_s('Dummy_device Dummy_PSIDetector._update_service_config .. done') + self.p_s('Dummy_device Dummy_PSIDetector._update_service_config .. done') def check_scan_id(self) -> None: """Checks if scan_id has changed and set stopped flagged to True if it has.""" - ll.p_s('Dummy_device Dummy_PSIDetector.check_scan_id') + self.p_s('Dummy_device Dummy_PSIDetector.check_scan_id') old_scan_id = self.scaninfo.scan_id self.scaninfo.load_scan_metadata() if self.scaninfo.scan_id != old_scan_id: self.stopped = True - ll.p_s('Dummy_device Dummy_PSIDetector.check_scan_id .. done ') + self.p_s('Dummy_device Dummy_PSIDetector.check_scan_id .. done ') def _init(self) -> None: """Initialize detector, filewriter and set default parameters""" - ll.p_s('Dummy_device Dummy_PSIDetector._init') + self.p_s('Dummy_device Dummy_PSIDetector._init') self.custom_prepare.on_init() - ll.p_s('Dummy_device Dummy_PSIDetector._init ... done ') + self.p_s('Dummy_device Dummy_PSIDetector._init ... done ') def stage(self) -> list[object]: @@ -399,14 +414,14 @@ class Dummy_PSIDetector(PSIDetectorBase): list(object): list of objects that were staged """ - ll.p_s('Dummy_device Dummy_PSIDetector.stage') + self.p_s('Dummy_device Dummy_PSIDetector.stage') if self._staged != Staged.no: return super().stage() self.stopped = False self.scaninfo.load_scan_metadata() self.custom_prepare.on_stage() - ll.p_s('Dummy_device Dummy_PSIDetector.stage done ') + self.p_s('Dummy_device Dummy_PSIDetector.stage done ') return super().stage() @@ -418,22 +433,22 @@ class Dummy_PSIDetector(PSIDetectorBase): time-critical actions. Therefore, it should also be kept as short/fast as possible. I.e. Arming a detector in case there is a risk of timing out. """ - ll.p_s('Dummy_device Dummy_PSIDetector.pre_scan') + self.p_s('Dummy_device Dummy_PSIDetector.pre_scan') self.custom_prepare.on_pre_scan() - ll.p_s('Dummy_device Dummy_PSIDetector.pre_scan .. done ') + self.p_s('Dummy_device Dummy_PSIDetector.pre_scan .. done ') def trigger(self) -> DeviceStatus: """Trigger the detector, called from BEC.""" # pylint: disable=assignment-from-no-return - ll.p_s('Dummy_device Dummy_PSIDetector.trigger') + self.p_s('Dummy_device Dummy_PSIDetector.trigger') status = self.custom_prepare.on_trigger() if isinstance(status, DeviceStatus): return status - ll.p_s('Dummy_device Dummy_PSIDetector.trigger.. done ') + self.p_s('Dummy_device Dummy_PSIDetector.trigger.. done ') return super().trigger() @@ -446,14 +461,14 @@ class Dummy_PSIDetector(PSIDetectorBase): Actions are implemented in custom_prepare.on_complete since they are beamline specific. """ # pylint: disable=assignment-from-no-return - ll.p_s('Dummy_device Dummy_PSIDetector.complete') + self.p_s('Dummy_device Dummy_PSIDetector.complete') status = self.custom_prepare.on_complete() if isinstance(status, DeviceStatus): return status status = DeviceStatus(self) status.set_finished() - ll.p_s('Dummy_device Dummy_PSIDetector.complete ... done ') + self.p_s('Dummy_device Dummy_PSIDetector.complete ... done ') return status @@ -469,11 +484,11 @@ class Dummy_PSIDetector(PSIDetectorBase): Returns: list(object): list of objects that were unstaged """ - ll.p_s('Dummy_device Dummy_PSIDetector.unstage') + self.p_s('Dummy_device Dummy_PSIDetector.unstage') self.check_scan_id() self.custom_prepare.on_unstage() self.stopped = False - ll.p_s('Dummy_device Dummy_PSIDetector.unstage .. done') + self.p_s('Dummy_device Dummy_PSIDetector.unstage .. done') return super().unstage() @@ -482,9 +497,9 @@ class Dummy_PSIDetector(PSIDetectorBase): Stop the scan, with camera and file writer """ - ll.p_s('Dummy_device Dummy_PSIDetector.stop') + self.p_s('Dummy_device Dummy_PSIDetector.stop') self.custom_prepare.on_stop() super().stop(success=success) self.stopped = True - ll.p_s('Dummy_device Dummy_PSIDetector.stop ... done') + self.p_s('Dummy_device Dummy_PSIDetector.stop ... done') diff --git a/phoenix_bec/devices/phoenix_trigger.py~ b/phoenix_bec/devices/phoenix_trigger.py~ deleted file mode 100644 index d457eb6..0000000 --- a/phoenix_bec/devices/phoenix_trigger.py~ +++ /dev/null @@ -1,182 +0,0 @@ -from ophyd import ( - ADComponent as ADCpt, - Device, - DeviceStatus, -) - -from ophyd import Component as Cpt -from ophyd import Device, EpicsSignal, EpicsSignalRO - -from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin - -from bec_lib import bec_logger, messages -from bec_lib.endpoints import MessageEndpoints - -import time - -logger = bec_logger.logger - -DETECTOR_TIMEOUT = 5 - -#class PhoenixTriggerError(Exce start_csmpl=Cpt(EPicsSignal,'START-CSMPL') # cont on / off - - - - - -class PhoenixTriggerSetup(CustomDetectorMixin): - """ - This defines the PHOENIX trigger setup. - - - """ - - def __init__(self, *args, parent:Device = None, **kwargs): - super().__init__(*args, parent=parent, **kwargs) - self._counter = 0 - - WW - def on_stage(self): - # is this called on each point in scan or just before scan ??? - print('on stage') - self.parent.start_csmpl.put(0) - time.sleep(0.05) - cycles=self.parent.total_cycles.get() - time.sleep(0.05) - cycles=self.parent.total_cycles.put(0) - time.sleep(0.05) - cycles=self.parent.smpl.put(2) - time.sleep(0.5) - cycles=self.parent.total_cycles.put(cycles) - - logger.success('PhoenixTrigger on stage') - - def on_trigger(self): - - self.parent.start_smpl.put(1) - time.sleep(0.05) # use blocking - logger.success('PhoenixTrigger on_trigger') - - return self.wait_with_status( - [(self.parent.smpl_done.get, 1)]) - - - - -# logger.success(' PhoenixTrigger on_trigger complete ') - -# if success: -# status.set_finished() -# else: -# status.set_exception(TimeoutError()) -# return status - - - - def on_complete(self): - - timeout =10 - - - logger.success('XXXX complete %d XXXX' % success) - - success = self.wait_for_signals( - [ - (self.parent.smpl_done.get, 0)) - ], - timeout, - check_stopped=True, - all_signals=True - ) - - - - if success: - status.set_finished() - else: - status.set_exception(TimeoutError()) - return status - - - - - def on_stop(self): - logger.success(' PhoenixTrigger on_stop ') - - self.parent.csmpl.put(1) - logger.success(' PhoenixTrigger on_stop finished ') - - def on_unstage(self): - logger.success(' PhoenixTrigger on_unstage ') - self.parent.csmpl.put(1) - self.parent.smpl.put(1) - logger.success(' PhoenixTrigger on_unstage finished ') - - - - - -class PhoenixTrigger(PSIDetectorBase): - - """ - Parent class: PSIDetectorBase - - class attributes: - custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS, - inherits from CustomDetectorMixin - in __init__ of PSIDetecor bases - class is initialized - self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs) - PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector - dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector - mca (EpicsMCARecord) : MCA parameters for XMAP detector - hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector - MIN_READOUT (float) : Minimum readout time for the detector - - - The class PhoenixTrigger is the class to be called via yaml configuration file - the input arguments are defined by PSIDetectorBase, - and need to be given in the yaml configuration file. - To adress chanels such as 'X07MB-OP2:SMPL-DONE': - - use prefix 'X07MB-OP2:' in the device definition in the yaml configuration file. - - PSIDetectorBase( - prefix='', - *,Q - name, - kind=None, - parent=None, - device_manager=None, - **kwargs, - ) - Docstring: - Abstract base class for SLS detectors - - Class attributes: - custom_prepare_cls (object): class for custom prepare logic (BL specific) - - Args: - prefix (str): EPICS PV prefix for component (optional) - name (str): name of the device, as will be reported via read() - kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal - omitted -> readout ignored for read 'ophydobj.read()' - normal -> readout for read - config -> config parameter for 'ophydobj.read_configuration()' - hinted -> which attribute is readout for read - parent (object): instance of the parent device - device_manager (object): bec device manager - **kwargs: keyword arguments - File: /data/test/x07mb-test-bec/bec_deployment/ophyd_devices/ophyd_devices/interfaces/base_classes/psi_detector_base.py - Type: type - Subclasses: EpicsSignal - """ - - custom_prepare_cls = PhoenixTriggerSetup - - - start_csmpl = Cpt(EpicsSignal,'START-CSMPL') # cont on / off - intr_count = Cpt(EpicsSignal,'INTR-COUNT') # conter run up - total_cycles = Cpt(EpicsSignal,'TOTAL-CYCLES') # cycles set - smpl_done = Cpt(EpicsSignal,'SMPL-DONE') # show trigger is done - diff --git a/phoenix_bec/local_scripts/Linescan_1.py b/phoenix_bec/local_scripts/Linescan_1.py index c35f269..0a41930 100644 --- a/phoenix_bec/local_scripts/Linescan_1.py +++ b/phoenix_bec/local_scripts/Linescan_1.py @@ -1,81 +1,81 @@ -#from unittest import mock -import numpy as np -#import pandas -#import pytest -#from bec_lib import messages -#import device_server -#from ophyd import Component as Cpt -from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO -#from ophyd import FormattedComponent as FCpt -#from ophyd import Kind, PVPositioner, Signal -#from ophyd.flyers import FlyerInterface -#from ophyd.pv_positioner import PVPositionerComparator -#from ophyd.status import DeviceStatus, SubscriptionStatus +#from unittest import mock +import numpy as np +#import pandas +#import pytest +#from bec_lib import messages +#import device_server +#from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +#from ophyd import FormattedComponent as FCpt +#from ophyd import Kind, PVPositioner, Signal +#from ophyd.flyers import FlyerInterface +#from ophyd.pv_positioner import PVPositionerComparator +#from ophyd.status import DeviceStatus, SubscriptionStatus -import time as tt - -#import ophyd -import os -import sys - -#logger = bec_logger.logger -# load simulation - -#bec.config.load_demo_config() - -bec.config.update_session_with_file("config/config_1.yaml") - -os.system('mv *.yaml tmp') +import time as tt + +#import ophyd +import os +import sys + +#logger = bec_logger.logger +# load simulation + +#bec.config.load_demo_config() + +bec.config.update_session_with_file("config/config_1.yaml") + +os.system('mv *.yaml tmp') -class PhoenixBL: - - #define some epics channels - - def __init__(self): - from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO - from ophyd import Component as Cpt - self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') - self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') - self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') - self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") - self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') - self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') - self.fielda =EpicsSignal(name='SMPL',read_pv='X07MB-SCAN:scan1.P1SP',write_pv='X07MB-SCAN:scan1.P1SP') -#end class - -ph=PhoenixBL() - -print('---------------------------------') - -# scan will not diode -print(' SCAN DO NOT READ DIODE ') -dev.PH_curr_conf.readout_priority='baseline' # do not read detector -ti=tt.time_ns() -s1=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) -tf=tt.time_ns() - -print('elapsed time',(tf-ti)/1e9) -# scan will read diode -print(' SCAN READ DIODE ') -tt.sleep(2) -dev.PH_curr_conf.readout_priority='monitored' # read detector - -s2=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=11,exp_time=.3,relative=False,delay=2) - - +class PhoenixBL: + + #define some epics channels + + def __init__(self): + from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO + from ophyd import Component as Cpt + self.ScanX = EpicsMotor(name='ScanX',prefix='X07MB-ES-MA1:ScanX') + self.ScanY = EpicsMotor(name='ScanY',prefix='X07MB-ES-MA1:ScanY') + self.DIODE = EpicsSignal(name='SI',read_pv='X07MB-OP2-SAI_07:MEAN') + self.SIG = Cpt(EpicsSignal,name='we',read_pv="X07MB-OP2-SAI_07:MEAN") + self.SMPL = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:SMPL') + self.CYCLES = EpicsSignal(name='SMPL',read_pv='X07MB-OP2:TOTAL-CYCLES',write_pv='X07MB-OP2:TOTAL-CYCLES') + self.fielda =EpicsSignal(name='SMPL',read_pv='X07MB-SCAN:scan1.P1SP',write_pv='X07MB-SCAN:scan1.P1SP') +#end class + +ph=PhoenixBL() + +print('---------------------------------') + +# scan will not diode +print(' SCAN DO NOT READ DIODE ') +dev.PH_curr_conf.readout_priority='baseline' # do not read detector +ti=tt.time_ns() +s1=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=4,exp_time=.01,relative=False,delay=2) +tf=tt.time_ns() + +print('elapsed time',(tf-ti)/1e9) +# scan will read diode +print(' SCAN READ DIODE ') +tt.sleep(2) +dev.PH_curr_conf.readout_priority='monitored' # read detector + +s2=scans.line_scan(dev.PH_ScanX_conf,0,0.002,steps=11,exp_time=.3,relative=False,delay=2) + + """ -next lines do not work as pandas is not installed on test system +next lines do not work as pandas is not installed on test system -res1 = s1.scan.to_pandas() -re1 = res1.to_numpy() -print('Scana') -print(res1) -print('') -print('Scan2 at pandas ') -print(res2) -print('Scan2 as numpy ') +res1 = s1.scan.to_pandas() +re1 = res1.to_numpy() +print('Scana') +print(res1) +print('') +print('Scan2 at pandas ') +print(res2) +print('Scan2 as numpy ') print(res2) """ @@ -83,4 +83,3 @@ print(res2) - \ No newline at end of file diff --git a/phoenix_bec/local_scripts/PhoenixTemplate.py b/phoenix_bec/local_scripts/PhoenixTemplate.py index 42c4ed9..5f07988 100644 --- a/phoenix_bec/local_scripts/PhoenixTemplate.py +++ b/phoenix_bec/local_scripts/PhoenixTemplate.py @@ -28,7 +28,7 @@ time.sleep(1) s1=scans.line_scan(dev.ScanX,0,0.1,steps=4,exp_time=.2,relative=False,delay=2) -s2=scans.phoenix_line_scan(dev.ScanX,0,0.002,steps=4,exp_time=.2,relative=False,delay=2) +s2=scans.phoenix_line_scan(dev.ScanX,0,0.1,steps=4,exp_time=.2,relative=False,delay=2) res1 = s1.scan.to_pandas() re1 = res1.to_numpy() diff --git a/phoenix_bec/local_scripts/test.py b/phoenix_bec/local_scripts/test.py index 64ba4f2..fec1381 100644 --- a/phoenix_bec/local_scripts/test.py +++ b/phoenix_bec/local_scripts/test.py @@ -1,3 +1,3 @@ import phoenix_bec.scripts.phoenix as PH w=PH.PhGroup('labelName') -w.linescan2group(s1) \ No newline at end of file +w.linescan2group(s1) diff --git a/phoenix_bec/scans/phoenix_scans.py b/phoenix_bec/scans/phoenix_scans.py index cce9c87..a55826e 100644 --- a/phoenix_bec/scans/phoenix_scans.py +++ b/phoenix_bec/scans/phoenix_scans.py @@ -61,6 +61,8 @@ from bec_server.scan_server.scans import ScanBase, ScanArgType import numpy as np import time from bec_lib.logger import bec_logger +from phoenix_bec.scripts.phoenix import PhoenixBL + logger = bec_logger.logger @@ -68,16 +70,21 @@ logger = bec_logger.logger class LogTime(): def __init__(self): - self.t0=time.process_time() + logger.success('init LogTime') + self.t0=time.time() def p_s(self,x): - now=time.process_time() - delta=now-self.t0 - m=str(delta)+' sec '+x + now=time.time() + #delta=now-self.t0 + m=str(now)+' sec '+x logger.success(m) - self.t0=now + #self.t0=now + file=open('MyLogfile.txt','a') + file.write(m+'\n') + file.close + + -ll=LogTime() class PhoenixScanBaseTTL(ScanBase): @@ -86,19 +93,19 @@ class PhoenixScanBaseTTL(ScanBase): """ - ll.p_s('enter scripts.phoenix.scans.PhoenixScanBaseTTL') + def scan_core(self): """perform the scan core procedure""" - ll.p_s('PhoenixScanBaseTT.scan_core') + self.p_s('PhoenixScanBaseTT.scan_core') for ind, pos in self._get_position(): for self.burst_index in range(self.burst_at_each_point): - ll.p_s('PhoenixScanBaseTT.scan_core in loop ') + self.p_s('PhoenixScanBaseTT.scan_core in loop ') yield from self._at_each_point(ind, pos) self.burst_index = 0 def _at_each_point(self, ind=None, pos=None): - ll.p_s('PhoenixScanBaseTT._at_each_point') + self.p_s('PhoenixScanBaseTT._at_each_point') yield from self._move_scan_motors_and_wait(pos) if ind > 0: yield from self.stubs.wait( @@ -115,11 +122,12 @@ class PhoenixScanBaseTTL(ScanBase): ) self.point_id += 1 - ll.p_s('done') + self.p_s('done') class PhoenixLineScan(PhoenixScanBaseTTL): - ll.p_s('enter scripts.phoenix.scans.PhoenixLineScan') + + scan_name = "phoenix_line_scan" required_kwargs = ["steps", "relative"] arg_input = { @@ -155,7 +163,11 @@ class PhoenixLineScan(PhoenixScanBaseTTL): ans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True) """ - ll.p_s('init scripts.phoenix.scans.PhoenixLineScan') + #from phoenix_bec.scripts.phoenix import PhoenixBL + self.p_s=PhoenixBL.my_log + + self.p_s('init scripts.phoenix.scans.PhoenixLineScan') + super().__init__( exp_time=exp_time, relative=relative, burst_at_each_point=burst_at_each_point, **kwargs ) @@ -163,14 +175,14 @@ ans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, rela self.setup_device = setup_device time.sleep(1) - ll.p_s('done') + self.p_s('done') def _calculate_positions(self) -> None: - ll.p_s('PhoenixLineScan._calculate_positions') + self.p_s('PhoenixLineScan._calculate_positions') axis = [] for _, val in self.caller_args.items(): ax_pos = np.linspace(val[0], val[1], self.steps, dtype=float) axis.append(ax_pos) self.positions = np.array(list(zip(*axis)), dtype=float) - ll.p_s('done') + self.p_s('done') diff --git a/phoenix_bec/scripts/phoenix.py b/phoenix_bec/scripts/phoenix.py index e698fdf..6bc40fe 100644 --- a/phoenix_bec/scripts/phoenix.py +++ b/phoenix_bec/scripts/phoenix.py @@ -1,7 +1,7 @@ #from unittest import mock import os import sys -import time as tt +import time import numpy as np #import pandas @@ -37,6 +37,7 @@ class PhoenixBL(): # General class for PHOENIX beamline located in phoenix_bec/phoenic_bec/scripts # """ + t0=time.time() def __init__(self): """ init PhoenixBL() in phoenix_bec/scripts @@ -56,6 +57,8 @@ class PhoenixBL(): self.path_phoenix_bec ='/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/' self.path_devices = self.path_phoenix_bec + 'phoenix_bec/device_configs/' # local yamal file self.file_devices_file = self.path_phoenix_bec + 'phoenix_bec/device_configs/phoenix_devices.yaml' # local yamal file + self.t0=time.time() + def read_local_phoenix_config(self): print('read file ') @@ -83,6 +86,26 @@ class PhoenixBL(): print(self.path_phoenix_bec) os.system('cat '+self.path_phoenix_bec+'phoenix_bec/scripts/Current_setup.txt') + @classmethod + def my_log(cls,x): + + """ + class method allows to write a user defined log file + time is seconds relative to some point max 10 minutes ago + + """ + + print(time.time()) + now = time.time() - (86400*(time.time()//86400)) + now = now - 3600.*(now//3600.) + now = now - 600.*(now//600.) + m=str(now)+' sec '+x + + logger.success(m) + + file=open('MyLogfile.txt','a') + file.write(m+'\n') + file.close @@ -90,23 +113,24 @@ class PhoenixBL(): class PhGroup(): """ Class to create data groups - with attributes prvidws as string + compatible with larch groups - call by + initialize by - ww=MakeGroup('YourName') + ww=PhGroup('YourLabel') it creates a group with default attributes - ww.GroupName='YourName' + ww.label = 'YourLabel' --- for compatibility with larch groups + ww.description =YourLabel' - To add further data use for example by + Further data can be added with new tags by ww.newtag=67 - or use meth - + ww.keys() -- list all keys + ww.linescan2group -- converts bec linescan data to group format """ @@ -169,6 +193,17 @@ class PhGroup(): def linescan2group(self,this_scan): + """ + + method merges results of linescan into group and + creates for each data a numpy variable constructed as + + group_name.{device_name}_{variable_name}_val (for value ) + group_name.{device_name}_{variable_name}_ts (for timestamp ) + + + """ + print('keys') print(this_scan.scan.data.keys()) for outer_key in this_scan.scan.data.keys(): @@ -192,6 +227,8 @@ class PhGroup(): #endfor self.add(inner_key+'_'+ outer_key+'_val',value) self.add(inner_key+'_'+ outer_key+'_ts',timestamp) + #endfor + #endfor #endfor #enddef -- 2.49.1 From bd7dff99c54d5d00316a1b764852d2e57d9c6fed Mon Sep 17 00:00:00 2001 From: gac-x07mb Date: Wed, 28 Aug 2024 09:39:23 +0200 Subject: [PATCH 14/14] next version --- phoenix_bec/device_configs/phoenix_devices.yaml | 12 +++++------- phoenix_bec/local_scripts/PhoenixTemplate.py | 2 +- phoenix_bec/scans/phoenix_scans.py | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/phoenix_bec/device_configs/phoenix_devices.yaml b/phoenix_bec/device_configs/phoenix_devices.yaml index f5af665..31fa2c9 100644 --- a/phoenix_bec/device_configs/phoenix_devices.yaml +++ b/phoenix_bec/device_configs/phoenix_devices.yaml @@ -61,11 +61,7 @@ PH_Dummy: # - phoenix # - Dummy_Dummy_PSIDetector # - phoenix_devices.yaml -# onFailure: buffer -# enabled: true -# readoutPriority: monitored -# softwareTrigger: false - +# onFailure: buffe ############################ @@ -75,9 +71,11 @@ PH_Dummy: ############################ + + ScanX: readoutPriority: baseline - description: 'Horizontal sample position' + description: 'Vert sample position' deviceClass: ophyd.EpicsMotor deviceConfig: prefix: 'X07MB-ES-MA1:ScanX' @@ -87,7 +85,7 @@ ScanX: onFailure: retry enabled: true readOnly: false - softwareTrigger: false + ScanY: readoutPriority: baseline diff --git a/phoenix_bec/local_scripts/PhoenixTemplate.py b/phoenix_bec/local_scripts/PhoenixTemplate.py index 5f07988..c289a2f 100644 --- a/phoenix_bec/local_scripts/PhoenixTemplate.py +++ b/phoenix_bec/local_scripts/PhoenixTemplate.py @@ -24,7 +24,7 @@ phoenix.add_phoenix_config() #bec.config.update_session_with_file('./ConfigPHOENIX/device_config/phoenix_devices.yaml') time.sleep(1) - +w1. s1=scans.line_scan(dev.ScanX,0,0.1,steps=4,exp_time=.2,relative=False,delay=2) diff --git a/phoenix_bec/scans/phoenix_scans.py b/phoenix_bec/scans/phoenix_scans.py index a55826e..dbbad2e 100644 --- a/phoenix_bec/scans/phoenix_scans.py +++ b/phoenix_bec/scans/phoenix_scans.py @@ -77,7 +77,8 @@ class LogTime(): now=time.time() #delta=now-self.t0 m=str(now)+' sec '+x - logger.success(m) + logger.success(m)custom_prepare_cls(parent=self, **kwargs) + # making the instance of PSID #self.t0=now file=open('MyLogfile.txt','a') file.write(m+'\n') -- 2.49.1