47 Commits

Author SHA1 Message Date
51da920272 test: fix import and mock due to refactoring in ophyd_devices 2025-01-14 17:58:27 +01:00
e583e25424 fix(scans): fixed scans for bec v3 2024-11-14 17:53:07 +01:00
14da4a1ad5 docs: Update device list 2024-11-14 10:40:24 +00:00
cea11ff48f rewrite Falcon/Sitoro using a new base class in sitoro.py, which is a copy-edited version of mca.py. mca.py works fine with XMAP, we now consider Falcon/sitoro as completely different device. This is a first working version, do dxp and no mca support yet. Structure of code is along the lines discussed with Xioquiang Wang 2024-11-14 11:27:47 +01:00
bae057995f Add channels for detector energy calibration to falcon 2024-11-14 11:27:47 +01:00
b806cbc9c2 remove unneded files 2024-11-14 11:27:47 +01:00
55e723ba24 Add script test_falcon.py, which allows testing falcon device in local iphython shell 2024-11-14 11:27:47 +01:00
e10be3e9bf syntax erro correction 2024-11-14 11:27:47 +01:00
2e93a0d811 falcon_phonix.py to match epics and bec names, add option for 4 detectors 2024-11-14 11:27:47 +01:00
956a3267c4 relax condition in pipeline tests for phoenix_trigger.py 2024-11-14 11:27:47 +01:00
b704ba1095 relax condition in pipeline tests for phoenix_trigger.py 2024-11-14 11:27:47 +01:00
08e2c73c2d relax condition in pipeline tests for phoenix_trigger.py 2024-11-14 11:27:47 +01:00
86582fdebc ... trouble shooting as tests fail phoenix_trigger.py 2024-11-14 11:27:47 +01:00
2a445e3449 ... trouble shooting as tests fail phoenix_trigger.py 2024-11-14 11:27:47 +01:00
06ffca03ee correct another editing error in phoenix_trigger.py 2024-11-14 11:27:47 +01:00
e07a1fe66f correct another editing error in phoenix_trigger.py 2024-11-14 11:27:47 +01:00
f162e8585f correct editing error in phoenix_trigger.py 2024-11-14 11:27:47 +01:00
d4b74302ef Add documentation folder, some changes to falcon_phoenix.py, phoenixtrigger.py, post_startup.py 2024-11-14 11:27:47 +01:00
e5f004258a add on yaml file 2024-11-14 11:27:47 +01:00
2f2b152ef1 1st version of falcon implementation for PHOENIX 2024-11-14 11:27:47 +01:00
ec61712803 enable adding of configs to existing configs some fixes 2024-11-14 11:27:47 +01:00
08a63c54ce Add comments to trigger.py, add option to add devices to existing devices in phoenix.py. Method connecting different config files via cat into a tmp file which is then loaded 2024-11-14 11:27:47 +01:00
16b49fe670 added license 2024-10-01 10:26:34 +02:00
20774ba2a4 docs: Update device list 2024-08-28 14:56:22 +00:00
3423a978b6 fix: remove copy/paste bug 2024-08-28 16:21:41 +02:00
66eff8e5f0 fix: remove DummyDetector import from init; crashes pipeline due to syntax error 2024-08-28 16:21:41 +02:00
5ba10cdff8 test: add tests for phoenix_trigger device 2024-08-28 16:21:41 +02:00
b752a79c41 refactor: cleanup phoenix_trigger device 2024-08-28 16:21:41 +02:00
bfa06fe519 docs: Update device list 2024-08-28 12:34:28 +00:00
6c8d5ee202 refactor: misc changes; playing around 2024-08-28 14:18:18 +02:00
1f55dc8736 feat: added phoenix trigger and config 2024-08-28 14:17:24 +02:00
1c9c94f28f chore: ignore gz files 2024-08-28 14:16:11 +02:00
08d576dbc2 docs: Update device list 2024-08-28 11:43:01 +00:00
bd7dff99c5 next version 2024-08-28 09:39:23 +02:00
f3f51d0a05 Add new logging method which writes to special data file to phoenix.PhoenixBL 2024-08-27 17:49:27 +02:00
d815c24e27 Add method to create larch type data group to phoenix_bec.scripts.phoenix, and data conversion to group for linescan data 2024-08-26 14:25:42 +02:00
dac949679d finally figured out how to handle EpicsSignals for string type.... 2024-08-23 17:48:52 +02:00
69097bc9aa next version phoenix trigger 2024-08-23 15:38:17 +02:00
9f34dc3b22 correct syntax error 2024-08-23 14:14:39 +02:00
0a62c9aac6 correct syntax error 2024-08-23 14:00:56 +02:00
9abbcd4d48 correct syntax error 2024-08-23 13:44:47 +02:00
c77f359451 Add magic to post_startup.py to restart bec server from command line, first version of TTL Trigger device 2024-08-23 13:37:46 +02:00
6572a71f3b minor change 2024-08-21 17:58:19 +02:00
943aa44abe next some base classed copied and commented to local scripts 2024-08-21 17:54:57 +02:00
20aaa069de next some base classed copied and commented to local scripts 2024-08-21 17:52:24 +02:00
7359d1b8f6 Change readm in scripts 2024-08-20 13:41:43 +02:00
1b8aabccc1 First implementation on x07mb-bec_001 after merging with x07mb-test 2024-08-20 13:29:30 +02:00
68 changed files with 14313 additions and 0 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
**/.vscode
**/.pytest_cache
**/*.egg*
*.gz
# recovery_config files
recovery_config_*

28
LICENSE Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, Paul Scherrer Institute
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -34,3 +34,165 @@ 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
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_add_xmap(line):
"""
magic for loading xmap
"""
t0 = tt.time()
phoenix.add_xmap()
print("elapsed time:", tt.time() - t0)
# enddef
print("...... %ph_load_falcon ... to reload falcon configuration")
@register_line_magic
def ph_add_falcon(line):
"""
magic to add falcon to existing configuration
"""
t0 = tt.time()
phoenix.add_falcon()
print("elapsed time:", tt.time() - t0)
## enddef
@register_line_magic
def ph_load_falcon(line):
"""
magic to load falcon as sole detector
"""
t0 = tt.time()
phoenix.load_falcon()
print("elapsed time:", tt.time() - t0)
print("...... %ph_load_config ... to reload phoenix default configuration")
@register_line_magic
def ph_create_base_config(line):
"""
load base configuration for PHOENIX beamline
using phoenix.create_base_config()
phoenix_bec/device_configs/phoenix_devices.yaml
"""
t0 = tt.time()
phoenix.create_base_config()
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 170X50 -- bash -c "source /data/test/x07mb-test-bec/bec_deployment/bec_venv/bin/activate ; bec-server attach; exec bash"'
)
# #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.Phoenix(BL()
##
#####################################################################################
print("###############################################################")
print("init phoenix_bec/scripts/phoenix.py in two different ways")
print(" 1) phoenix_server = PhoenixBL() ... takes code from server version ")
print("SERVR VERSION DOES NOT WORK ANYMORE ")
print("FOLDER SCRIPT 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

View File

@ -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

View File

@ -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})

View File

@ -0,0 +1,413 @@
PH_TTL:
description: PHOENIX TTL trigger
deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger
deviceConfig:
prefix: 'X07MB-OP2:'
deviceTags:
- phoenix
- TTL Trigger
- phoenix_devices.yaml
onFailure: buffer
enabled: true
readoutPriority: monitored
softwareTrigger: true
###################################################
#
# phoenix standard devices (motors)
#
#
####################################################:
PH_Dummy:
description: Class to test functionality of PSI detector. reads/writes into X07MB-PC-PSCAN.P-P0D0"
deviceClass: phoenix_bec.devices.dummy_devices.Dummy_PSIDetector
deviceConfig:
prefix: 'X07MB-PC-PSCAN:'
name: 'Dummy_Detector_PSI_Detector'
deviceTags:
- phoenix
- phoenix_devices.yamllass
- reads channel X07MB-PC-PSCAN.P-P0D0
- Dummy class to test PSI detector c from DAQ GUI
onFailure: buffer
enabled: true
readoutPriority: monitored
softwareTrigger: false
############################
#
# MOTORS ES1
#
############################
ScanX:
readoutPriority: baseline
description: 'Vertical sample position ES-MA1.ScanX'
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: 'X07MB-ES-MA1:ScanX'
deviceTags:
- ES-MA1.ScanX
- phoenix_bec/device_configs/phoenix_devices.yaml
onFailure: retry
enabled: true
readOnly: false
ScanY:
readoutPriority: baseline
description: 'Horizontal sample position ES-MA1.ScanY'
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: 'X07MB-ES-MA1:ScanY'
deviceTags:
- ES-MA1.ScanY
- 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 SAI07
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
#
# END OF STANDARD CONFIG
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#

View File

@ -0,0 +1,112 @@
PH_TTL:
description: PHOENIX TTL trigger
deviceClass: phoenix_bec.devices.phoenix_trigger.PhoenixTrigger
deviceConfig:
prefix: 'X07MB-OP2:'
deviceTags:
- phoenix
- TTL Trigger
- phoenix_devices.yaml
onFailure: buffer
enabled: true
readoutPriority: monitored
softwareTrigger: true
###################################################
#
# phoenix standard devices (motors)
#
#
####################################################:
PH_Dummy:
description: Class to test functionality of PSI detector. reads/writes into X07MB-PC-PSCAN.P-P0D0"
deviceClass: phoenix_bec.devices.dummy_devices.Dummy_PSIDetector
deviceConfig:
prefix: 'X07MB-PC-PSCAN:'
name: 'Dummy_Detector_PSI_Detector'
deviceTags:
- phoenix
- phoenix_devices.yamllass
- reads channel X07MB-PC-PSCAN.P-P0D0
- Dummy class to test PSI detector c from DAQ GUI
onFailure: buffer
enabled: true
readoutPriority: monitored
softwareTrigger: false
############################
#
# MOTORS ES1
#
############################
ScanX:
readoutPriority: baseline
description: 'Vertical sample position ES-MA1.ScanX'
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: 'X07MB-ES-MA1:ScanX'
deviceTags:
- ES-MA1.ScanX
- phoenix_bec/device_configs/phoenix_devices.yaml
onFailure: retry
enabled: true
readOnly: false
ScanY:
readoutPriority: baseline
description: 'Horizontal sample position ES-MA1.ScanY'
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: 'X07MB-ES-MA1:ScanY'
deviceTags:
- ES-MA1.ScanY
- 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 SAI07
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
#
# END OF STANDARD CONFIG
#

View File

@ -0,0 +1,21 @@
#
# falcon without hdf5
#
falcon_hdf5:
description: Falcon detector x-ray fluoresence with hdf5 plugin from device class phoenix_bec.devices. falcon_phoenix.FalconPhoenix
deviceClass: phoenix_bec.devices.falcon_phoenix.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- with hdf5
- phoenix_falcon.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
#
# END FALCON with HDF5
#

View File

@ -0,0 +1,17 @@
#
# falcon without hdf5
#
falcon_nohdf5:
description: Falcon detector x-ray fluoresence II
deviceClass: phoenix_bec.devices.falcon_phoenix_no_hdf5.FalconPhoenix
deviceConfig:
prefix: 'X07MB-SITORO:'
deviceTags:
- phoenix
- falcon
- no hdf5
- phoenix_devices.yaml
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false

View File

@ -0,0 +1,18 @@
#
# Configuration XMAP without hdf5
#
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

View File

@ -0,0 +1 @@
from .phoenix_trigger import PhoenixTrigger

View File

@ -3,3 +3,30 @@
### phoenix_bec
| Device | Documentation | Module |
| :----- | :------------- | :------ |
| Dummy_PSIDetector | <br> Abstract base class for SLS detectors<br><br> Class attributes:<br> custom_prepare_cls (object): class for custom prepare logic (BL specific)<br><br> Args:<br> prefix (str): EPICS PV prefix for component (optional)<br> name (str): name of the device, as will be reported via read()<br> kind (str): member of class 'ophydobj.Kind', defaults to Kind.normal<br> omitted -> reado_PSIDetectorBase<br> | [phoenix_bec.devices.dummy_devices](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/dummy_devices.py) |
| EpicsDXPFalcon | <br> DXP parameters for Falcon detector<br><br> Base class to map EPICS PVs from DXP parameters to ophyd signals.<br> | [phoenix_bec.devices.falcon_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/falcon_phoenix_no_hdf5.py) |
| EpicsDXPXMAP | <br> DXP parameters for XMAP detector<br><br> Base class to map EPICS PVs from DXP parameters to ophyd signals.<br> | [phoenix_bec.devices.xmap_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/xmap_phoenix_no_hdf5.py) |
| EpicsMCARecordExtended | | [phoenix_bec.devices.falcon_phoenix](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/falcon_phoenix.py) |
| FalconcSAXS | <br> Falcon Sitoro detector for CSAXS<br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector<br> mca (EpicsMCARecord) : MCA parameters for Falcon detector<br> hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector<br> MIN_READOUT (float) : Minimum readout time for the detector<br> | [phoenix_bec.devices.falcon_csaxs_original](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/falcon_csaxs_original.py) |
| FalconHDF5Plugins | <br> HDF5 parameters for Falcon detector<br><br> Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.<br> | [phoenix_bec.devices.falcon_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/falcon_phoenix_no_hdf5.py) |
| FalconPhoenix | <br> Falcon detector for phoenix<br> custom_prepare_cls (XMAPSetu<br> custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> in __init__ of PSIDetecor base<br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector<br> mca (EpicsMCARecord) : MCA parameters for XMAP detector<br> hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector<br> MIN_READOUT (float) : Minimum readout time for the detector<br> | [phoenix_bec.devices.falcon_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/falcon_phoenix_no_hdf5.py) |
| PhoenixTrigger | <br> Class for PHOENIX TTL hardware trigger: 'X07MB-OP2:'<br><br> This device is used to trigger communicate with an ADC card<br> that creates TTL signals to trigger cameras and detectors at Phoenix.<br><br> | [phoenix_bec.devices.phoenix_trigger](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/phoenix_trigger.py) |
| ROI | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXP | All high-level DXP parameters for each channel | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXP_OLD | <br> DXP parameters for Sitoro detector<br><br> Base class to map EPICS PVs from DXP parameters to ophyd signals.<br> | [phoenix_bec.devices.sitoro_phoenix](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro_phoenix.py) |
| SitoroEpicsDXPBaseSystem | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXPLowLevel | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXPLowLevelParameter | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXPMapping | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsDXPMultiElementSystem | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsMCA | mca records with extras from mca.db | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsMCACallback | Callback-related signals for MCA devices | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsMCAReadNotify | mca record with extras from mcaReadNotify.db | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsMCARecord | SynApps MCA Record interface | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroEpicsMCARecordExtended_OLD | | [phoenix_bec.devices.sitoro_phoenix](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro_phoenix.py) |
| SitoroHDF5Plugins | <br> HDF5 parameters for Sitoro detector<br><br> Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.<br> | [phoenix_bec.devices.sitoro_phoenix](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro_phoenix.py) |
| SitoroPhoenix | <br> Sitoro Sitoro detector for Phoenix<br><br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (SitoroSetup) : Custom detector setup class,<br> inherits from CustomDetectorMixin<br><br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> dxp1, .. dxpi, .. , dxpN (SitoroEpicsDXP) : DXP parameters for Sitoro detector Nr i<br> mca1, .. mcai, .. , mcaN (SitoroEpicsMCARecord) : MCA parameters for Sitoro detector Nr i<br><br> hdf5 (SitoroHDF5Plugins) : HDF5 parameters for Sitoro detector<br> MIN_READOUT (float) : Minimum readout time for the detector<br><br><br><br> | [phoenix_bec.devices.sitoro_phoenix](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro_phoenix.py) |
| SitoroSoftDXPTrigger | Simple soft trigger for DXP devices<br><br> Parameters<br> ----------<br> count_signal : str, optional<br> Signal to set acquisition time (default: 'preset_real_time')<br> preset_mode : str, optional<br> Default preset mode for the stage signals (default: 'Real time')<br> mode_signal : str, optional<br> Preset mode signal attribute (default 'preset_mode')<br> stop_signal : str, optional<br> Stop signal attribute (default 'stop_all')<br> | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| SitoroTest | | [phoenix_bec.devices.sitoro](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/sitoro.py) |
| XMAPHDF5Plugins | <br> HDF5 parameters for XMAP detector<br><br> Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.<br> | [phoenix_bec.devices.xmap_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/xmap_phoenix_no_hdf5.py) |
| XMAPPhoenix | MCA<br> XMAP detector for phoenix<br> custom_prepare_cls (XMAPSetu<br> custom_prepare_cls (XMAPSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> in __init__ of PSIDetecor base<br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> dxp (EpicsDXPXMAP) : DXP parameters for XMAP detector<br> mca (EpicsMCARecord) : MCA parameters for XMAP detector<br> hdf5 (XMAPHDF5Plugins) : HDF5 parameters for XMAP detector<br> MIN_READOUT (float) : Minimum readout time for the detector<br> | [phoenix_bec.devices.xmap_phoenix_no_hdf5](https://gitlab.psi.ch/bec/phoenix_bec/-/blob/main/phoenix_bec/devices/xmap_phoenix_no_hdf5.py) |

View File

@ -0,0 +1,499 @@
"""
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
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO
from ophyd import FormattedComponent as FCpt
from ophyd import Kind
from ophyd.device import Staged
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
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 phoenix_bec.scripts.phoenix import PhoenixBL
logger = bec_logger.logger
# class LogTime():
# def __init__(self):
# self.t0=time.time()
# 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
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."""
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): 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
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):
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)
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()
# .. prepare my own log file
self.p_s("Dummy_device Dummy_PSIDetector.__init__ .. done ")
def _update_filewriter(self) -> None:
"""Update filewriter with service config"""
self.p_s("Dummy_device Dummy_PSIDetector._update_filewriter")
self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector)
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
"""
self.p_s("Dummy_device Dummy_PSIDetector._update_scaninfo")
self.scaninfo = BecScaninfoMixin(self.device_manager)
self.scaninfo.load_scan_metadata()
self.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
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(".")}
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."""
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
self.p_s("Dummy_device Dummy_PSIDetector.check_scan_id .. done ")
def _init(self) -> None:
"""Initialize detector, filewriter and set default parameters"""
self.p_s("Dummy_device Dummy_PSIDetector._init")
self.custom_prepare.on_init()
self.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
"""
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()
self.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.
"""
self.p_s("Dummy_device Dummy_PSIDetector.pre_scan")
self.custom_prepare.on_pre_scan()
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
self.p_s("Dummy_device Dummy_PSIDetector.trigger")
status = self.custom_prepare.on_trigger()
if isinstance(status, DeviceStatus):
return status
self.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
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()
self.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
"""
self.p_s("Dummy_device Dummy_PSIDetector.unstage")
self.check_scan_id()
self.custom_prepare.on_unstage()
self.stopped = False
self.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
"""
self.p_s("Dummy_device Dummy_PSIDetector.stop")
self.custom_prepare.on_stop()
super().stop(success=success)
self.stopped = True
self.p_s("Dummy_device Dummy_PSIDetector.stop ... done")

View File

@ -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)

View File

@ -0,0 +1,468 @@
"""
Implementation for falcon at PHOENIX, derived from
implementation on csaxs (file falcon_csaxs.py)
17.10.2024 try to streamline implementation with mca record
Differences to implement
1) we consider EPICS initialization as standard implementaion,
so no reinitialization when bec device is initrialized ... DONE ...
2) in EpicsDXPFalcon(Device) add ICR and OCR for individual detectors
3) can we make this generic to make it suited for both falcon and XMAP ?
3) make easy switching between mca spectra an mca mapping
fix defiend relation bwetween variables and names used here for example DONE
aquiring is currently called 'state' --> should be renamed to aquiring
Currently state = Cpt(EpicsSignal, "Acquiring")
should be acquiring = Cpt(EpicsSignal, "Acquiring")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
def arm_aquisition
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
CHANGES LOG and
System as taken from cSAXS some times works for one element need about 7 second
There seem to be still serious timout issues
changes log
TIMEOUT_FOR_SIGNALs from 5 to 10
"""
import enum
import threading
import time
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 # old import
# now import ophyd.mca completely
import ophyd.mca as Mca
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 EpicsMCARecordExtended(Mca.EpicsMCARecord):
# add parameters for detector energy calibration
# which are missing in mca.py
calo = Cpt(EpicsSignal, ".CALO")
cals = Cpt(EpicsSignal, ".CALS")
calq = Cpt(EpicsSignal, ".CALQ")
tth = Cpt(EpicsSignal, ".TTH")
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")
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 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 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"""
pass
"""
THIS IS THE OLD CSACS CODE. uncomment for now, as we consider EPICS as the boss
for initialization
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.acquiring.read()[self.parent.acquiring.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)
time.sleep(0.5)
self.parent.erase_all.put(1)
time.sleep(0.5)
signal_conditions = [
(
lambda: self.parent.acquiring.read()[self.parent.acquiring.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 FalconPhoenix(PSIDetectorBase):
"""
Falcon Sitoro detector for Phoenix
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 = 1
# specify class attributes
# Parameters for individual detector elements
# Note: need to wrote 'dxp: here, but not dxp'
# dxp1 = Cpt(Mca.EpicsDXP, "dxp1:")
dxp1 = Cpt(EpicsDXPFalcon, "dxp1:")
# dxp2 = Cpt(EpicsDXPFalcon, "dxp2:")
# dxp3 = Cpt(EpicsDXPFalcon, "dxp3:")
# dxp4 = Cpt(EpicsDXPFalcon, "dxp4:")
#
# THIS IS NOT WELL-DONE as it take out one part of mca.py from ophy.py
#
#
mca1 = Cpt(EpicsMCARecordExtended, "mca1")
# mca2 = Cpt(EpicsMCARecordExtended, "mca2")
# mca3 = Cpt(EpicsMCARecordExtended, "mca3")
# mca4 = Cpt(EpicsMCARecordExtended, "mca4")
# need to write 'mca1', but not 'mca1:'
# mca1 = Cpt(EpicsMCARecord, "mca1")
# mca2 = Cpt(EpicsMCARecord, "mca2")
# mca3 = Cpt(EpicsMCARecord, "mca3")
# mca4 = Cpt(EpicsMCARecord, "mca4")
# other general parameters
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
stop_all = Cpt(EpicsSignal, "StopAll")
erase_all = Cpt(EpicsSignal, "EraseAll")
start_all = Cpt(EpicsSignal, "StartAll")
# state = Cpt(EpicsSignal, "Acquiring") # <-- This is from cSAX implementation
acquiring = 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")
# _________________ General Epic parameters
# changes Oct 2024
# triggers--> max_triggers,
# events-->max_events
# input_count_rate--> max_input_count_rate
# output_count_rate--> max_output_count_rate
max_triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
max_events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
max_input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
max_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")
# print(pixel_per_run
# if "SITORO" in prefix:
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
# endif
if __name__ == "__main__":
falcon = FalconPhoenix(name="falcon_hdf5", prefix="X07MB-SITORO:", sim_mode=True)

View File

@ -0,0 +1,367 @@
#
#
# 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.SPECTRUM, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# 1 Realtime
self.parent.preset_mode.put(0)
# 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 on_trigger(self) -> None:
"""Actions on pre_scan. This is performed AFTER stage, just before scan_core"""
self.arm_acquisition()
def prepare_detector(self) -> None:
"""Prepare detector for acquisition"""
self.set_trigger(
mapping_mode=MappingSource.SPECTRUM, 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 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"]
# 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 = FalconPhoenix(name="falcon", prefix="X07MB-SITORO:", sim_mode=True)

View File

@ -0,0 +1,244 @@
"""
Module for the PhoenixTrigger class to connect to the ADC card
that creates TTL signals to trigger cameras and detectors at Phoenix.
TO Do
-- allow for variable dwell times
-- add erase/Start for XMAP and FALCON
-- check values for time.sleep()
-- check in on_triggerthe status check for Falcon
-- rework togther with Xiaoquiang the functionality of involved EPICS channels
(Callbacks etc.) to minimize the need of sleeping times.
"""
import enum
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import DeviceStatus, EpicsSignal, EpicsSignalRO, Kind
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
DETECTOR_TIMEOUT = 5
class PhoenixTriggerError(Exception):
"""PhoenixTrigger specific error"""
class SAMPLING(int, enum.Enum):
"""Sampling Done PV
This class serves redabilty of missinx class and ensure correct setting of
certain conditions.
defiend preset values for certain states to be called in the
mixing class, where we for example check whether the sampling is done of not
by comparing with SAMPLING.DONE
xxx==SAMPLING.DONE
"""
RUNNING = 0
DONE = 1
class PhoenixTriggerSetup(CustomDetectorMixin):
"""
Mixin Class to setup the PhoenixTrigger device
"""
def on_stage(self) -> None:
"""
On stage actions which are executed upon staging the device
"""
if self.parent.scaninfo.scan_type == "step": # check whether we use step scanning
###############
# next lines ensure that TTL trigger is on single sampling mode (posible )
##############
self.parent.start_csmpl.set(0)
time.sleep(0.1)
self.parent.total_cycles.set(1)
self.parent.smpl.put(1)
time.sleep(0.5)
#####
# set sampling to dwell time of scan
######
self.parent.total_cycles.set(np.ceil(self.parent.scaninfo.exp_time * 5))
logger.info(f"Device {self.parent.name} was staged for step scan")
def on_unstage(self) -> None:
"""On unstage actions which are executed upon unstaging the device"""
self.on_stop()
def on_trigger(self) -> DeviceStatus:
"""On trigger actions which are executed upon triggering the device"""
# TODO Test the proper check for the falcon state
# Check first that falcon is set to acquiring
falcon = self.parent.device_manager.devices.get("falcon_nohdf5", None) # get device
timeout = 1
if falcon is not None:
# TODO Check that falcon.state.get() == 1 is the correct check.
# --> When is the falcon acquiring, this assumes 1?
# self.wait_for_signals is defined in PSI_detector_base.CustomDetectorMixin
#
if not self.wait_for_signals([(falcon.state.get, 1)], timeout=timeout):
raise PhoenixTriggerError(
f"Device {self.parent.name} is not ready to take trigger, timeout due to waiting for Falcon to get ready. Timeout after {timeout}s"
)
if self.parent.scaninfo.scan_type == "step":
time.sleep(0.2)
self.parent.smpl.put(1)
# Minimum of 1 cycle has to be waited. Cycle == 0.2s
time.sleep(0.2)
# Trigger function from ophyd.Device returns a DeviceStatus. This function
# starts a process that creates a DeviceStatus, and waits for the signal_conditions
# self.parent.smpl_done.get to change to the value SAMPLING.DONE
# Once this takes place, the DeviceStatus.done flag will be set to True.
# When BEC calls trigger() on the devices, this method will be called assuming that
# the devices config softwareTrigger=True is set.
# In ScanBase, the _at_each_point function calls
# self.stubs.wait(wait_type="trigger", group="trigger", wait_time=self.exp_time)
# which ensures that the DeviceStatus object resolves before continuing,
# i.e. DeviceStatus.done = True
status = self.wait_with_status(
signal_conditions=[(self.parent.smpl_done.get, SAMPLING.DONE)],
timeout=5 * self.parent.scaninfo.exp_time, # Check if timeout is appropriate
check_stopped=True,
)
# explanation of last line (self.parent.smpl_done.get, SAMPLINGDONE.DONE)
# creates a tuple which defines a
# condition, which is tested in self.wait_with_status, here it tests for :
# (self.parent.smpl_done.get() == SAMPLINGDONE.DONE),
# where SAMPLINGDONE.DONE =1, as set in code above
# As this is in mixing class (PhoenixtriggerSetup), parent.sample_done is defined in
# main class as smpl_done = Cpt(EpicsSignalRO, "SMPL-DONE", kind=Kind.config)
return status # should this be in if clause level or outside?
# endif
def on_stop(self) -> None:
"""
Actions to stop the Device
Here the device is switched back to continuous sampling.
"""
self.parent.total_cycles.set(5)
self.parent.start_csmpl.set(1)
time.sleep(0.5)
self.parent.smpl.put(1)
time.sleep(0.5)
self.parent.smpl.put(1)
time.sleep(0.2)
if self.parent.smpl_done.get() == SAMPLING.RUNNING:
return
self.parent.smpl.put(1)
class PhoenixTrigger(PSIDetectorBase):
"""
Class for PHOENIX TTL hardware trigger: 'X07MB-OP2:'
This device is used to trigger communicate with an ADC card
that creates TTL signals to trigger cameras and detectors at Phoenix.
"""
##################################################################
#
# The Variable USER_ACCESS contains an ascii list of functions which will be
# visible in dev.TTL. Here, this list is empty.
# note that components are alway public
#
##################################################################
USER_ACCESS = []
####################################################################
#
# # specify Setup class into variable custom_prepare_cls
#
####################################################################
custom_prepare_cls = PhoenixTriggerSetup
###################################################################
# in __init__ of PSIDetectorBase will the initialzed by
# self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs)
# making the instance of PSIDetectorBase availble to functions
# in PhoenixTriggerSetup.
#
# This inherits, among other things, the input parameters, such as
# the 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
#
# then Cpt will construct - magically- the
# Epics channel name = prefix+suffix,
#
# for example
# 'X07MB-OP2:' + 'START-CSMPL' -> 'X07MB-OP2:' + 'START-CSMPL'
#
# to construct names, for now we keep the convention to derive
# them from the EPICS NAMES (such as ) X07MB-OP2:SMPL-DONE --> smpl_done
#
# this mean access to channel using dev.PH_TTL.smpl_done.get()
#
#
###################################################################
start_csmpl = Cpt(
EpicsSignal, "START-CSMPL", kind=Kind.config, put_complete=True
) # cont on / off
intr_count = Cpt(
EpicsSignal, "INTR-COUNT", kind=Kind.config, put_complete=True
) # conter run up
total_cycles = Cpt(
EpicsSignal, "TOTAL-CYCLES", kind=Kind.config, put_complete=True
) # cycles set
smpl = Cpt(
EpicsSignal, "SMPL", kind=Kind.config, put_complete=True
) # start sampling --> aquire
smpl_done = Cpt(
EpicsSignalRO, "SMPL-DONE", kind=Kind.config
) # show trigger is done, consider using string=True
if __name__ == "__main__":
# Test the PhoenixTrigger class
trigger = PhoenixTrigger(name="trigger", prefix="X07MB-OP2:")
trigger.wait_for_connection(all_signals=True)
trigger.read()
trigger.read_configuration()
trigger.stage()
device_status = trigger.trigger()
device_status.wait()
trigger.unstage()

View File

@ -0,0 +1,431 @@
"""
Base implementation for Sitoro Falcon
This is based on ophyd.mca.py
All relevant classes are renames by putting Sitoro ahead of the class name
eg. EpicsMCARecord(Device): --> SitoroEpicsMCARecord(Device)
fundamentally on could use
class SitoroEpicsMCARecord(Device):
class SitoroEpicsMCA(SitoroEpicsMCARecord):
class SitoroEpicsMCAReadNotify(SitoroEpicsMCARecord):
class SitoroEpicsMCAReadNotify(SitoroEpicsMCARecord):
class SitoroEpicsMCACallback(Device):
class SitoroEpicsDXP(Device):
class SitoroEpicsDXPLowLevelParameter(Device):
class SitoroEpicsDXPLowLevel(Device):
class SitoroEpicsDXPMapping(Device):
class SitoroEpicsDXPBaseSystem(Device):
class SitoroEpicsDXPMultiElementSystem(SitoroEpicsDXPBaseSystem):
class SitoroSoftDXPTrigger(Device):
"""
import logging
from collections import OrderedDict
from ophyd.areadetector import EpicsSignalWithRBV as SignalWithRBV
from ophyd.device import Component as Cpt
from ophyd.device import Device
from ophyd.device import DynamicDeviceComponent as DDC
from ophyd.device import Kind
from ophyd.signal import EpicsSignal, EpicsSignalRO, Signal
logger = logging.getLogger(__name__)
class ROI(Device): # must keep name
# 'name' is not an allowed attribute
label = Cpt(EpicsSignal, "NM", lazy=True)
count = Cpt(EpicsSignalRO, "", lazy=True)
net_count = Cpt(EpicsSignalRO, "N", lazy=True)
preset_count = Cpt(EpicsSignal, "P", lazy=True)
is_preset = Cpt(EpicsSignal, "IP", lazy=True)
bkgnd_chans = Cpt(EpicsSignal, "BG", lazy=True)
hi_chan = Cpt(EpicsSignal, "HI", lazy=True)
lo_chan = Cpt(EpicsSignal, "LO", lazy=True)
def __init__(
self, prefix, *, read_attrs=None, configuration_attrs=None, name=None, parent=None, **kwargs
):
super().__init__(
prefix,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
name=name,
parent=parent,
**kwargs,
)
def add_rois(range_, **kwargs): # must keep name
"""Add one or more ROIs to an MCA instance
Parameters
----------
range_ : sequence of ints
Must be be in the set [0,31]
By default, an EpicsMCA is initialized with all 32 rois.
These provide the following Components as EpicsSignals (N=[0,31]):
EpicsMCA.rois.roiN.(label,count,net_count,preset_cnt, is_preset,
bkgnd_chans, hi_chan, lo_chan)
"""
defn = OrderedDict()
for roi in range_:
if not (0 <= roi < 32):
raise ValueError("roi must be in the set [0,31]")
attr = "roi{}".format(roi)
defn[attr] = (ROI, ".R{}".format(roi), kwargs)
return defn
class SitoroEpicsMCARecord(Device):
"""SynApps MCA Record interface"""
stop_signal = Cpt(EpicsSignal, ".STOP", kind="omitted")
preset_real_time = Cpt(EpicsSignal, ".PRTM", kind=Kind.config | Kind.normal)
preset_live_time = Cpt(EpicsSignal, ".PLTM", kind="omitted")
elapsed_real_time = Cpt(EpicsSignalRO, ".ERTM")
elapsed_live_time = Cpt(EpicsSignalRO, ".ELTM", kind="omitted")
spectrum = Cpt(EpicsSignalRO, ".VAL")
background = Cpt(EpicsSignalRO, ".BG", kind="omitted")
mode = Cpt(EpicsSignal, ".MODE", string=True, kind="omitted")
rois = DDC(add_rois(range(0, 32), kind="omitted"), kind="omitted")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# could arguably be made a configuration_attr instead...
self.stage_sigs["mode"] = "PHA"
def stop(self, *, success=False):
self.stop_signal.put(1)
class SitoroEpicsMCA(SitoroEpicsMCARecord):
"""mca records with extras from mca.db"""
start = Cpt(EpicsSignal, "Start", kind="omitted")
stop_signal = Cpt(EpicsSignal, "Stop", kind="omitted")
erase = Cpt(EpicsSignal, "Erase", kind="omitted")
erase_start = Cpt(EpicsSignal, "EraseStart", trigger_value=1, kind="omitted")
check_acquiring = Cpt(EpicsSignal, "CheckACQG", kind="omitted")
client_wait = Cpt(EpicsSignal, "ClientWait", kind="omitted")
enable_wait = Cpt(EpicsSignal, "EnableWait", kind="omitted")
force_read = Cpt(EpicsSignal, "Read", kind="omitted")
set_client_wait = Cpt(EpicsSignal, "SetClientWait", kind="omitted")
status = Cpt(EpicsSignal, "Status", kind="omitted")
when_acq_stops = Cpt(EpicsSignal, "WhenAcqStops", kind="omitted")
why1 = Cpt(EpicsSignal, "Why1", kind="omitted")
why2 = Cpt(EpicsSignal, "Why2", kind="omitted")
why3 = Cpt(EpicsSignal, "Why3", kind="omitted")
why4 = Cpt(EpicsSignal, "Why4", kind="omitted")
class SitoroEpicsMCAReadNotify(SitoroEpicsMCARecord):
"""mca record with extras from mcaReadNotify.db"""
start = Cpt(EpicsSignal, "Start", kind="omitted")
stop_signal = Cpt(EpicsSignal, "Stop", kind="omitted")
erase = Cpt(EpicsSignal, "Erase", kind="omitted")
erase_start = Cpt(EpicsSignal, "EraseStart", trigger_value=1, kind="omitted")
check_acquiring = Cpt(EpicsSignal, "CheckACQG", kind="omitted")
client_wait = Cpt(EpicsSignal, "ClientWait", kind="omitted")
enable_wait = Cpt(EpicsSignal, "EnableWait", kind="omitted")
force_read = Cpt(EpicsSignal, "Read", kind="omitted")
set_client_wait = Cpt(EpicsSignal, "SetClientWait", kind="omitted")
status = Cpt(EpicsSignal, "Status", kind="omitted")
class SitoroEpicsMCACallback(Device):
"""Callback-related signals for MCA devices"""
read_callback = Cpt(EpicsSignal, "ReadCallback")
read_data_once = Cpt(EpicsSignal, "ReadDataOnce")
read_status_once = Cpt(EpicsSignal, "ReadStatusOnce")
collect_data = Cpt(EpicsSignal, "CollectData")
class SitoroEpicsDXP(Device):
"""All high-level DXP parameters for each channel"""
preset_mode = Cpt(EpicsSignal, "PresetMode", string=True)
live_time_output = Cpt(SignalWithRBV, "LiveTimeOutput", string=True)
# elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
# elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
# elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
# Trigger Filter PVs
trigger_peaking_time = Cpt(SignalWithRBV, "TriggerPeakingTime")
trigger_threshold = Cpt(SignalWithRBV, "TriggerThreshold")
trigger_gap_time = Cpt(SignalWithRBV, "TriggerGapTime")
trigger_output = Cpt(SignalWithRBV, "TriggerOutput", string=True)
max_width = Cpt(SignalWithRBV, "MaxWidth")
# Energy Filter PVs
peaking_time = Cpt(SignalWithRBV, "PeakingTime")
energy_threshold = Cpt(SignalWithRBV, "EnergyThreshold")
gap_time = Cpt(SignalWithRBV, "GapTime")
# Baseline PVs
# baseline_cut_percent = Cpt(SignalWithRBV, "BaselineCutPercent")
# baseline_cut_enable = Cpt(SignalWithRBV, "BaselineCutEnable")
# baseline_filter_length = Cpt(SignalWithRBV, "BaselineFilterLength")
# baseline_threshold = Cpt(SignalWithRBV, "BaselineThreshold")
# baseline_energy_array = Cpt(EpicsSignal, "BaselineEnergyArray")
# baseline_histogram = Cpt(EpicsSignal, "BaselineHistogram")
# baseline_threshold = Cpt(SignalWithRBV, "BaselineThreshold")
# Misc PVs
preamp_gain = Cpt(SignalWithRBV, "PreampGain")
detector_polarity = Cpt(SignalWithRBV, "DetectorPolarity")
reset_delay = Cpt(SignalWithRBV, "ResetDelay")
decay_time = Cpt(SignalWithRBV, "DecayTime")
max_energy = Cpt(SignalWithRBV, "MaxEnergy")
adc_percent_rule = Cpt(SignalWithRBV, "ADCPercentRule")
max_width = Cpt(SignalWithRBV, "MaxWidth")
# read-only diagnostics
triggers = Cpt(EpicsSignalRO, "Triggers", lazy=True)
events = Cpt(EpicsSignalRO, "Events", lazy=True)
overflows = Cpt(EpicsSignalRO, "Overflows", lazy=True)
underflows = Cpt(EpicsSignalRO, "Underflows", lazy=True)
input_count_rate = Cpt(EpicsSignalRO, "InputCountRate", lazy=True)
output_count_rate = Cpt(EpicsSignalRO, "OutputCountRate", lazy=True)
mca_bin_width = Cpt(EpicsSignalRO, "MCABinWidth_RBV")
calibration_energy = Cpt(EpicsSignalRO, "CalibrationEnergy_RBV")
current_pixel = Cpt(EpicsSignal, "CurrentPixel")
dynamic_range = Cpt(EpicsSignalRO, "DynamicRange_RBV")
# Preset options
preset_events = Cpt(SignalWithRBV, "PresetEvents")
preset_mode = Cpt(SignalWithRBV, "PresetMode", string=True)
preset_triggers = Cpt(SignalWithRBV, "PresetTriggers")
# Trace options
trace_data = Cpt(EpicsSignal, "TraceData")
trace_mode = Cpt(SignalWithRBV, "TraceMode", string=True)
trace_time_array = Cpt(EpicsSignal, "TraceTimeArray")
trace_time = Cpt(SignalWithRBV, "TraceTime")
class SitoroEpicsDXPLowLevelParameter(Device):
param_name = Cpt(EpicsSignal, "Name")
value = Cpt(SignalWithRBV, "Val")
class SitoroEpicsDXPLowLevel(Device):
num_low_level_params = Cpt(EpicsSignal, "NumLLParams")
read_low_level_params = Cpt(EpicsSignal, "ReadLLParams")
parameter_prefix = "LL{}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._parameter_cache = {}
def get_low_level_parameter(self, index):
"""Get a DXP low level parameter
Parameters
----------
index : int
In the range of [0, 229]
Returns
-------
param : EpicsDXPLowLevelParameter
"""
try:
return self._parameter_cache[index]
except KeyError:
pass
prefix = "{}{}".format(self.prefix, self.parameter_prefix)
name = "{}_param{}".format(self.name, index)
param = EpicsDXPLowLevelParameter(prefix, name=name)
self._parameter_cache[index] = param
return param
class SitoroEpicsDXPMapping(Device):
apply = Cpt(EpicsSignal, "Apply")
auto_apply = Cpt(SignalWithRBV, "AutoApply")
auto_pixels_per_buffer = Cpt(SignalWithRBV, "AutoPixelsPerBuffer")
buffer_size = Cpt(EpicsSignalRO, "BufferSize_RBV")
collect_mode = Cpt(SignalWithRBV, "CollectMode")
ignore_gate = Cpt(SignalWithRBV, "IgnoreGate")
input_logic_polarity = Cpt(SignalWithRBV, "InputLogicPolarity")
list_mode = Cpt(SignalWithRBV, "ListMode")
mbytes_read = Cpt(EpicsSignalRO, "MBytesRead_RBV")
next_pixel = Cpt(EpicsSignal, "NextPixel")
pixel_advance_mode = Cpt(SignalWithRBV, "PixelAdvanceMode")
pixels_per_buffer = Cpt(SignalWithRBV, "PixelsPerBuffer")
pixels_per_run = Cpt(SignalWithRBV, "PixelsPerRun")
read_rate = Cpt(EpicsSignalRO, "ReadRate_RBV")
sync_count = Cpt(SignalWithRBV, "SyncCount")
class SitoroEpicsDXPBaseSystem(Device):
channel_advance = Cpt(EpicsSignal, "ChannelAdvance")
client_wait = Cpt(EpicsSignal, "ClientWait")
dwell = Cpt(EpicsSignal, "Dwell")
max_scas = Cpt(EpicsSignal, "MaxSCAs")
num_scas = Cpt(SignalWithRBV, "NumSCAs")
poll_time = Cpt(SignalWithRBV, "PollTime")
prescale = Cpt(EpicsSignal, "Prescale")
save_system = Cpt(SignalWithRBV, "SaveSystem")
save_system_file = Cpt(EpicsSignal, "SaveSystemFile")
set_client_wait = Cpt(EpicsSignal, "SetClientWait")
class SitoroTest(Device):
preset_mode = Cpt(EpicsSignal, "PresetMode", string=True)
class SitoroEpicsDXPMultiElementSystem(SitoroEpicsDXPBaseSystem):
# Preset info
preset_mode = Cpt(EpicsSignal, "PresetMode", string=True)
preset_real_time = Cpt(EpicsSignal, "PresetReal")
preset_events = Cpt(EpicsSignal, "PresetEvents")
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
mca_refresh_period = Cpt(EpicsSignal, "MCARefreshPeriod")
# preset_live_time = Cpt(EpicsSignal, "PresetLive")
# Acquisition
erase_all = Cpt(EpicsSignal, "EraseAll")
erase_start = Cpt(EpicsSignal, "EraseStart", trigger_value=1)
start_all = Cpt(EpicsSignal, "StartAll")
stop_all = Cpt(EpicsSignal, "StopAll")
# Status
set_acquire_busy = Cpt(EpicsSignal, "SetAcquireBusy") # -- not working
acquire_busy = Cpt(EpicsSignal, "AcquireBusy") # -- not working
status_all = Cpt(EpicsSignal, "StatusAll") # -- not working
status_all_once = Cpt(EpicsSignal, "StatusAllOnce") # -- not working
acquiring = Cpt(EpicsSignalRO, "Acquiring") # -- not working
# Reading
# read_baseline_histograms = Cpt(EpicsSignal, "ReadBaselineHistograms")
read_all = Cpt(EpicsSignal, "ReadAll") # -- not working
read_all_once = Cpt(EpicsSignal, "ReadAllOnce") # -- not working
# As a debugging note, if snl_connected is not '1', your IOC is
# misconfigured:
snl_connected = Cpt(EpicsSignal, "SNL_Connected")
"""
# Copying to individual elements
copy_adcp_ercent_rule = Cpt(EpicsSignal, "CopyADCPercentRule")
#copy_baseline_cut_enable = Cpt(EpicsSignal, "CopyBaselineCutEnable")
#copy_baseline_cut_percent = Cpt(EpicsSignal, "CopyBaselineCutPercent")
#copy_baseline_filter_length = Cpt(EpicsSignal, "CopyBaselineFilterLength")
#copy_baseline_threshold = Cpt(EpicsSignal, "CopyBaselineThreshold")
copy_decay_time = Cpt(EpicsSignal, "CopyDecayTime")
copy_detector_polarity = Cpt(EpicsSignal, "CopyDetectorPolarity")
copy_energy_threshold = Cpt(EpicsSignal, "CopyEnergyThreshold")
copy_gap_time = Cpt(EpicsSignal, "CopyGapTime")
copy_max_energy = Cpt(EpicsSignal, "CopyMaxEnergy")
copy_max_width = Cpt(EpicsSignal, "CopyMaxWidth")
copy_peaking_time = Cpt(EpicsSignal, "CopyPeakingTime")
copy_preamp_gain = Cpt(EpicsSignal, "CopyPreampGain")
copy_roic_hannel = Cpt(EpicsSignal, "CopyROIChannel")
copy_roie_nergy = Cpt(EpicsSignal, "CopyROIEnergy")
copy_roi_sca = Cpt(EpicsSignal, "CopyROI_SCA")
copy_reset_delay = Cpt(EpicsSignal, "CopyResetDelay")
copy_trigger_gap_time = Cpt(EpicsSignal, "CopyTriggerGapTime")
copy_trigger_peaking_time = Cpt(EpicsSignal, "CopyTriggerPeakingTime")
copy_trigger_threshold = Cpt(EpicsSignal, "CopyTriggerThreshold")
# do_* executes the process:
do_read_all = Cpt(EpicsSignal, "DoReadAll")
#do_read_baseline_histograms = Cpt(EpicsSignal, "DoReadBaselineHistograms")
do_read_traces = Cpt(EpicsSignal, "DoReadTraces")
do_status_all = Cpt(EpicsSignal, "DoStatusAll")
"""
# Time
# dead_time = Cpt(EpicsSignal, "DeadTime")
# elapsed_live = Cpt(EpicsSignal, "ElapsedLive")
# elapsed_real = Cpt(EpicsSignal, "ElapsedReal")
# low-level
# read_low_level_params = Cpt(EpicsSignal, "ReadLLParams")
# Traces
# read_traces = Cpt(EpicsSignal, "ReadTraces")
# trace_modes = Cpt(EpicsSignal, "TraceModes", string=True)
# trace_times = Cpt(EpicsSignal, "TraceTimes")
class SitoroSoftDXPTrigger(Device):
"""Simple soft trigger for DXP devices
Parameters
----------
count_signal : str, optional
Signal to set acquisition time (default: 'preset_real_time')
preset_mode : str, optional
Default preset mode for the stage signals (default: 'Real time')
mode_signal : str, optional
Preset mode signal attribute (default 'preset_mode')
stop_signal : str, optional
Stop signal attribute (default 'stop_all')
"""
count_time = Cpt(Signal, value=None, doc="bluesky count time")
def __init__(
self,
*args,
count_signal="preset_real_time",
stop_signal="stop_all",
mode_signal="preset_mode",
preset_mode="Real time",
**kwargs,
):
super().__init__(*args, **kwargs)
self._status = None
self._count_signal = getattr(self, count_signal)
stop_signal = getattr(self, stop_signal)
self.stage_sigs[stop_signal] = 1
mode_signal = getattr(self, mode_signal)
self.stage_sigs[mode_signal] = preset_mode
def stage(self):
if self.count_time.get() is None:
# remove count_time from the stage signals if count_time unset
try:
del self.stage_sigs[self._count_signal]
except KeyError:
pass
else:
self.stage_sigs[self._count_signal] = self.count_time.get()
super().stage()

View File

@ -0,0 +1,485 @@
"""
Implementation for falcon at PHOENIX, derived from
implementation on csaxs (file falcon_csaxs.py)
18.10.2024 further development of falcon_phoenix.y to phoenix to sitoro_phoenix.py
Now we use the definition of all EPICS channels for falcon as defined in the classes in sitoro.py
WIP......
17.10.2024 try to streamline implementation with mca record
Differences to implement
1) we consider EPICS initialization as standard implementaion,
so no reinitialization when bec device is initrialized ... DONE ...
2) in EpicsDXPFalcon(Device) add ICR and OCR for individual detectors
3) can we make this generic to make it suited for both falcon and XMAP ?
3) make easy switching between mca spectra an mca mapping
fix defiend relation bwetween variables and names used here for example DONE
aquiring is currently called 'state' --> should be renamed to aquiring
Currently state = Cpt(EpicsSignal, "Acquiring")
should be acquiring = Cpt(EpicsSignal, "Acquiring")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
def arm_aquisition
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
CHANGES LOG and
System as taken from cSAXS some times works for one element need about 7 second
There seem to be still serious timout issues
changes log
TIMEOUT_FOR_SIGNALs from 5 to 10
"""
import enum
import threading
import time
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 # old import
# now import ophyd.mca completely
# import ophyd.mca as Mca
from .sitoro import (
SitoroEpicsMCARecord,
SitoroEpicsMCA,
SitoroEpicsMCAReadNotify,
SitoroEpicsDXP,
SitoroEpicsDXPBaseSystem,
SitoroEpicsDXPMultiElementSystem,
SitoroTest,
)
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
class SitoroError(Exception):
"""Base class for exceptions in this module."""
class SitoroTimeoutError(SitoroError):
"""Raised when the Sitoro does not respond in time."""
class DetectorState(enum.IntEnum):
"""Detector states for Sitoro detector"""
DONE = 0
ACQUIRING = 1
class TriggerSource(enum.IntEnum):
"""Trigger source for Sitoro detector"""
USER = 0
GATE = 1
SYNC = 2
class MappingSource(enum.IntEnum):
"""Mapping source for Sitoro detector"""
SPECTRUM = 0
MAPPING = 1
class SitoroEpicsMCARecordExtended_OLD(SitoroEpicsMCARecord):
# add parameters for detector energy calibration
# which are missing in mca.py
calo = Cpt(EpicsSignal, ".CALO")
cals = Cpt(EpicsSignal, ".CALS")
calq = Cpt(EpicsSignal, ".CALQ")
tth = Cpt(EpicsSignal, ".TTH")
class SitoroEpicsDXP_OLD(Device):
"""
DXP parameters for Sitoro 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")
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
class SitoroHDF5Plugins(Device):
"""
HDF5 parameters for Sitoro 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 SitoroSetup(CustomDetectorMixin):
"""
Sitoro 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 Sitoro detector"""
self.initialize_default_parameter()
self.initialize_detector()
self.initialize_detector_backend()
def initialize_default_parameter(self) -> None:
"""
Set default parameters for Sitoro
This will set:
- readout (float): readout time in seconds
- value_pixel_per_buffer (int): number of spectra in buffer of Sitoro 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 Sitoro detector"""
pass
"""
THIS IS THE OLD CSACS CODE. uncomment for now, as we consider EPICS as the boss
for initialization
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 Sitoro."""
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.acquiring.read()[self.parent.acquiring.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 SitoroTimeoutError(
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)
time.sleep(0.5)
self.parent.erase_all.put(1)
time.sleep(0.5)
signal_conditions = [
(
lambda: self.parent.acquiring.read()[self.parent.acquiring.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 SitoroTimeoutError(
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"Sitoro 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 SitoroPhoenix(PSIDetectorBase, SitoroEpicsDXPMultiElementSystem):
# class SitoroPhoenix(PSIDetectorBase):
"""
Sitoro Sitoro detector for Phoenix
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (SitoroSetup) : Custom detector setup class,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
dxp1, .. dxpi, .. , dxpN (SitoroEpicsDXP) : DXP parameters for Sitoro detector Nr i
mca1, .. mcai, .. , mcaN (SitoroEpicsMCARecord) : MCA parameters for Sitoro detector Nr i
hdf5 (SitoroHDF5Plugins) : HDF5 parameters for Sitoro 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 = SitoroSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 1
# specify class attributes
# Parameters for individual detector elements
# Note: need to wrote 'dxp: here, but not dxp'
# dxp1 = Cpt(SitoroEpicsDXP, "dxp1:")
# dxp2 = Cpt(SitoroEpicsDXP, "dxp2:")
# dxp3 = Cpt(SitoroEpicsDXP, "dxp3:")
# dxp4 = Cpt(SitoroEpicsDXP, "dxp4:")
#
# THIS IS NOT WELL-DONE as it take out one part of mca.py from ophy.py
#
#
# mca1 = Cpt(SitoroEpicsMCARecordExtended, "mca1")
# mca2 = Cpt(SitoroEpicsMCARecordExtended, "mca2")
# mca3 = Cpt(SitoroEpicsMCARecordExtended, "mca3")
# mca4 = Cpt(SitoroEpicsMCARecordExtended, "mca4")
# need to write 'mca1', but not 'mca1:'
# mca1 = Cpt(EpicsMCARecord, "mca1")
# mca2 = Cpt(EpicsMCARecord, "mca2")
# mca3 = Cpt(EpicsMCARecord, "mca3")
# mca4 = Cpt(EpicsMCARecord, "mca4")
# other general parameters
hdf5 = Cpt(SitoroHDF5Plugins, "HDF1:")
# stop_all = Cpt(EpicsSignal, "StopAll")
# erase_all = Cpt(EpicsSignal, "EraseAll")
# start_all = Cpt(EpicsSignal, "StartAll")
# state = Cpt(EpicsSignal, "Acquiring") # <-- This is from cSAX implementation
# acquiring = 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")
# _________________ General Epic parameters
# changes Oct 2024
# triggers--> max_triggers,
# events-->max_events
# input_count_rate--> max_input_count_rate
# output_count_rate--> max_output_count_rate
# max_triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
# max_events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
# max_input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
# max_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")
# print(pixel_per_run
# if "SITORO" in prefix:
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
# endif
if __name__ == "__main__":
sitoro = SitoroPhoenix(name="sitoro", prefix="X07MB-SITORO:", sim_mode=True)

View File

@ -0,0 +1,369 @@
#
#
# 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
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"]
# 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')
def aaaa(self):
print('aaaa')

View File

@ -0,0 +1,17 @@
# against all rues, make sure ff and falcon are really
# creates newly
ff = 0
falcon = 0
from ophyd import Component as Cpt
import phoenix_bec.devices.falcon_phoenix as ff
falcon = ff.FalconPhoenix(name="falcon_hdf5", prefix="X07MB-SITORO:")
# xmap = ff.FalconPhoenix(name="falcon_hdf5", prefix="X07MB-XMAP:")
# make a 'get to read all epics channels
# there will be an error message, if device contains a channel whcih does not exist
w = falcon.get()
print(w)

View File

@ -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 ?
#

View File

@ -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

View File

@ -0,0 +1,512 @@
"""
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 typecheck
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}")

View File

@ -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

View File

@ -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

View File

@ -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.")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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:')
)

View File

@ -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:')

View File

@ -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:')

View File

@ -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:')

View File

@ -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()

View File

@ -0,0 +1,2 @@
print('Work with unix shell comands :')
print('use %ls %mkdir etc ')

View File

@ -0,0 +1,93 @@
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):
# 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
print()
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
# __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__

View File

@ -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__

View File

@ -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)

View File

@ -0,0 +1,85 @@
#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)
"""

View File

@ -0,0 +1,85 @@
"""
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
#
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.1, steps=4, exp_time=0.2, relative=False, delay=2)
s2 = scans.phoenix_line_scan(dev.ScanX, 0, 0.1, steps=4, exp_time=0.2, 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
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)
"""

View File

@ -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

View File

@ -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.

View File

@ -0,0 +1 @@
from .phoenix import PhoenixBL

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -0,0 +1,5 @@
print('test')
base='/data/test/x07mb-test-bec/bec_deployment/phoenix_bec/phoenix_bec'
bec.config.update_session_with_file(base+'/device_configs/phoenix_falcon.yaml')
#bec.config.update_session_with_file(base+'/device_configs/phoenix_devices.yaml')

View File

@ -0,0 +1,3 @@
import phoenix_bec.scripts.phoenix as PH
w=PH.PhGroup('labelName')
w.linescan2group(s1)

View File

@ -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)
"""

View File

@ -0,0 +1 @@
from .phoenix_scans import PhoenixLineScan

View File

@ -0,0 +1,174 @@
"""
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
"""
# 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
import time
import numpy as np
# from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_server.scan_server.scans import ScanArgType, ScanBase
from phoenix_bec.scripts.phoenix import PhoenixBL
# 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
logger = bec_logger.logger
class LogTime:
def __init__(self):
logger.success("init LogTime")
self.t0 = time.time()
def p_s(self, x):
now = time.time()
# delta=now-self.t0
m = str(now) + " sec " + x
logger.success(m)
# making the instance of PSID
# self.t0=now
file = open("MyLogfile.txt", "a")
file.write(m + "\n")
file.close
class PhoenixScanBaseTTL(ScanBase):
"""
Base scan cl p_s('init scrips.phoenix.scans.PhoenixLineScan')
"""
def scan_core(self):
"""perform the scan core procedure"""
self.p_s("PhoenixScanBaseTT.scan_core")
for ind, pos in self._get_position():
for self.burst_index in range(self.burst_at_each_point):
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):
self.p_s("PhoenixScanBaseTT._at_each_point")
yield from self._move_scan_motors_and_wait(pos)
time.sleep(self.settling_time)
yield from self.stubs.trigger(min_wait=self.exp_time)
yield from self.stubs.read(group="monitored", point_id=self.point_id)
self.point_id += 1
self.p_s("done")
class PhoenixLineScan(PhoenixScanBaseTTL):
scan_name = "phoenix_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,
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)
"""
# 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
)
self.steps = steps
self.setup_device = setup_device
time.sleep(1)
self.p_s("done")
def _calculate_positions(self) -> None:
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)
self.p_s("done")

View File

@ -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

View File

@ -0,0 +1,11 @@
## scripts
Directory for general phoenix specific python code
to autoload register in __init__.py
## FILES
phoenix.py Base file with general definitions

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,262 @@
# from unittest import mock
import os
import sys
import time
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
#
"""
t0 = time.time()
def __init__(self):
"""
init PhoenixBL() in phoenix_bec/scripts
"""
import os
print("..... init PhoenixBL from phoenix_bec/scripts/phoenix.py")
# 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/"
# yamal file for default configuration
self.file_devices_file = (
self.path_phoenix_bec + "phoenix_bec/device_configs/phoenix_devices.yaml"
) # local yamal file
self.file_devices_tmp = (
self.path_phoenix_bec + "phoenix_bec/device_configs/current_devices_tmp.yaml"
) # tmp configuration file. Will be electronicall created and appended if needed
self.t0 = time.time()
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 create_base_config(self):
# create a yaml file from standard configuration
os.system("cat " + self.file_devices_file + " > " + self.file_devices_tmp)
# os.system("ls -altr" + self.path_phoenix_bec + "phoenix_bec/devices")
bec.config.update_session_with_file(self.file_devices_tmp)
def add_phoenix_config(self):
print("add_phoenix_config ")
print("self.file_devices_file")
bec.config.update_session_with_file(self.tmp.file_devices_file)
def add_xmap(self):
print("add xmap ")
os.system("cat " + self.path_devices + "phoenix_xmap.yaml" + " >> " + self.file_devices_tmp)
bec.config.update_session_with_file(self.file_devices_tmp)
def add_falcon(self):
print("add_falcon to existing configuration ")
os.system(
"cat " + self.path_devices + "phoenix_falcon.yaml" + " >> " + self.file_devices_tmp
)
bec.config.update_session_with_file(self.file_devices_tmp)
def load_falcon(self):
print("load_falcon")
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")
@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.0 * (now // 3600.0)
now = now - 600.0 * (now // 600.0)
m = str(now) + " sec " + x
logger.success(m)
file = open("MyLogfile.txt", "a")
file.write(m + "\n")
file.close
class PhGroup:
"""
Class to create data groups
compatible with larch groups
initialize by
ww=PhGroup('YourLabel')
it creates a group
with default attributes
ww.label = 'YourLabel' --- for compatibility with larch groups
ww.description =YourLabel'
Further data can be added with new tags by
ww.newtag=67
(bec_venv) [gac-x07mb@x07mb-bec-001 phoenix_bec]$
ww.keys() -- list all keys
ww.linescan2group -- converts bec linescan data to group format
"""
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))) != "<class 'method'>":
box.append(i)
# endif
# endfor
return box
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():
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
# endfor
# endfor
# enddef

View File

@ -0,0 +1,103 @@
import threading
import time
from unittest import mock
import numpy as np
import ophyd
import pytest
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from phoenix_bec.devices.phoenix_trigger import SAMPLING, PhoenixTrigger
@pytest.fixture(scope="function")
def mock_trigger():
name = "phoenix_trigger"
prefix = "X07MB-OP2:"
dm = DMMock()
with mock.patch.object(dm, "connector"):
with (
mock.patch(
"ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
) as filemixin,
mock.patch(
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
) as mock_service_config,
):
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
with mock.patch.object(PhoenixTrigger, "_init"):
det = PhoenixTrigger(name=name, prefix=prefix, device_manager=dm)
patch_dual_pvs(det)
det.TIMEOUT_FOR_SIGNALS = 0.1
yield det
def test_phoenix_trigger_init(mock_trigger):
"""Test PhoenixTrigger init"""
assert mock_trigger.name == "phoenix_trigger"
assert mock_trigger.prefix == "X07MB-OP2:"
def test_phoenix_trigger_stage(mock_trigger):
"""Test PhoenixTrigger on_stage"""
with mock.patch.object(mock_trigger.scaninfo, "load_scan_metadata") as mock_load_scan_metadata:
mock_trigger.scaninfo.scan_type = "step"
mock_trigger.scaninfo.exp_time = exp_time = 1
mock_trigger.stage()
assert mock_load_scan_metadata.call_count == 1
assert mock_trigger.start_csmpl.get() == 0
assert mock_trigger.total_cycles.get() == np.ceil(exp_time * 5)
assert mock_trigger.smpl.get() == 1
def test_phoenix_trigger_unstage(mock_trigger):
"""Test PhoenixTrigger on_unstage"""
with mock.patch.object(mock_trigger.custom_prepare, "on_stop") as mock_on_stop:
mock_trigger.unstage()
assert mock_on_stop.call_count == 1
def test_phoenix_trigger_stop(mock_trigger):
"""Test PhoenixTrigger on_stop"""
with mock.patch.object(mock_trigger.smpl, "put") as mock_smpl_put:
mock_trigger.smpl_done._read_pv.mock_data = SAMPLING.RUNNING
mock_trigger.stop()
assert mock_trigger.stopped is True
# assert mock_trigger.total_cycles.get() == 5
# 5 cycles is too tight during development
assert mock_trigger.start_csmpl.get() == 1
assert mock_smpl_put.call_args_list == [mock.call(1), mock.call(1)]
"""
uncomment this test, as device names etc will change
and as other devices will bee added
def test_phoenix_trigger_trigger(mock_trigger):
#Test PhoenixTrigger on_trigger
#
#irst test that the trigger timeouts due to readback from smpl_done not being done.
#Afterwards, check that status object resolved correctly if smpl_done is done.
#
exp_time = 0.05
mock_trigger.device_manager.add_device("falcon_nohdf5")
falcon_state = mock_trigger.device_manager.devices.falcon_nohdf5.state = mock.MagicMock()
falcon_state.get = mock.MagicMock(return_value=1)
mock_trigger.scaninfo.scan_type = "step"
mock_trigger.scaninfo.exp_time = exp_time
with mock.patch.object(
mock_trigger.custom_prepare, "wait_with_status", return_value=mock.MagicMock()
) as mock_wait_with_status:
status = mock_trigger.trigger()
assert mock_wait_with_status.call_count == 1
assert mock_wait_with_status.call_args[1]["signal_conditions"] == [
(mock_trigger.smpl_done.get, SAMPLING.DONE)
]
assert mock_wait_with_status.call_args[1]["timeout"] == 5 * exp_time
assert mock_wait_with_status.call_args[1]["check_stopped"] is True
"""