From 687ae8981b06d8d3708103c9d6a5abf698d5d980 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 16 Jan 2025 17:28:57 -0500 Subject: [PATCH 01/18] progress --- .ci/drop-in.py | 2 + iocs/areadetector.py | 338 +++++++++++ iocs/shutter.py | 37 ++ iocs/spoof_beamline.py | 144 +++++ startup/00-startup.py | 8 +- startup/19-exp_shutter.py | 2 +- startup/20-area-detectors.py | 20 +- startup/81-beam.py | 4 +- startup/90-bluesky.py | 4 +- startup/94-sample.py | 2 + user_TSAXSWAXS.py | 1109 ++++++++++++++++++++++++++++++++++ 11 files changed, 1652 insertions(+), 18 deletions(-) create mode 100644 .ci/drop-in.py create mode 100644 iocs/areadetector.py create mode 100644 iocs/shutter.py create mode 100644 iocs/spoof_beamline.py create mode 100644 user_TSAXSWAXS.py diff --git a/.ci/drop-in.py b/.ci/drop-in.py new file mode 100644 index 0000000..11f69c7 --- /dev/null +++ b/.ci/drop-in.py @@ -0,0 +1,2 @@ +sam = Sample("test") +detselect(pilatus2M) diff --git a/iocs/areadetector.py b/iocs/areadetector.py new file mode 100644 index 0000000..b17a547 --- /dev/null +++ b/iocs/areadetector.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +import numpy as np +import ophyd + +from caproto.server import (PVGroup, SubGroup, get_pv_pair_wrapper, + ioc_arg_parser, pvproperty, run) +from caproto.server.conversion import ophyd_device_to_caproto_ioc + + +async def get_images(w, h): + img = np.zeros((w, h), dtype=np.int32) + while True: + for i in range(256): + img[:, :] = (i << 16) | (i << 8) | i + yield img + + +class SC(ophyd.areadetector.cam.SimDetectorCam): + pool_max_buffers = None + + +class IP(ophyd.ImagePlugin): + pool_max_buffers = None + + +class Detector(ophyd.SimDetector): + image1 = ophyd.Component(IP, 'image1:') + cam = ophyd.Component(SC, 'cam1:') + + +pvproperty_with_rbv = get_pv_pair_wrapper(setpoint_suffix='', + readback_suffix='_RBV') + + +# -- DetectorGroup -- +class DetectorGroup(PVGroup): + # configuration_names = pvproperty(name=None, dtype=str) + + class SimDetectorCamGroup(PVGroup): + # configuration_names = pvproperty(name=None, dtype=str) + array_counter = pvproperty_with_rbv(name='ArrayCounter', dtype=int) + array_rate = pvproperty( + name='ArrayRate_RBV', dtype=float, read_only=True) + asyn_io = pvproperty(name='AsynIO', dtype=int) + nd_attributes_file = pvproperty( + name='NDAttributesFile', dtype=str, max_length=256) + pool_alloc_buffers = pvproperty( + name='PoolAllocBuffers', dtype=int, read_only=True) + pool_free_buffers = pvproperty( + name='PoolFreeBuffers', dtype=int, read_only=True) + pool_max_buffers = pvproperty( + name='PoolMaxBuffers', dtype=int, read_only=True) + pool_max_mem = pvproperty( + name='PoolMaxMem', dtype=float, read_only=True) + pool_used_buffers = pvproperty( + name='PoolUsedBuffers', dtype=float, read_only=True) + pool_used_mem = pvproperty( + name='PoolUsedMem', dtype=float, read_only=True) + port_name = pvproperty(name='PortName_RBV', dtype=str, read_only=True) + acquire = pvproperty_with_rbv(name='Acquire', dtype=int) + acquire_period = pvproperty_with_rbv(name='AcquirePeriod', dtype=float) + acquire_time = pvproperty_with_rbv(name='AcquireTime', dtype=float) + array_callbacks = pvproperty_with_rbv(name='ArrayCallbacks', dtype=int) + + class SimDetectorCamArraySizeGroup(PVGroup): + array_size_x = pvproperty( + name='ArraySizeX_RBV', dtype=int, read_only=True) + array_size_y = pvproperty( + name='ArraySizeY_RBV', dtype=int, read_only=True) + array_size_z = pvproperty( + name='ArraySizeZ_RBV', dtype=int, read_only=True) + + array_size = SubGroup(SimDetectorCamArraySizeGroup, prefix='') + + array_size_bytes = pvproperty( + name='ArraySize_RBV', dtype=int, read_only=True) + bin_x = pvproperty_with_rbv(name='BinX', dtype=int) + bin_y = pvproperty_with_rbv(name='BinY', dtype=int) + color_mode = pvproperty_with_rbv(name='ColorMode', dtype=int) + data_type = pvproperty_with_rbv(name='DataType', dtype=int) + detector_state = pvproperty( + name='DetectorState_RBV', dtype=int, read_only=True) + frame_type = pvproperty_with_rbv(name='FrameType', dtype=int) + gain = pvproperty_with_rbv(name='Gain', dtype=float) + image_mode = pvproperty_with_rbv(name='ImageMode', dtype=int) + manufacturer = pvproperty( + name='Manufacturer_RBV', dtype=str, read_only=True) + + class SimDetectorCamMaxSizeGroup(PVGroup): + max_size_x = pvproperty( + name='MaxSizeX_RBV', dtype=int, read_only=True) + max_size_y = pvproperty( + name='MaxSizeY_RBV', dtype=int, read_only=True) + + max_size = SubGroup(SimDetectorCamMaxSizeGroup, prefix='') + + min_x = pvproperty_with_rbv(name='MinX', dtype=int) + min_y = pvproperty_with_rbv(name='MinY', dtype=int) + model = pvproperty(name='Model_RBV', dtype=str, read_only=True) + num_exposures = pvproperty_with_rbv(name='NumExposures', dtype=int) + num_exposures_counter = pvproperty( + name='NumExposuresCounter_RBV', dtype=int, read_only=True) + num_images = pvproperty_with_rbv(name='NumImages', dtype=int) + num_images_counter = pvproperty( + name='NumImagesCounter_RBV', dtype=int, read_only=True) + read_status = pvproperty(name='ReadStatus', dtype=int) + + class SimDetectorCamReverseGroup(PVGroup): + reverse_x = pvproperty_with_rbv(name='ReverseX', dtype=int) + reverse_y = pvproperty_with_rbv(name='ReverseY', dtype=int) + + reverse = SubGroup(SimDetectorCamReverseGroup, prefix='') + + shutter_close_delay = pvproperty_with_rbv( + name='ShutterCloseDelay', dtype=float) + shutter_close_epics = pvproperty(name='ShutterCloseEPICS', dtype=float) + shutter_control = pvproperty_with_rbv(name='ShutterControl', dtype=int) + shutter_control_epics = pvproperty( + name='ShutterControlEPICS', dtype=int) + shutter_fanout = pvproperty(name='ShutterFanout', dtype=int) + shutter_mode = pvproperty_with_rbv(name='ShutterMode', dtype=int) + shutter_open_delay = pvproperty_with_rbv( + name='ShutterOpenDelay', dtype=float) + shutter_open_epics = pvproperty(name='ShutterOpenEPICS', dtype=float) + shutter_status_epics = pvproperty( + name='ShutterStatusEPICS_RBV', dtype=int, read_only=True) + shutter_status = pvproperty( + name='ShutterStatus_RBV', dtype=int, read_only=True) + + class SimDetectorCamSizeGroup(PVGroup): + size_x = pvproperty_with_rbv(name='SizeX', dtype=int) + size_y = pvproperty_with_rbv(name='SizeY', dtype=int) + + size = SubGroup(SimDetectorCamSizeGroup, prefix='') + + status_message = pvproperty( + name='StatusMessage_RBV', + dtype=str, + max_length=256, + read_only=True) + string_from_server = pvproperty( + name='StringFromServer_RBV', + dtype=str, + max_length=256, + read_only=True) + string_to_server = pvproperty( + name='StringToServer_RBV', + dtype=str, + max_length=256, + read_only=True) + temperature = pvproperty_with_rbv(name='Temperature', dtype=float) + temperature_actual = pvproperty(name='TemperatureActual', dtype=float) + time_remaining = pvproperty( + name='TimeRemaining_RBV', dtype=float, read_only=True) + trigger_mode = pvproperty_with_rbv(name='TriggerMode', dtype=int) + + class SimDetectorCamGainRgbGroup(PVGroup): + gain_red = pvproperty_with_rbv(name='GainRed', dtype=float) + gain_green = pvproperty_with_rbv(name='GainGreen', dtype=float) + gain_blue = pvproperty_with_rbv(name='GainBlue', dtype=float) + + gain_rgb = SubGroup(SimDetectorCamGainRgbGroup, prefix='') + + class SimDetectorCamGainXyGroup(PVGroup): + gain_x = pvproperty_with_rbv(name='GainX', dtype=float) + gain_y = pvproperty_with_rbv(name='GainY', dtype=float) + + gain_xy = SubGroup(SimDetectorCamGainXyGroup, prefix='') + + noise = pvproperty_with_rbv(name='Noise', dtype=int) + + class SimDetectorCamPeakNumGroup(PVGroup): + peak_num_x = pvproperty_with_rbv(name='PeakNumX', dtype=int) + peak_num_y = pvproperty_with_rbv(name='PeakNumY', dtype=int) + + peak_num = SubGroup(SimDetectorCamPeakNumGroup, prefix='') + + class SimDetectorCamPeakStartGroup(PVGroup): + peak_start_x = pvproperty_with_rbv(name='PeakStartX', dtype=int) + peak_start_y = pvproperty_with_rbv(name='PeakStartY', dtype=int) + + peak_start = SubGroup(SimDetectorCamPeakStartGroup, prefix='') + + class SimDetectorCamPeakStepGroup(PVGroup): + peak_step_x = pvproperty_with_rbv(name='PeakStepX', dtype=int) + peak_step_y = pvproperty_with_rbv(name='PeakStepY', dtype=int) + + peak_step = SubGroup(SimDetectorCamPeakStepGroup, prefix='') + + peak_variation = pvproperty_with_rbv(name='PeakVariation', dtype=int) + + class SimDetectorCamPeakWidthGroup(PVGroup): + peak_width_x = pvproperty_with_rbv(name='PeakWidthX', dtype=int) + peak_width_y = pvproperty_with_rbv(name='PeakWidthY', dtype=int) + + peak_width = SubGroup(SimDetectorCamPeakWidthGroup, prefix='') + + reset = pvproperty_with_rbv(name='Reset', dtype=int) + sim_mode = pvproperty_with_rbv(name='SimMode', dtype=int) + + cam = SubGroup(SimDetectorCamGroup, prefix='cam1:') + + class ImagePluginGroup(PVGroup): + # configuration_names = pvproperty(name=None, dtype=str) + array_counter = pvproperty_with_rbv(name='ArrayCounter', dtype=int) + array_rate = pvproperty( + name='ArrayRate_RBV', dtype=float, read_only=True) + asyn_io = pvproperty(name='AsynIO', dtype=int) + nd_attributes_file = pvproperty( + name='NDAttributesFile', dtype=str, max_length=256) + pool_alloc_buffers = pvproperty( + name='PoolAllocBuffers', dtype=int, read_only=True) + pool_free_buffers = pvproperty( + name='PoolFreeBuffers', dtype=int, read_only=True) + pool_max_buffers = pvproperty( + name='PoolMaxBuffers', dtype=int, read_only=True) + pool_max_mem = pvproperty( + name='PoolMaxMem', dtype=float, read_only=True) + pool_used_buffers = pvproperty( + name='PoolUsedBuffers', dtype=float, read_only=True) + pool_used_mem = pvproperty( + name='PoolUsedMem', dtype=float, read_only=True) + port_name = pvproperty(name='PortName_RBV', dtype=str, read_only=True) + # asyn_pipeline_config = pvproperty(name=None, dtype=str) + # width = pvproperty(name='ArraySize0_RBV', dtype=int, read_only=True) + # height = pvproperty(name='ArraySize1_RBV', dtype=int, read_only=True) + # depth = pvproperty(name='ArraySize2_RBV', dtype=int, read_only=True) + + class ImagePluginArraySizeGroup(PVGroup): + height = pvproperty( + name='ArraySize1_RBV', dtype=int, read_only=True) + width = pvproperty( + name='ArraySize0_RBV', dtype=int, read_only=True) + depth = pvproperty( + name='ArraySize2_RBV', dtype=int, read_only=True) + + array_size = SubGroup(ImagePluginArraySizeGroup, prefix='') + + bayer_pattern = pvproperty( + name='BayerPattern_RBV', dtype=int, read_only=True) + blocking_callbacks = pvproperty_with_rbv( + name='BlockingCallbacks', dtype=str) + color_mode = pvproperty( + name='ColorMode_RBV', dtype=int, read_only=True) + data_type = pvproperty(name='DataType_RBV', dtype=str, read_only=True) + # dim0_sa = pvproperty(name='Dim0SA', dtype=int, max_length=10) + # dim1_sa = pvproperty(name='Dim1SA', dtype=int, max_length=10) + # dim2_sa = pvproperty(name='Dim2SA', dtype=int, max_length=10) + + class ImagePluginDimSaGroup(PVGroup): + dim0 = pvproperty(name='Dim0SA', dtype=int, max_length=10) + dim1 = pvproperty(name='Dim1SA', dtype=int, max_length=10) + dim2 = pvproperty(name='Dim2SA', dtype=int, max_length=10) + + dim_sa = SubGroup(ImagePluginDimSaGroup, prefix='') + + dimensions = pvproperty( + name='Dimensions_RBV', dtype=int, max_length=10, read_only=True) + dropped_arrays = pvproperty_with_rbv(name='DroppedArrays', dtype=int) + enable = pvproperty_with_rbv(name='EnableCallbacks', dtype=str) + min_callback_time = pvproperty_with_rbv( + name='MinCallbackTime', dtype=float) + nd_array_address = pvproperty_with_rbv( + name='NDArrayAddress', dtype=int) + nd_array_port = pvproperty_with_rbv(name='NDArrayPort', dtype=str) + ndimensions = pvproperty( + name='NDimensions_RBV', dtype=int, read_only=True) + plugin_type = pvproperty( + name='PluginType_RBV', dtype=str, read_only=True) + queue_free = pvproperty(name='QueueFree', dtype=int) + queue_free_low = pvproperty(name='QueueFreeLow', dtype=float) + queue_size = pvproperty(name='QueueSize', dtype=int) + queue_use = pvproperty(name='QueueUse', dtype=float) + queue_use_high = pvproperty(name='QueueUseHIGH', dtype=float) + queue_use_hihi = pvproperty(name='QueueUseHIHI', dtype=float) + time_stamp = pvproperty( + name='TimeStamp_RBV', dtype=float, read_only=True) + unique_id = pvproperty(name='UniqueId_RBV', dtype=int, read_only=True) + array_data = pvproperty( + name='ArrayData', dtype=int, max_length=300000) + + # NOTE: this portion written by hand: + @array_data.startup + async def array_data(self, instance, async_lib): + await self.plugin_type.write('NDPluginStdArrays') + + w, h = 256, 256 + await self.array_size.width.write(w) + await self.array_size.height.write(h) + await self.array_size.depth.write(0) + await self.ndimensions.write(2) + await self.dimensions.write([w, h, 0]) + + async for image in get_images(w, h): + await self.array_data.write(image.flatten()) + await async_lib.library.sleep(1.0) + # END hand-written portion + + image1 = SubGroup(ImagePluginGroup, prefix='image1:') + +# -- end autogenerated code -- + + +def generate_detector_code(prefix='13SIM1:'): + ''' + Use the simDetector IOC to automatically create code for our detector, + with the help of ophyd and `ophyd_device_to_caproto_ioc` + ''' + my_detector = Detector('13SIM1:', name='detector') + try: + my_detector.wait_for_connection(5.0) + except TimeoutError: + print('Connection timed out') + return + else: + print('Connected to detector') + + dev_dict = ophyd_device_to_caproto_ioc(my_detector) + print('# -- autogenerated code --') + for dev, lines in dev_dict.items(): + print(f'# -- {dev} --') + + for line in lines: + print(line) + + print(f'# -- end {dev} --') + print('# -- end autogenerated code --') + + +if __name__ == '__main__': + ioc_options, run_options = ioc_arg_parser( + default_prefix='adimage:', + desc='Simulate an Area Detector IOC.') + ioc = DetectorGroup(**ioc_options) + generate_detector_code() + + detector_ioc = DetectorGroup(ioc_options['prefix']) + run(ioc.pvdb, **run_options) diff --git a/iocs/shutter.py b/iocs/shutter.py new file mode 100644 index 0000000..ebaee05 --- /dev/null +++ b/iocs/shutter.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import asyncio +import re +from collections import defaultdict + +from caproto import ChannelType +from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup + + +class Shutter(PVGroup): + shutter = pvproperty(value=0, name="{{Shutter}}", dtype=ChannelType.INT) + psh_blade2_pos = pvproperty(value=0, name="{{Psh_blade2}}Pos", dtype=ChannelType.INT) + psh_blade1_pos = pvproperty(value=0, name="{{Psh_blade1}}Pos", dtype=ChannelType.INT) + + def __init__(self, *args, **kwargs): + super().__init__(prefix="XF:11BM-ES", *args, **kwargs) + + @shutter.putter # type: ignore + async def shutter(self, instance, value): + await asyncio.sleep(0.1) + await self.psh_blade2_pos.write(value) + await self.psh_blade1_pos.write(value) + return value + + +def main(): + _, run_options = ioc_arg_parser( + default_prefix='XF:11BM-ES', + desc="Shutter IOC") + run_options['interfaces'] = ['127.0.0.1'] + shutter = Shutter() + run(shutter.pvdb, + **run_options) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py new file mode 100644 index 0000000..5fa7acf --- /dev/null +++ b/iocs/spoof_beamline.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +import asyncio +import re +from collections import defaultdict + +from caproto import (ChannelChar, ChannelData, ChannelDouble, ChannelEnum, + ChannelInteger, ChannelString, ChannelType) +from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup + +PLUGIN_TYPE_PVS = [ + (re.compile('image\\d:'), 'NDPluginStdArrays'), + (re.compile('Stats\\d:'), 'NDPluginStats'), + (re.compile('CC\\d:'), 'NDPluginColorConvert'), + (re.compile('Proc\\d:'), 'NDPluginProcess'), + (re.compile('Over\\d:'), 'NDPluginOverlay'), + (re.compile('ROI\\d:'), 'NDPluginROI'), + (re.compile('Trans\\d:'), 'NDPluginTransform'), + (re.compile('netCDF\\d:'), 'NDFileNetCDF'), + (re.compile('TIFF\\d:'), 'NDFileTIFF'), + (re.compile('JPEG\\d:'), 'NDFileJPEG'), + (re.compile('Nexus\\d:'), 'NDPluginNexus'), + (re.compile('HDF\\d:'), 'NDFileHDF5'), + (re.compile('Magick\\d:'), 'NDFileMagick'), + (re.compile('TIFF\\d:'), 'NDFileTIFF'), + (re.compile('HDF\\d:'), 'NDFileHDF5'), + (re.compile('Current\\d:'), 'NDPluginStats'), + (re.compile('SumAll'), 'NDPluginStats'), +] + + +class ReallyDefaultDict(defaultdict): + def __contains__(self, key): + #if "{Shutter}" in key or "{Psh_blade2}Pos" in key or "{Psh_blade1}Pos" in key: + # return False + return True + + def __missing__(self, key): + #if "{Shutter}" in key or "{Psh_blade2}Pos" in key or "{Psh_blade1}Pos" in key: + # return None + if (key.endswith('-SP') or key.endswith('-I') or + key.endswith('-RB') or key.endswith('-Cmd')): + key, *_ = key.rpartition('-') + return self[key] + if key.endswith('_RBV') or key.endswith(':RBV'): + return self[key[:-4]] + ret = self[key] = self.default_factory(key) + return ret + +class Shutter(PVGroup): + shutter = pvproperty(value=0, name="{{Shutter}}", dtype=ChannelType.INT) + psh_blade2_pos = pvproperty(value=0, name="{{Psh_blade2}}Pos", dtype=ChannelType.INT) + psh_blade1_pos = pvproperty(value=0, name="{{Psh_blade1}}Pos", dtype=ChannelType.INT) + + def __init__(self, *args, **kwargs): + super().__init__(prefix="XF:11BM-ES", *args, **kwargs) + # FIXME: Find a way to get pvdb updated with shutter, psh_blade2_pos, psh_blade1_pos + self.pvdb = ReallyDefaultDict(fabricate_channel) + self._create_pvdb() + + @shutter.putter # type: ignore + async def shutter(self, instance, value): + await asyncio.sleep(0.1) + await self.psh_blade2_pos.write(value) + await self.psh_blade1_pos.write(value) + return value + +def fabricate_channel(key): + if 'PluginType' in key: + for pattern, val in PLUGIN_TYPE_PVS: + if pattern.search(key): + return ChannelString(value=val) + elif 'ArrayPort' in key: + return ChannelString(value="PIL") + elif 'PortName' in key: + port_name = key.split(':')[-2] + if port_name == "cam1": + port_name = "PIL" + return ChannelString(value=port_name) + elif 'name' in key.lower(): + return ChannelString(value=key) + elif 'EnableCallbacks' in key: + return ChannelEnum(value=0, enum_strings=['Disabled', 'Enabled']) + elif 'BlockingCallbacks' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif 'Auto' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif 'ImageMode' in key: + return ChannelEnum(value=0, enum_strings=['Single', 'Multiple', 'Continuous']) + elif 'WriteMode' in key: + return ChannelEnum(value=0, enum_strings=['Single', 'Capture', 'Stream']) + elif 'ArraySize' in key: + return ChannelData(value=10) + elif 'TriggerMode' in key: + return ChannelEnum(value=0, enum_strings=['Internal', 'External']) + elif 'FileWriteMode' in key: + return ChannelEnum(value=0, enum_strings=['Single']) + elif 'FilePathExists' in key: + return ChannelData(value=1) + elif 'WaitForPlugins' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif ('file' in key.lower() and 'number' not in key.lower() and + 'mode' not in key.lower()): + return ChannelChar(value='a' * 250) + elif ('filenumber' in key.lower()): + return ChannelInteger(value=0) + elif 'Compression' in key: + return ChannelEnum(value=0, enum_strings=['None', 'N-bit', 'szip', 'zlib', 'blosc']) + return ChannelDouble(value=0.0) + + +def main(): + print(''' +*** WARNING *** +This script spawns an EPICS IOC which responds to ALL caget, caput, camonitor +requests. As this is effectively a PV black hole, it may affect the +performance and functionality of other IOCs on your network. + +The script ignores the --interfaces command line argument, always +binding only to 127.0.0.1, superseding the usual default (0.0.0.0) and any +user-provided value. +*** WARNING *** + +Press return if you have acknowledged the above, or Ctrl-C to quit.''') + + try: + input() + except KeyboardInterrupt: + print() + return + print(''' + + PV blackhole started + +''') + _, run_options = ioc_arg_parser( + default_prefix='', + desc="PV black hole") + run_options['interfaces'] = ['127.0.0.1'] + run(Shutter().pvdb, + **run_options) + + +if __name__ == '__main__': + main() diff --git a/startup/00-startup.py b/startup/00-startup.py index e04bb6b..204710c 100644 --- a/startup/00-startup.py +++ b/startup/00-startup.py @@ -15,7 +15,7 @@ from redis_json_dict import RedisJSONDict -nslsii.configure_base(get_ipython().user_ns, "cms", publish_documents_with_kafka=True) +nslsii.configure_base(get_ipython().user_ns, "temp", publish_documents_with_kafka=True) from bluesky.magics import BlueskyMagics from bluesky.preprocessors import pchain @@ -106,9 +106,9 @@ def _epicssignal_get(self, *, as_string=None, connection_timeout=1.0, **kwargs): # Increase the timeout for EpicsSignal.get() # This beamline was occasionally getting ReadTimeoutErrors -# EpicsSignal.set_defaults(timeout=10) -# EpicsSignalRO.set_defaults(timeout=10) -ophyd.signal.EpicsSignalBase.set_defaults(timeout=120) +EpicsSignal.set_defaults(timeout=10, connection_timeout=10) +EpicsSignalRO.set_defaults(timeout=10, connection_timeout=10) +ophyd.signal.EpicsSignalBase.set_defaults(timeout=120, connection_timeout=120) # We have commented this because we would like to identify the PVs that are causing problems. diff --git a/startup/19-exp_shutter.py b/startup/19-exp_shutter.py index 761c97d..fa61644 100644 --- a/startup/19-exp_shutter.py +++ b/startup/19-exp_shutter.py @@ -37,7 +37,7 @@ def shutter_off(verbosity=3): def shutter_state(verbosity=3): - if shutter_sts1_pv.get() == 1 & shutter_sts2_pv.get() == 1: + if shutter_sts1_pv.get() == 1 and shutter_sts2_pv.get() == 1: status = 1 if verbosity >= 3: print("Shutter is OPEN.") diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index 46c31c2..b7fe5da 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -506,16 +506,16 @@ def stage(self): # for cam_number, fs in zip([1,2,3,4], [fs1, fs2, fs3, fs4]): - for cam_number, fs in zip([2, 3, 4], [fs2, fs3, fs4]): - G, port_dict = fs.get_asyn_digraph() - cam = port_dict["cam{:02}".format(cam_number)] - for v in port_dict.values(): - try: - if v.nd_array_port.get() == "CAM": - v.nd_array_port.set("cam{:02}".format(cam_number)) - except AttributeError: - pass - fs.validate_asyn_ports() + # for cam_number, fs in zip([2, 3, 4], [fs2, fs3, fs4]): + # G, port_dict = fs.get_asyn_digraph() + # cam = port_dict["cam{:02}".format(cam_number)] + # for v in port_dict.values(): + # try: + # if v.nd_array_port.get() == "CAM": + # v.nd_array_port.set("cam{:02}".format(cam_number)) + # except AttributeError: + # pass + # fs.validate_asyn_ports() diff --git a/startup/81-beam.py b/startup/81-beam.py index cb2270f..fdb96a7 100644 --- a/startup/81-beam.py +++ b/startup/81-beam.py @@ -1068,7 +1068,7 @@ def energy(self, verbosity=3): wavelength_A = 2.0 * self.dmm_dsp * np.sin(Bragg_rad) wavelength_m = wavelength_A * 1e-10 - energy_eV = self.hc_over_e / wavelength_m + energy_eV = self.hc_over_e / (wavelength_m + 1e-8) energy_keV = energy_eV / 1000.0 if verbosity >= 3: @@ -1097,7 +1097,7 @@ def wavelength(self, verbosity=3): # (planck constant * speed of light)/(electronic charge) - energy_eV = self.hc_over_e / wavelength_m + energy_eV = self.hc_over_e / (wavelength_m + 1e-8) energy_keV = energy_eV / 1000.0 if verbosity >= 3: diff --git a/startup/90-bluesky.py b/startup/90-bluesky.py index f940a38..f965563 100644 --- a/startup/90-bluesky.py +++ b/startup/90-bluesky.py @@ -357,7 +357,9 @@ def pump_chm(onoff, q=0): # PROFILE_ROOT = os.path.dirname(__file__) # PROFILE_ROOT = '/nsls2/data/cms/legacy/xf11bm/ipython_profiles/profile_collection/startup' -PROFILE_ROOT = "/home/xf11bm/.ipython/profile_collection/startup" +from IPython import get_ipython + +PROFILE_ROOT = get_ipython().profile_dir.startup_dir CMS_CONFIG_FILENAME = os.path.join(PROFILE_ROOT, ".cms_config") ## CMS config file diff --git a/startup/94-sample.py b/startup/94-sample.py index d72c976..378c45e 100644 --- a/startup/94-sample.py +++ b/startup/94-sample.py @@ -2123,6 +2123,8 @@ def _expose_test(self, exposure_time=None, extra=None, handlefile=True, verbosit status *= 0 elif detector.name is "pilatus2M": if caget("XF:11BMB-ES{Det:PIL2M}:cam1:Acquire") == 1: + print('pilatus2M is done') + print(status) status *= 0 elif detector.name is "pilatus800": if caget("XF:11BMB-ES{Det:PIL800K}:cam1:Acquire") == 1: diff --git a/user_TSAXSWAXS.py b/user_TSAXSWAXS.py new file mode 100644 index 0000000..e0c1db9 --- /dev/null +++ b/user_TSAXSWAXS.py @@ -0,0 +1,1109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vi: ts=4 sw=4 + + + + +################################################################################ +# Short-term settings (specific to a particular user/experiment) can +# be placed in this file. You may instead wish to make a copy of this file in +# the user's data directory, and use that as a working copy. +################################################################################ + + +#logbooks_default = ['User Experiments'] +#tags_default = ['CFN Soft-Bio'] + +import pickle +import os +from shutil import copyfile + +from ophyd import EpicsSignal +from bluesky.suspenders import SuspendFloor, SuspendCeil + +# ring_current = EpicsSignal('SR:OPS-BI{DCCT:1}I:Real-I') +# sus = SuspendFloor(ring_current, 100, resume_thresh=400, sleep=600) +# RE.install_suspender(sus) + + +# RE.md['experiment_alias_directory'] = '/nsls2/data/cms/legacy/xf11bm/data/2023_2/LZhu2/' +# cms.SAXS.setCalibration([759, 1079], 5.03, [-65, -73]) #20201021, 13.5 keV 1680 - 573 + + +# cms.SAXS.setCalibration([758, 1075], 5.03, [-65, -73]) #20201021, 13.5 keV +# # cms.SAXS.setCalibration([759, 1078], 5.03, [-65, -73]) # 17keV +# RE.md['experiment_alias_directory'] = '/nsls2/data/cms/legacy/xf11bm/data/2024_1/beamline/RLi/YXu' +# cms.SAXS.setCalibration([756, 1079], 5.0, [-65, -73]) #5m,13.5kev, Jan2024 +# cms.SAXS.setCalibration([732, 1098], 5.0, [-65, -73]) +cms.SAXS.setCalibration([748, 1085], 5, [-65, -73]) + +RE.md['experiment_alias_directory'] = '/nsls2/data3/cms/legacy/xf11bm/data/2025_1/Commissioning' + + +#absorber_pos = EpicsSignal( 'XF:11BMB-ES{SM:1-Ax:ArmR}Mtr.RBV') +#sus_abs_low = SuspendFloor(absorber_pos, -56, resume_thresh=-55) +#sus_abs_hi = SuspendCeil(absorber_pos, -54, resume_thresh=-55) +#RE.install_suspender(sus_abs_low) +#RE.install_suspender(sus_abs_hi) +#from ophyd import EpicsSignal +#from bluesky.suspenders import SuspendFloor + +#beam_current = EpicsSignal('SR:OPS-BI{DCCT:1}I:Real-I') +#sus = SuspendFloor(beam_current, 100, resume_thresh=101) +#RE.install_suspender(sus) +#RE.clear_suspenders() + +### DEFINE YOUR PARENT DATA FOLDER HERE + + +if False: + # The following shortcuts can be used for unit conversions. For instance, + # for a motor operating in 'mm' units, one could instead do: + # sam.xr( 10*um ) + # To move it by 10 micrometers. HOWEVER, one must be careful if using + # these conversion parameters, since they make implicit assumptions. + # For instance, they assume linear axes are all using 'mm' units. Conversely, + # you will not receive an error if you try to use 'um' for a rotation axis! + m = 1e3 + cm = 10.0 + mm = 1.0 + um = 1e-3 + nm = 1e-6 + + inch = 25.4 + pixel = 0.172 # Pilatus + + deg = 1.0 + rad = np.degrees(1.0) + mrad = np.degrees(1e-3) + urad = np.degrees(1e-6) + + + + +def get_default_stage(): + return stg + + +class SampleTSAXS(SampleTSAXS_Generic): + + def __init__(self, name, base=None, **md): + super().__init__(name=name, base=base, **md) + self.naming_scheme = ['name', 'extra', 'exposure_time'] + + +class SampleGISAXS(SampleGISAXS_Generic): + + def __init__(self, name, base=None, **md): + super().__init__(name=name, base=base, **md) + self.naming_scheme = ['name', 'extra', 'th', 'exposure_time'] + self.naming_scheme = ['name', 'extra', 'clock', 'temperature', 'exposure_time'] + + +class Sample(SampleTSAXS): +# class Sample(SampleGISAXS): + + def __init__(self, name, base=None, **md): + + super().__init__(name=name, base=base, **md) + + + #self.naming_scheme = ['name', 'extra', 'clock', 'temperature', 'th', 'exposure_time'] + #self.naming_scheme = ['name', 'extra', 'th', 'exposure_time'] + #self.naming_scheme = ['name', 'extra', 'th', 'exposure_time'] + #self.naming_scheme = ['name', 'extra', 'y', 'th', 'clock', 'exposure_time'] + self.naming_scheme = ['name', 'extra', 'x', 'y', 'exposure_time'] + #self.naming_scheme = ['name', 'extra', 'clock', 'temperature', 'exposure_time'] + + self._axes['y'].origin = 10 + self._axes['th'].origin = 1.1 + + self.md['exposure_time'] = 1 + #self.SAXS_time = 10 + #self.WAXS_time = 10 + self.SAXS_time = 5 + self.WAXS_time = 20 + + #self.incident_angles_default = [0.08, 0.10, 0.12, 0.15, 0.20] + #self.incident_angles_default = [0.08, 0.10, 0.12, 0.14, 0.16, 0.18, 0.20] + #self.incident_angles_default = [0.08, 0.12, 0.14, 0.20, 0.26, 0.32] #for 17kev/15kev + #self.incident_angles_default = [0.10, 0.16, 0.20] #for 17kev/15kev + #self.incident_angles_default = [0.08, 0.12, 0.15, 0.18, 0.20] #for 10kev + #self.incident_angles_default = [0.08, 0.12, 0.15, 0.18] #for 10kev LJR + #self.incident_angles_default = [0.12, 0.16, 0.20, 0.24] #for 10kev, Perovskites + self.incident_angles_default = [0.12, 0.16, 0.20, 0.24] #for 10kev, Perovskites + #self.incident_angles_default = [0.02, 0.04, 0.05, 0.06, 0.08, 0.09, 0.1, 0.12, 0.15] + #self.incident_angles_default = [0.02, 0.04, 0.06, 0.08, 0.1, 0.12, 0.15] + #self.incident_angles_default = [0.0] + + self.x_pos_default = [-1, 0, 1] + + self.total_flow = 20 + self.wetflow_default = self.total_flow*np.arange(.1, .51, .1) + self.wetwait_default = [1200,1200,1200,1200,1200] + + + + def _measureTimeSeries(self, exposure_time=None, num_frames=10, wait_time=None, extra=None, measure_type='measureTimeSeries', verbosity=3, **md): + + self.naming_scheme_hold = self.naming_scheme + self.naming_scheme = ['name', 'extra', 'clock', 'exposure_time'] + super().measureTimeSeries(exposure_time=exposure_time, num_frames=num_frames, wait_time=wait_time, extra=extra, measure_type=measure_type, verbosity=verbosity, **md) + self.naming_scheme = self.naming_scheme_hold + + def goto(self, label, verbosity=3, **additional): + super().goto(label, verbosity=verbosity, **additional) + # You can add customized 'goto' behavior here + + def scan_SAXSdet(self, exposure_time=None) : + SAXS_pos=[-73, 0, 73] + #SAXSx_pos=[-65, 0, 65] + + RE.md['stitchback'] = True + + for SAXSx_pos in SAXS_pos: + for SAXSy_pos in SAXS_pos: + mov(SAXSx, SAXSx_pos) + mov(SAXSy, SAXSy_pos) + self.measure(10) + + + def doTemperatures_series(self, exposure_time=15, output_channel='3', T1=160, T2=145, wait_time=60, wait_time2=40, temperature_tolerance=1, temperature_probe='temperature_C', verbosity=3): + #increasing + T_ramp_set(5) + for temperature in self.T_series: + self.setTemperature(temperature, output_channel=output_channel, verbosity=verbosity) + + # Wait until we reach the temperature + #while abs(self.temperature(verbosity=0) - temperature)>temperature_tolerance: + while abs(self.temperature(temperature_probe=temperature_probe, verbosity=0) - temperature)>temperature_tolerance: + if verbosity>=3: + print(' setpoint = {:.3f}°C, Temperature = {:.3f}°C \r'.format(self.temperature_setpoint()-273.15, self.temperature(temperature_probe=temperature_probe, verbosity=0)), end='') + time.sleep(poling_period) + + # Allow for additional equilibration at this temperature + if wait_time is not None: + time.sleep(wait_time) + + self.measure(exposure_time=exposure_time) + + #First cooling to T1 + self.setTemperature(T1, output_channel=output_channel, verbosity=verbosity) + + # Wait until we reach the temperature + #while abs(self.temperature(verbosity=0) - temperature)>temperature_tolerance: + while abs(self.temperature(temperature_probe=temperature_probe, verbosity=0) - T1)>temperature_tolerance: + if verbosity>=3: + print(' setpoint = {:.3f}°C, Temperature = {:.3f}°C \r'.format(self.temperature_setpoint()-273.15, self.temperature(temperature_probe=temperature_probe, verbosity=0)), end='') + time.sleep(poling_period) + + # Allow for additional equilibration at this temperature + if wait_time is not None: + time.sleep(wait_time) + + self.measure(exposure_time=exposure_time) + + #2nd cooling to T2 + T_ramp_set(2) + self.setTemperature(T2, output_channel=output_channel, verbosity=verbosity) + + # Wait until we reach the temperature + #while abs(self.temperature(verbosity=0) - temperature)>temperature_tolerance: + while abs(self.temperature(temperature_probe=temperature_probe, verbosity=0) - T2)>temperature_tolerance: + if verbosity>=3: + print(' setpoint = {:.3f}°C, Temperature = {:.3f}°C \r'.format(self.temperature_setpoint()-273.15, self.temperature(temperature_probe=temperature_probe, verbosity=0)), end='') + time.sleep(poling_period) + + # Allow for additional equilibration at this temperature + if wait_time is not None: + time.sleep(wait_time) + + self.measure(exposure_time=exposure_time) + + #3rd cooling process + T_ramp_set(Tramp) + self.setTemperature(T3, output_channel=output_channel, verbosity=verbosity) + + while abs(self.temperature(temperature_probe=temperature_probe, verbosity=0) - (T3-1))>temperature_tolerance: + self.measure(exposure_time=exposure_time) + time.sleep(wait_time2) + + + + def do(self, step=0, align_step=0, **md): + + #NOTE: if align_step =8 is not working, try align_step=4 + + if step<=1: + saxs_on() + get_beamline().modeAlignment() + + if step<=2: + self.xo() # goto origin + + + if step<=4: + self.yo() + self.tho() + + if step<=5: + self.align(step=align_step, reflection_angle=0.12) + #self.setOrigin(['y','th']) # This is done within align + + #if step<=7: + #self.xr(0.2) + + if step<=8: + get_beamline().modeMeasurement() + + if step<=10: + + if self.incident_angles==None: + incident_angles = self.incident_angles_default + else: + incident_angles = self.incident_angles + + + swaxs_on() + self.measureIncidentAngles_Stitch(incident_angles, exposure_time=self.SAXS_time, tiling='ygaps', **md) + #waxs_on() + #self._test2_measureIncidentAngles(self.incident_angles_default, exposure_time=self.WAXS_time, tiling='ygaps', **md) + + self.thabs(0.0) + + + + + def IC_int(self): + + ion_chamber_readout1=caget('XF:11BMB-BI{IM:3}:IC1_MON') + ion_chamber_readout2=caget('XF:11BMB-BI{IM:3}:IC2_MON') + ion_chamber_readout3=caget('XF:11BMB-BI{IM:3}:IC3_MON') + ion_chamber_readout4=caget('XF:11BMB-BI{IM:3}:IC4_MON') + + ion_chamber_readout=ion_chamber_readout1+ion_chamber_readout2+ion_chamber_readout3+ion_chamber_readout4 + + return ion_chamber_readout>1*5e-08 + + + def do_SAXS(self, step=0, align_step=0, **md): + + if step<=1: + saxs_on() + get_beamline().modeAlignment() + + if step<=2: + self.xo() # goto origin + + + if step<=4: + self.yo() + self.tho() + + if step<=5: + self.align(step=align_step) + #self.setOrigin(['y','th']) # This is done within align + + #if step<=7: + #self.xr(0.2) + + if step<=8: + get_beamline().modeMeasurement() + + if step<=10: + if self.incident_angles==None: + incident_angles = self.incident_angles_default + else: + incident_angles = self.incident_angles + + saxs_on_det() + self.measureIncidentAngles_Stitch(incident_angles, exposure_time=self.SAXS_time, tiling='ygaps', **md) + + #if self.exposure_time_SAXS==None: + #self.measureIncidentAngles(incident_angles, exposure_time=self.SAXS_time, tiling=self.tiling, **md) + #else: + #self.measureIncidentAngles(incident_angles, exposure_time=self.exposure_time_SAXS, tiling=self.tiling, **md) + + self.thabs(0.0) + + def align_y(self, step=0, reflection_angle=0.08, verbosity=3): + '''Align the sample with respect to the beam. GISAXS alignment involves + vertical translation to the beam center, and rocking theta to get the + sample plane parralel to the beam. Finally, the angle is re-optimized + in reflection mode. + + The 'step' argument can optionally be given to jump to a particular + step in the sequence.''' + + if verbosity>=4: + print(' Aligning Y {}'.format(self.name)) + + if step<=0: + # Prepare for alignment + + if RE.state!='idle': + RE.abort() + + if get_beamline().current_mode!='alignment': + #if verbosity>=2: + #print("WARNING: Beamline is not in alignment mode (mode is '{}')".format(get_beamline().current_mode)) + print("Switching to alignment mode (current mode is '{}')".format(get_beamline().current_mode)) + get_beamline().modeAlignment() + + + get_beamline().setDirectBeamROI() + + beam.on() + + if step<=9 and reflection_angle is not None: + # Final alignment using reflected beam + if verbosity>=4: + print(' align: reflected beam') + get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0) + #get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0, size=[12,2]) + + self.thabs(reflection_angle) + + fit_scan(smy, 0.2, 21, fit='max') + self.setOrigin(['y']) + + if step<=10: + self.thabs(0.0) + beam.off() + get_beamline().modeMeasurement() + + + def do_WAXS_only(self, step=0, align_step=0, **md): + if step<5: + self.xo() + self.yo() + self.tho() + get_beamline().modeMeasurement() + if step<=10: + if self.incident_angles==None: + incident_angles = self.incident_angles_default + else: + incident_angles = self.incident_angles + + waxs_on() + #for detector in get_beamline().detector: + #detector.setExposureTime(self.MAXS_time) + self.measureIncidentAngles_Stitch(incident_angles, exposure_time=self.WAXS_time, tiling='ygaps', **md) + + #if self.exposure_time_MAXS==None: + #self.measureIncidentAngles(incident_angles, exposure_time=self.MAXS_time, tiling=self.tiling, **md) + #else: + #self.measureIncidentAngles(incident_angles, exposure_time=self.exposure_time_MAXS, tiling=self.tiling, **md) + + self.thabs(0.0) + + def do_WAXS(self, step=0, align_step=0, reflection_angle=0.12, tiling='ygaps', **md): + + if step<=1: + saxs_on() + get_beamline().modeAlignment() + + if step<=2: + self.xo() # goto origin + + + if step<=4: + self.yo() + self.tho() + + if step<=5: + self.align(step=align_step, reflection_angle=reflection_angle ) + #self.setOrigin(['y','th']) # This is done within align + + #if step<=7: + #self.xr(0.2) + + if step<=8: + get_beamline().modeMeasurement() + + if step<=10: + if self.incident_angles==None: + incident_angles = self.incident_angles_default + else: + incident_angles = self.incident_angles + + waxs_on() # edited from waxs_on 3/25/19 through a saxs_on error + #for detector in get_beamline().detector: + #detector.setExposureTime(self.MAXS_time) + self.measureIncidentAngles_Stitch(incident_angles, exposure_time=self.WAXS_time, tiling=tiling, **md) + + #if self.exposure_time_MAXS==None: + #self.measureIncidentAngles(incident_angles, exposure_time=self.MAXS_time, tiling=self.tiling, **md) + #else: + #self.measureIncidentAngles(incident_angles, exposure_time=self.exposure_time_MAXS, tiling=self.tiling, **md) + self.thabs(0.0) + + def alignQuick(self, align_step=8, reflection_angle=0.12, verbosity=3): + + saxs_on() + get_beamline().modeAlignment() + #self.yo() + self.tho() + #beam.on() + + + if verbosity>=4: + print(' align: reflected beam') + get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0) + + self.thabs(reflection_angle) + + + + result = fit_scan(sth, 0.2, 41, fit='max') + #result = fit_scan(sth, 0.2, 81, fit='max') #it's useful for alignment of SmarAct stage + sth_target = result.values['x_max']-reflection_angle + if result.values['y_max']>10: + th_target = self._axes['th'].motor_to_cur(sth_target) + self.thsetOrigin(th_target) + + result = fit_scan(smy, 0.3, 21, fit='max') + smy_target = result.values['x_max'] + if result.values['y_max']>10: + smy_target = self._axes['y'].motor_to_cur(smy_target) + self.ysetOrigin(smy_target) + + + get_beamline().modeMeasurement() + + def align_crazy(self, reflection_angle=0.12, ROI_size=[10, 180], th_range=0.3, int_threshold=10, verbosity=3): + + #def ROI3 in 160pixels with the center located at reflection beam + get_beamline().setReflectedBeamROI(total_angle = reflection_angle*2, size=ROI_size) #set ROI3 + + rel_th = 1 + ct = 0 + cycle = 0 + self.thabs(reflection_angle) + while abs(rel_th) > 0.005 and ct < 5: + + + self.snap(0.5) + refl_ypos = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:MaxY_RBV') + refl_ypos_center = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:CentroidY_RBV') + + while abs(refl_ypos-refl_ypos_center) > 10: + print('The center does not match to the max') + self.thr(.05) + self.snap(0.5) + refl_ypos = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:MaxY_RBV') + refl_ypos_center = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:CentroidY_RBV') + + + #refl_ypos = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:SigmaY_RBV') + int_max = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:MaxValue_RBV') + + while int_max < int_threshold and cycle<=np.round(th_range/0.1): + + self.thabs(reflection_angle+th_range-cycle*0.1) + cycle += 1 + self.snap(0.5) + refl_ypos = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:MaxY_RBV') + int_max = caget('XF:11BMB-ES{Det:PIL2M}:Stats3:MaxValue_RBV') + #return False + cycle = 0 + + if int_max < int_threshold: + return False + + rel_ypos = refl_ypos - ROI_size[1]/2 + + rel_th = rel_ypos/get_beamline().SAXS.distance/1000*0.172/np.pi*180/2 + + print('The th offset is {}'.format(rel_th)) + self.thr(rel_th) + + ct += 1 + if abs(rel_th) > 0.005 : + print('Fast alignment failed after {} times of aligment'.format(ct)) + + return False + else: + self.thset(reflection_angle) + + return True + + def align_custom(self, step=0, reflection_angle=0.08, verbosity=3): + '''Align the sample with respect to the beam. GISAXS alignment involves + vertical translation to the beam center, and rocking theta to get the + sample plane parralel to the beam. Finally, the angle is re-optimized + in reflection mode. + + The 'step' argument can optionally be given to jump to a particular + step in the sequence.''' + + if verbosity>=3: + print(' Aligning {}'.format(self.name)) + + if step<=0: + # Prepare for alignment + + if RE.state!='idle': + RE.abort() + + if get_beamline().current_mode!='alignment': + #if verbosity>=2: + #print("WARNING: Beamline is not in alignment mode (mode is '{}')".format(get_beamline().current_mode)) + print("Switching to alignment mode (current mode is '{}')".format(get_beamline().current_mode)) + get_beamline().modeAlignment() + + + get_beamline().setDirectBeamROI() + + beam.on() + + + if step<=2: + if verbosity>=4: + print(' align: searching') + + # Estimate full-beam intensity + value = None + if True: + # You can eliminate this, in which case RE.md['beam_intensity_expected'] is used by default + self.yr(-2) + #detector = gs.DETS[0] + detector = get_beamline().detector[0] + value_name = get_beamline().TABLE_COLS[0] + RE(count([detector])) + value = detector.read()[value_name]['value'] + self.yr(+2) + + if 'beam_intensity_expected' in RE.md: + if value=4: + print(' align: fitting') + + fit_scan(smy, 1.2, 21, fit='HMi') + ##time.sleep(2) + fit_scan(sth, 1.5, 21, fit='max') + ##time.sleep(2) + + #if step<=5: + # #fit_scan(smy, 0.6, 17, fit='sigmoid_r') + # fit_edge(smy, 0.6, 17) + # fit_scan(sth, 1.2, 21, fit='max') + + + if step<=8: + #fit_scan(smy, 0.3, 21, fit='sigmoid_r') + + fit_edge(smy, 0.6, 21) + #fit_edge(smy, 0.3, 16) + #time.sleep(2) + #fit_edge(smy, 0.4, 21) + #fit_scan(sth, 0.8, 21, fit='COM') + #time.sleep(2) + #self.setOrigin(['y', 'th']) + self.setOrigin(['y']) + + + if step<=9 and reflection_angle is not None: + # Final alignment using reflected beam + if verbosity>=4: + print(' align: reflected beam') + + if self.align_crazy() == False: + + get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0) + #get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0, size=[12,2]) + + self.thabs(reflection_angle) + + result = fit_scan(sth, 0.4, 41, fit='max') + #result = fit_scan(sth, 0.2, 81, fit='max') #it's useful for alignment of SmarAct stage + sth_target = result.values['x_max']-reflection_angle + + if result.values['y_max']>50: + th_target = self._axes['th'].motor_to_cur(sth_target) + self.thsetOrigin(th_target) + + + get_beamline().setReflectedBeamROI(total_angle=reflection_angle*2.0) + self.thabs(reflection_angle) + fit_scan(smy, 0.3, 16, fit='max') + self.setOrigin(['y']) + + if step<=10: + self.thabs(0.0) + beam.off() + + def intMeasure(self, output_file, exposure_time=1): + '''Measure the transmission intensity of the sample by ROI4. + The intensity will be saved in output_file + ''' + if abs(beam.energy(verbosity=0) - 13.5) < 0.1: + #beam.setAbsorber(4) + beam.setTransmission(1e-4) + elif abs(beam.energy(verbosity=0) - 17) < 0.1: + beam.setTransmission(1e-6) + + # print('Absorber is moved to position {}'.format(beam.absorber()[0])) + + detselect([pilatus2M]) + #if beam.absorber()[0]>=4: + bsx.move(bsx.position+6) + #beam.setTransmission(1) + + self.measure(exposure_time) + + temp_data = self.transmission_data_output(4) + + cms.modeMeasurement() + #beam.setAbsorber(0) + #beam.absorber_out() + + #output_data = output_data.iloc[0:0] + + #create a data file to save the INT data + INT_FILENAME='{}/data/{}.csv'.format(os.path.dirname(__file__) , output_file) + + if os.path.isfile(INT_FILENAME): + output_data = pds.read_csv(INT_FILENAME, index_col=0) + output_data = pds.concat([output_data, temp_data]) + output_data.to_csv(INT_FILENAME) + else: + temp_data.to_csv(INT_FILENAME) + + def transmission_data_output(self, slot_pos): + '''Output the tranmission of direct beam + ''' + h = db[-1] + dtable = h.table() + + #beam.absorber_transmission_list = [1, 0.041, 0.0017425, 0.00007301075, 0.00000287662355, 0.000000122831826, 0.00000000513437] + scan_id = h.start['scan_id'] + I_bim5 = h.start['beam_int_bim5'] #beam intensity from bim5 + I0 = dtable.pilatus2M_stats4_total + filename = h.start['sample_name'] + exposure_time = h.start['sample_exposure_time'] + #I2 = dtable.pilatus2M_stats2_total + #I3 = 2*dtable.pilatus2M_stats1_total - dtable.pilatus2M_stats2_total + #In = I3 / beam.absorber_transmission_list[slot_pos] / exposure_time + + current_data = {'a_filename': filename, + 'b_scanID': scan_id, + 'c_I0': I0, + 'd_I_bim5': I_bim5, + 'e_absorber_slot': slot_pos, + #'f_absorber_ratio': beam.absorber_transmission_list[slot_pos], + 'f_absorber_ratio': 0.000001, + 'g_exposure_seconds': exposure_time} + + return pds.DataFrame(data=current_data) + +# class TransOffCenteredCustom(OffCenteredHoder): +# def __init__(self, name='OffCenteredHoderCustom', base=None, **kwargs): +# super().__init__(name=name, base=base, **kwargs) +# self._axes['x'].origin = -16.6 # -17.8 # -0.3 +# self._axes['y'].origin =10.46 #10.96 # -0.1 + + +# class CapillaryHolderThreeRowsCustom(CapillaryHolderThreeRows): + +# def __init__(self, name='CapillaryHolderThreeRowsCustom', base=None, **kwargs): +# super().__init__(name=name, base=base, **kwargs) + +# self._axes['x'].origin = -16.6 # -0.3 +# self._axes['y'].origin = -2 + +class CapillaryHolderCustom(CapillaryHolder): + def __init__(self, name='CapillaryHolderCustom', base=None, **kwargs): + super().__init__(name=name, base=base, **kwargs) + # self._axes['y'].origin = -1.8 + # self._axes['x'].origin = -16.8 + + #QCAPHolder + self._axes['y'].origin = -5 + self._axes['x'].origin = -20.4 + + + self.WAXS_time=10 + + ##self._axes['x'].origin = -59.3 + ###position for calibration + #self._axes['x'].origin = -71.5+.2 + #self._axes['y'].origin = 10 + #self._axes['th'].origin = 1.1 + + # CREATE DIRECTORIES TO SAVE DATA + #holder_data_folder = os.path.join(parent_data_folder, self.name) + #os.makedirs(holder_data_folder, exist_ok=True) + #os.makedirs(os.path.join(holder_data_folder, 'waxs'), exist_ok=True) + #os.makedirs(os.path.join(holder_data_folder, 'saxs'), exist_ok=True) + #os.makedirs(os.path.join(holder_data_folder, 'waxs/raw'), exist_ok=True) + #os.makedirs(os.path.join(holder_data_folder, 'saxs/raw'), exist_ok=True) + #RE.md['experiment_alias_directory'] = holder_data_folder + + #### COPY CURRENT STATE OF user.py TO SAMPLE DIRECTORY + #copyfile(os.path.join(parent_data_folder, 'user.py'), os.path.join(holder_data_folder,'user.py')) + + + #def doSamples(self, verbosity=3): + + ##maxs_on() + #for sample in self.getSamples(): + #if verbosity>=3: + #print('Doing sample {}...'.format(sample.name)) + #if sample.detector=='SAXS' or sample.detector=='BOTH': + #sample.do_SAXS() + + #for sample in self.getSamples(): + #if verbosity>=3: + #print('Doing sample {}...'.format(sample.name)) + #if sample.detector=='BOTH': + #sample.do_WAXS_only() + + #for sample in self.getSamples(): + #if verbosity>=3: + #print('Doing sample {}...'.format(sample.name)) + #if sample.detector=='WAXS': + #sample.do_WAXS() + + def intMeasures(self,output_file='Transmission_output'): + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving==True: + time.sleep(0.2) + sample.intMeasure(output_file=output_file) + + def doSamples(self, sequence='Outer', exposure_WAXS_time=10, exposure_SAXS_time=60, verbosity=3): + + if sequence =='Outer': + step = 0 + elif sequence == 'Inner': + step = 5 + else: + return print('Please define the first measurement: Outer or Inner') + if step< 1: + + waxs_on_outer() + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving: + time.sleep(0.3) + sample.measure(exposure_WAXS_time, extra='outer-normal', tiling='ygaps') + + detselect(pilatus2M) + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving: + time.sleep(0.3) + sample.measure(exposure_SAXS_time, tiling='ygaps') + + if step< 10: + waxs_on_inner() + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving: + time.sleep(0.3) + sample.measure(exposure_WAXS_time, extra='inner-normal', tiling='ygaps') + + + if step > 1: + waxs_on_outer() + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving: + time.sleep(0.3) + sample.measure(exposure_WAXS_time, extra='outer-normal', tiling='ygaps') + + detselect(pilatus2M) + for sample in self.getSamples(): + sample.gotoOrigin() + while smx.moving: + time.sleep(0.3) + sample.measure(exposure_SAXS_time, tiling='ygaps') + + + def doSamples_custom(self, exposure_WAXS_time=10, exposure_SAXS_time=60, verbosity=3, tiling='ygaps'): + + # swaxs_on() + for sample in self.getSamples(): + sample.gotoOrigin() + sample.measure(exposure_WAXS_time) + # sample.measure(exposure_WAXS_time, tiling=tiling) + # for sample in self.getSamples(): + # sample.gotoOrigin() + # sample.intMeasure(output_file='Transmission_output') + + def measure_meshscan(self, exposure_time=10, xmin=-6, xmax=6, ymin=-6, ymax=6, interval=0.2, verbosity=3, tiling='ygaps'): + + # swaxs_on() + # sam=Sample("Tianjian2_PBLS100") + # sample.gotoOrigin() + + for sample in self.getSamples(): + + sample.gotoOrigin() + for xx in np.arange(xmin,xmax,interval): + sample.xabs(xx) + while smx.moving: + time.sleep(0.1) + for yy in np.arange(ymin,ymax,interval): + sample.yabs(yy) + while smy.moving: + time.sleep(0.1) + if tiling=='ygaps': + extra='pos1' + else: + extra=None + sample.measure(exposure_time, extra=extra) + + + if tiling=='ygaps': + WAXSyo = WAXSy.position + WAXSy.move(WAXSyo+5.16) + SAXSyo = SAXSy.position + SAXSy.move(SAXSyo+5.16) + + + for xx in np.arange(xmin,xmax,interval): + sample.xabs(xx) + while smx.moving: + time.sleep(0.1) + for yy in np.arange(ymin,ymax,interval): + sample.yabs(yy) + while smy.moving: + time.sleep(0.1) + sample.measure(exposure_time, extra='pos2') + + + WAXSy.move(WAXSyo) + SAXSy.move(SAXSyo) + + # sample.measure(exposure_WAXS_time, tiling=tiling) + # for sample in self.getSamples(): + # sample.gotoOrigin() + # sample.intMeasure(output_file='Transmission_output') + + + + +def swaxs_on(): + detselect([pilatus2M, pilatus800]) + WAXSx.move(-191) + WAXSy.move(16) + + +def saxs_on(): + detselect(pilatus2M) + #WAXSx.move(-195) + #WAXSy.move(24) + +def saxs_on_det(): + detselect(pilatus2M) + WAXSx.move(-200) + WAXSy.move(30) + +def waxs_on(): + detselect(pilatus800) + WAXSx.move(-192) + WAXSy.move(20) + + +# def waxs_on_outer(): #for inner-outer stitching +# detselect(pilatus800) +# WAXSx.move(-230) +# WAXSy.move(40) + +# def waxs_on_inner(): #for inner-outer stitching +# detselect(pilatus800) +# WAXSx.move(-195) +# WAXSy.move(20) + + + +def waxs_on_outer(): #for inner-outer stitching + # detselect(pilatus800) + detselect([pilatus2M,pilatus800]) + WAXSx.move(-240) + WAXSy.move(50) + +def waxs_on_inner(): #for inner-outer stitching + detselect(pilatus800) + # WAXSx.move(-210) + # WAXSy.move(20) + WAXSx.move(-195) + WAXSy.move(17) + + + + + +if True: + + cali = CapillaryHolder(base=stg) + #hol = CapillaryHolderCustom(base=stg) + + cali.addSampleSlot( Sample('FL_screen'), 5.0 ) + cali.addSampleSlot( Sample('AgBH_cali_5m_13.5kev'), 8.0 ) + cali.addSampleSlot( Sample('Empty'), 11.0 ) + +if True: + + # Example of a multi-sample holder + + md = { + 'owner' : 'beamline' , + 'series' : 'various' , + } + hol1 = CapillaryHolderCustom(base=stg) + hol1.addGaragePosition(1,1) + hol1.name = 'hol1' + hol1.addSampleSlot( Sample('TL_PBDF_power'),1) + hol1.addSampleSlot( Sample('TL_PBDF_thinfilm'),2) + + hol1.addSampleSlot( Sample('HN_s1'),4) + + + hol1.addSampleSlot( Sample('HN_s2'),7) + + + hol1.addSampleSlot( Sample('HN_s3'),10) + + hol1.addSampleSlot( Sample('HN_s4'),12) + + hol1.addSampleSlot( Sample('HN_s5'),14) + + + + hol2 = CapillaryHolderCustom(base=stg) + hol2.addGaragePosition(1,2) + hol2.name = 'hol2' + + hol2.addSampleSlot( Sample('HN_s6'),2) + + hol2.addSampleSlot( Sample('HN_s7'),4) + + + hol2.addSampleSlot( Sample('HN_s8'),7) + + + + + que = Queue(base=stg) + que.addHolderIntoQueue(hol1, [1, 1], 1) + que.addHolderIntoQueue(hol2, [1, 2], 2) + # que.addHolderIntoQueue(hol3, [1, 3], 3) + # que.addHolderIntoQueue(hol4, [3, 1], 4) + # # que.addHolderIntoQueue(hol1, [2, 2], 4) + # # #que.addHolderIntoQueue(hol5, [2, 2], 5) + # # #que.addHolderIntoQueue(hol6, [2, 3], 6) + # # ##que.addHolderIntoQueue(hol7, [3, 1], 7) + # # #que.addHolderIntoQueue(hol8, [3, 2], 8) + # # #que.addHolderIntoQueue(hol9, [3, 3], 9) + # # #que.addHolderIntoQueue(hol10, [4, 1], 10) + # # #que.addHolderIntoQueue(hol11, [1, 2], 2) + # # #que.addHolderIntoQueue(hol10, [4, 1], 10) + # # #que.addHolderIntoQueue(hol11, [4, 2], 11) + # # #que.addHolder(hol3, [2, 3]) + # # que.addHolderIntoQueue(cali, [3, 1], 10) + que.setSequence() + que.checkStatus(verbosity=5) + + + + + +#robot.listGarage() + + +''' +-15.2, -9.3, 2 + + +In [18]: wbs() +bsx = -13.800244 +bsy = 17.5 +bsphi = -205.69908099999998 + + + + +2024_07_13 +rod beamstop +In [1153]: wbs() +bsx = -16.699974 +bsy = 16.999982 +bsphi = -204.999438 + +round beamstop +In [1162]: wbs() +bsx = -18.2 +bsy = -9.0 +bsphi = 2.0 + + + +2023_3 (Sept) + +################################# +Run 2 + +17 keV, TSAXS/WAXS, in vacuum +beam size (0.2, 0.2) + +round beamstop +In [75]: wbs() +bsx = -18.393963 +bsy = -0.2998839999999997 +bsphi = -20.012715 + +ThreeRows +Capillary + + + + +################################# +Run 1 + +17 keV, edge on, in vacuum +beam size (0.2, 0.2) + +rod beamstop +In [558]: wbs() +bsx = -15.591719999999999 +bsy = 17.000437 +bsphi = -177.99958500000002 + +offcenter sample holder, +find the good sample spot manually and set origin 'x' and 'y' +measure stitched, inner and outer + +''' + + + + +''' + +rob bs + + +In [2274]: wbs() +bsx = -12.000095 +bsy = 14.499919 +bsphi = -182.501192 + + +round bs + +''' \ No newline at end of file From 66e793388292da8820f0e949a3eab677d7333d01 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 17 Jan 2025 09:41:28 -0500 Subject: [PATCH 02/18] caproto ioc blackhole with custom PVs --- iocs/shutter.py | 14 ------- iocs/spoof_beamline.py | 95 ++++++++++++++++++++++-------------------- 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/iocs/shutter.py b/iocs/shutter.py index ebaee05..b4e28c4 100644 --- a/iocs/shutter.py +++ b/iocs/shutter.py @@ -7,20 +7,6 @@ from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup -class Shutter(PVGroup): - shutter = pvproperty(value=0, name="{{Shutter}}", dtype=ChannelType.INT) - psh_blade2_pos = pvproperty(value=0, name="{{Psh_blade2}}Pos", dtype=ChannelType.INT) - psh_blade1_pos = pvproperty(value=0, name="{{Psh_blade1}}Pos", dtype=ChannelType.INT) - - def __init__(self, *args, **kwargs): - super().__init__(prefix="XF:11BM-ES", *args, **kwargs) - - @shutter.putter # type: ignore - async def shutter(self, instance, value): - await asyncio.sleep(0.1) - await self.psh_blade2_pos.write(value) - await self.psh_blade1_pos.write(value) - return value def main(): diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index 5fa7acf..fdb0d6a 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -52,10 +52,11 @@ class Shutter(PVGroup): psh_blade1_pos = pvproperty(value=0, name="{{Psh_blade1}}Pos", dtype=ChannelType.INT) def __init__(self, *args, **kwargs): + # Initialize the explicit pv properties super().__init__(prefix="XF:11BM-ES", *args, **kwargs) - # FIXME: Find a way to get pvdb updated with shutter, psh_blade2_pos, psh_blade1_pos - self.pvdb = ReallyDefaultDict(fabricate_channel) - self._create_pvdb() + # Overwrite the pvdb with the blackhole, while keeping the explicit pv properties + self.old_pvdb = self.pvdb.copy() + self.pvdb = ReallyDefaultDict(self.fabricate_channel) @shutter.putter # type: ignore async def shutter(self, instance, value): @@ -64,48 +65,52 @@ async def shutter(self, instance, value): await self.psh_blade1_pos.write(value) return value -def fabricate_channel(key): - if 'PluginType' in key: - for pattern, val in PLUGIN_TYPE_PVS: - if pattern.search(key): - return ChannelString(value=val) - elif 'ArrayPort' in key: - return ChannelString(value="PIL") - elif 'PortName' in key: - port_name = key.split(':')[-2] - if port_name == "cam1": - port_name = "PIL" - return ChannelString(value=port_name) - elif 'name' in key.lower(): - return ChannelString(value=key) - elif 'EnableCallbacks' in key: - return ChannelEnum(value=0, enum_strings=['Disabled', 'Enabled']) - elif 'BlockingCallbacks' in key: - return ChannelEnum(value=0, enum_strings=['No', 'Yes']) - elif 'Auto' in key: - return ChannelEnum(value=0, enum_strings=['No', 'Yes']) - elif 'ImageMode' in key: - return ChannelEnum(value=0, enum_strings=['Single', 'Multiple', 'Continuous']) - elif 'WriteMode' in key: - return ChannelEnum(value=0, enum_strings=['Single', 'Capture', 'Stream']) - elif 'ArraySize' in key: - return ChannelData(value=10) - elif 'TriggerMode' in key: - return ChannelEnum(value=0, enum_strings=['Internal', 'External']) - elif 'FileWriteMode' in key: - return ChannelEnum(value=0, enum_strings=['Single']) - elif 'FilePathExists' in key: - return ChannelData(value=1) - elif 'WaitForPlugins' in key: - return ChannelEnum(value=0, enum_strings=['No', 'Yes']) - elif ('file' in key.lower() and 'number' not in key.lower() and - 'mode' not in key.lower()): - return ChannelChar(value='a' * 250) - elif ('filenumber' in key.lower()): - return ChannelInteger(value=0) - elif 'Compression' in key: - return ChannelEnum(value=0, enum_strings=['None', 'N-bit', 'szip', 'zlib', 'blosc']) - return ChannelDouble(value=0.0) + def fabricate_channel(self, key): + # If the channel already exists from initialization, return it + if key in self.old_pvdb: + return self.old_pvdb[key] + # Otherwise, fabricate new channels + if 'PluginType' in key: + for pattern, val in PLUGIN_TYPE_PVS: + if pattern.search(key): + return ChannelString(value=val) + elif 'ArrayPort' in key: + return ChannelString(value="PIL") + elif 'PortName' in key: + port_name = key.split(':')[-2] + if port_name == "cam1": + port_name = "PIL" + return ChannelString(value=port_name) + elif 'name' in key.lower(): + return ChannelString(value=key) + elif 'EnableCallbacks' in key: + return ChannelEnum(value=0, enum_strings=['Disabled', 'Enabled']) + elif 'BlockingCallbacks' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif 'Auto' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif 'ImageMode' in key: + return ChannelEnum(value=0, enum_strings=['Single', 'Multiple', 'Continuous']) + elif 'WriteMode' in key: + return ChannelEnum(value=0, enum_strings=['Single', 'Capture', 'Stream']) + elif 'ArraySize' in key: + return ChannelData(value=10) + elif 'TriggerMode' in key: + return ChannelEnum(value=0, enum_strings=['Internal', 'External']) + elif 'FileWriteMode' in key: + return ChannelEnum(value=0, enum_strings=['Single']) + elif 'FilePathExists' in key: + return ChannelData(value=1) + elif 'WaitForPlugins' in key: + return ChannelEnum(value=0, enum_strings=['No', 'Yes']) + elif ('file' in key.lower() and 'number' not in key.lower() and + 'mode' not in key.lower()): + return ChannelChar(value='a' * 250) + elif ('filenumber' in key.lower()): + return ChannelInteger(value=0) + elif 'Compression' in key: + return ChannelEnum(value=0, enum_strings=['None', 'N-bit', 'szip', 'zlib', 'blosc']) + return ChannelDouble(value=0.0) def main(): From 6b24a4286706f0442d976d3489b98f08314cd36f Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 17 Jan 2025 16:43:25 -0500 Subject: [PATCH 03/18] day 2 progress --- .ci/drop-in.py | 3 ++- iocs/spoof_beamline.py | 34 +++++++++++++++++++++++++++------- startup/81-beam.py | 10 +++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.ci/drop-in.py b/.ci/drop-in.py index 11f69c7..84117e8 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -1,2 +1,3 @@ -sam = Sample("test") +caput(beam.mono_bragg_pv, 1.03953) +sam = SampleGISAXS('test') detselect(pilatus2M) diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index fdb0d6a..e471ffa 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -5,7 +5,8 @@ from caproto import (ChannelChar, ChannelData, ChannelDouble, ChannelEnum, ChannelInteger, ChannelString, ChannelType) -from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup +from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup, SubGroup +from caproto.ioc_examples.fake_motor_record import FakeMotor PLUGIN_TYPE_PVS = [ (re.compile('image\\d:'), 'NDPluginStdArrays'), @@ -32,11 +33,19 @@ class ReallyDefaultDict(defaultdict): def __contains__(self, key): #if "{Shutter}" in key or "{Psh_blade2}Pos" in key or "{Psh_blade1}Pos" in key: # return False + if "XF:11BMB-ES{Chm:Smpl-Ax:" in key: + return False + if "XF:11BMB-ES{BS-Ax:" in key: + return False return True def __missing__(self, key): #if "{Shutter}" in key or "{Psh_blade2}Pos" in key or "{Psh_blade1}Pos" in key: # return None + if "XF:11BMB-ES{Chm:Smpl-Ax:" in key: + return None + if "XF:11BMB-ES{BS-Ax:" in key: + return None if (key.endswith('-SP') or key.endswith('-I') or key.endswith('-RB') or key.endswith('-Cmd')): key, *_ = key.rpartition('-') @@ -51,12 +60,9 @@ class Shutter(PVGroup): psh_blade2_pos = pvproperty(value=0, name="{{Psh_blade2}}Pos", dtype=ChannelType.INT) psh_blade1_pos = pvproperty(value=0, name="{{Psh_blade1}}Pos", dtype=ChannelType.INT) - def __init__(self, *args, **kwargs): + def __init__(self, prefix: str, *args, **kwargs): # Initialize the explicit pv properties - super().__init__(prefix="XF:11BM-ES", *args, **kwargs) - # Overwrite the pvdb with the blackhole, while keeping the explicit pv properties - self.old_pvdb = self.pvdb.copy() - self.pvdb = ReallyDefaultDict(self.fabricate_channel) + super().__init__(prefix=prefix, *args, **kwargs) @shutter.putter # type: ignore async def shutter(self, instance, value): @@ -65,6 +71,20 @@ async def shutter(self, instance, value): await self.psh_blade1_pos.write(value) return value + +class CMS_IOC(PVGroup): + shutter = SubGroup(Shutter, prefix="XF:11BM-ES") + #motor = SubGroup(FakeMotor, prefix="XF:11BMB-ES{{Chm:Smpl-Ax:X}}Mtr") + + def __init__(self, *args, **kwargs): + # Initialize the explicit pv properties + super().__init__(prefix="", *args, **kwargs) + # Overwrite the pvdb with the blackhole, while keeping the explicit pv properties + self.old_pvdb = self.pvdb.copy() + print(self.old_pvdb) + self.pvdb = ReallyDefaultDict(self.fabricate_channel) + + def fabricate_channel(self, key): # If the channel already exists from initialization, return it if key in self.old_pvdb: @@ -141,7 +161,7 @@ def main(): default_prefix='', desc="PV black hole") run_options['interfaces'] = ['127.0.0.1'] - run(Shutter().pvdb, + run(CMS_IOC().pvdb, **run_options) diff --git a/startup/81-beam.py b/startup/81-beam.py index fdb96a7..9aa3651 100644 --- a/startup/81-beam.py +++ b/startup/81-beam.py @@ -1068,7 +1068,7 @@ def energy(self, verbosity=3): wavelength_A = 2.0 * self.dmm_dsp * np.sin(Bragg_rad) wavelength_m = wavelength_A * 1e-10 - energy_eV = self.hc_over_e / (wavelength_m + 1e-8) + energy_eV = self.hc_over_e / wavelength_m energy_keV = energy_eV / 1000.0 if verbosity >= 3: @@ -1508,7 +1508,7 @@ def transmission(self, verbosity=3): energy_keV = self.energy(verbosity=0) if energy_keV < 6.0 or energy_keV > 18.0: - print("Transmission data not available at the current X-ray energy ({.2f} keV).".format(energy_keV)) + print("Transmission data not available at the current X-ray energy ({:.2f} keV).".format(energy_keV)) else: # The states of the foils in the filter box @@ -1651,7 +1651,7 @@ def setTransmission(self, transmission, retries=3, tolerance=0.7, verbosity=3): energy_keV = self.energy(verbosity=0) if energy_keV < 6.0 or energy_keV > 18.0: - print("Transmission data not available at the current X-ray energy ({.2f} keV).".format(energy_keV)) + print("Transmission data not available at the current X-ray energy ({:.2f} keV).".format(energy_keV)) elif transmission > 1.0: print("A transmission above 1.0 is not possible.") @@ -1735,7 +1735,7 @@ def absorber(self, verbosity=3): energy_keV = self.energy(verbosity=0) if energy_keV < 6.0 or energy_keV > 18.0: - print("Transmission data not available at the current X-ray energy ({.2f} keV).".format(energy_keV)) + print("Transmission data not available at the current X-ray energy ({:.2f} keV).".format(energy_keV)) else: # The foil layers @@ -1826,7 +1826,7 @@ def setAbsorber(self, slot, retries=3, tolerance=0.5, verbosity=3): energy_keV = self.energy(verbosity=0) if energy_keV < 6.0 or energy_keV > 18.0: - print("Transmission data not available at the current X-ray energy ({.2f} keV).".format(energy_keV)) + print("Transmission data not available at the current X-ray energy ({:.2f} keV).".format(energy_keV)) # elif slot < 0 or slot > 6: # print('Absorber cannot move beyond [0, 6]') elif slot < 0 or slot > 7: # changed by RL, 202307 From 32973688bd6183d5327f9d73139b4d2c778df467 Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Sun, 19 Jan 2025 21:24:41 -0500 Subject: [PATCH 04/18] Use epics-containers/ADSimDetector for Pilatus --- .ci/commands.py | 16 ++++++++++++++++ .ci/drop-in.py | 2 ++ iocs/spoof_beamline.py | 2 ++ 3 files changed, 20 insertions(+) create mode 100644 .ci/commands.py diff --git a/.ci/commands.py b/.ci/commands.py new file mode 100644 index 0000000..254f07e --- /dev/null +++ b/.ci/commands.py @@ -0,0 +1,16 @@ +cms.setMetadata() +%run -i user_TSAXSWAXS.py + +sam = SampleGISAXS('test') +detselect(pilatus2M) +sam.measureIncidentAngle(0.1, exposure_time=1) + +sam.xr(1) +sam.thr(0.1) +sam.thabs(0.2) + +cms.setMetadata() +sam.measure(1) +cms.modeMeasurement() +wbs() +sam.measure(1) diff --git a/.ci/drop-in.py b/.ci/drop-in.py index 84117e8..d21616a 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -1,3 +1,5 @@ caput(beam.mono_bragg_pv, 1.03953) sam = SampleGISAXS('test') detselect(pilatus2M) + +pilatus2M.cam.num_images.put(1) diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index e471ffa..5cf41ca 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -46,6 +46,8 @@ def __missing__(self, key): return None if "XF:11BMB-ES{BS-Ax:" in key: return None + if "XF:11BMB-ES{Det:PIL2M}" in key: + return None if (key.endswith('-SP') or key.endswith('-I') or key.endswith('-RB') or key.endswith('-Cmd')): key, *_ = key.rpartition('-') From b455af61f7b56df171c58bd4e947cbc11a5f4f80 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 21 Jan 2025 16:18:13 -0500 Subject: [PATCH 05/18] Create directory for tiff plugin --- .ci/drop-in.py | 1 + iocs/spoof_beamline.py | 2 ++ startup/94-sample.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.ci/drop-in.py b/.ci/drop-in.py index d21616a..47f0d8a 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -1,4 +1,5 @@ caput(beam.mono_bragg_pv, 1.03953) +caput("XF:11BMB-ES{Det:PIL2M}:TIFF1:CreateDirectory", -20) sam = SampleGISAXS('test') detselect(pilatus2M) diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index 5cf41ca..6461089 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -37,6 +37,8 @@ def __contains__(self, key): return False if "XF:11BMB-ES{BS-Ax:" in key: return False + if "XF:11BMB-ES{Det:PIL2M}" in key: + return False return True def __missing__(self, key): diff --git a/startup/94-sample.py b/startup/94-sample.py index 378c45e..90d3f74 100644 --- a/startup/94-sample.py +++ b/startup/94-sample.py @@ -2171,7 +2171,7 @@ def handle_file(self, detector, extra=None, verbosity=3, subdirs=True, linksave= filename = detector.tiff.full_file_name.get() # RL, 20210831 if not os.path.isfile(filename): - print("File does not exist") + print(f"File does not exist: {filename}") return # Alternate method to get the last filename # filename = '{:s}/{:s}.tiff'.format( detector.tiff.file_path.get(), detector.tiff.file_name.get() ) From d1a9dbff71f0bef0dc67936f4b7f82499caae959 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 21 Jan 2025 17:01:09 -0500 Subject: [PATCH 06/18] Use TIFFPlugin_V33 --- .ci/drop-in.py | 2 +- startup/20-area-detectors.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/drop-in.py b/.ci/drop-in.py index 47f0d8a..b89bc18 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -1,5 +1,5 @@ caput(beam.mono_bragg_pv, 1.03953) -caput("XF:11BMB-ES{Det:PIL2M}:TIFF1:CreateDirectory", -20) +pilatus2M.tiff.create_directory.set(-20) sam = SampleGISAXS('test') detselect(pilatus2M) diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index b7fe5da..495f177 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -3,7 +3,6 @@ from ophyd import ( ProsilicaDetector, SingleTrigger, - TIFFPlugin, ImagePlugin, DetectorBase, HDF5Plugin, @@ -19,6 +18,7 @@ StatsPlugin, ) from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.plugins import TIFFPlugin_V33 from ophyd.areadetector.base import ADComponent, EpicsSignalWithRBV from ophyd.areadetector.filestore_mixins import FileStoreTIFFIterativeWrite from ophyd import Component as Cpt, Signal @@ -62,7 +62,7 @@ -class TIFFPluginWithFileStore(TIFFPlugin, FileStoreTIFFIterativeWrite): +class TIFFPluginWithFileStore(TIFFPlugin_V33, FileStoreTIFFIterativeWrite): pass class HDF5PluginWithFileStore(HDF5Plugin, FileStoreHDF5IterativeWrite): From 31d74cd56718ed23a2b49204c9031b364bfac719 Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Thu, 6 Feb 2025 16:08:15 +0100 Subject: [PATCH 07/18] Add `cms.yml` and update `00-startup.py` --- .ci/cms.yml | 16 ++++++++++++++++ startup/00-startup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .ci/cms.yml diff --git a/.ci/cms.yml b/.ci/cms.yml new file mode 100644 index 0000000..0775ebf --- /dev/null +++ b/.ci/cms.yml @@ -0,0 +1,16 @@ +metadatastore: + module: 'databroker.headersource.mongo' + class: 'MDS' + config: + host: 'localhost' + port: 27017 + database: 'test-datastore' + timezone: 'US/Eastern' +assets: + module: 'databroker.assets.mongo' + class: 'Registry' + config: + host: 'localhost' + port: 27017 + database: 'test-filestore' + diff --git a/startup/00-startup.py b/startup/00-startup.py index 204710c..38c82c1 100644 --- a/startup/00-startup.py +++ b/startup/00-startup.py @@ -15,7 +15,7 @@ from redis_json_dict import RedisJSONDict -nslsii.configure_base(get_ipython().user_ns, "temp", publish_documents_with_kafka=True) +nslsii.configure_base(get_ipython().user_ns, "cms", publish_documents_with_kafka=True) from bluesky.magics import BlueskyMagics from bluesky.preprocessors import pchain From 8711f8bed9410e821b43af63d3fa058f7017b315 Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Fri, 7 Feb 2025 10:52:41 +0100 Subject: [PATCH 08/18] CI: add a script for tiled config - `.ci/tiled-config.sh` --- .ci/tiled-config.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 .ci/tiled-config.sh diff --git a/.ci/tiled-config.sh b/.ci/tiled-config.sh new file mode 100755 index 0000000..9277949 --- /dev/null +++ b/.ci/tiled-config.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Tiled profile config: + +tiled_profiles_dir="$HOME/.config/tiled/profiles/" +beamline_acronym="cms" + +mkdir -v -p ${tiled_profiles_dir} + +cat << EOF > ${tiled_profiles_dir}/profiles.yml +${beamline_acronym:-local}: + direct: + authentication: + allow_anonymous_access: true + trees: + - tree: databroker.mongo_normalized:Tree.from_uri + path: / + args: + uri: mongodb://localhost:27017/metadatastore-local + asset_registry_uri: mongodb://localhost:27017/asset-registry-local +EOF + +cat ${tiled_profiles_dir}/profiles.yml From 0a3f78568cfb682d019e6f3afa5470f12acff21e Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Fri, 7 Feb 2025 14:56:45 +0100 Subject: [PATCH 09/18] CI: 2024-2.3 conda envs --- azure-pipelines.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d190d26..4abf909 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,18 +7,12 @@ resources: endpoint: github jobs: - - template: collection-2021-1.2.yml@templates + - template: 2024-2.3-py310-tiled.yml@templates parameters: beamline_acronym: CMS - - template: nsls2-collection-2021-2.2.yml@templates + - template: 2024-2.3-py311-tiled.yml@templates parameters: beamline_acronym: CMS - - template: nsls2-collection-2021-2.2-py39.yml@templates - parameters: - beamline_acronym: CMS - - template: nsls2-collection-2021-3.0-py37.yml@templates - parameters: - beamline_acronym: CMS - - template: nsls2-collection-2021-3.0-py39.yml@templates + - template: 2024-2.3-py312-tiled.yml@templates parameters: beamline_acronym: CMS From 945f7eb7cd4a76bd23aeec19cb3dfa8cb6aaa920 Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Tue, 11 Feb 2025 17:05:14 -0500 Subject: [PATCH 10/18] CI: enable GHA testing --- .ci/drop-in.py | 2 + .github/workflows/testing.yml | 108 ++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 .github/workflows/testing.yml diff --git a/.ci/drop-in.py b/.ci/drop-in.py index b89bc18..d26eb14 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -4,3 +4,5 @@ detselect(pilatus2M) pilatus2M.cam.num_images.put(1) + +RE(bp.count([pilatus2M], num=3)) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..a41e6b3 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,108 @@ +name: Tests + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + run_tests: + # pull requests are a duplicate of a branch push if within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + + name: Test IPython startup files + runs-on: ubuntu-latest + strategy: + matrix: + conda-env-version: ["2024-3.0"] + zenodo-id: ["14019710"] + md5-checksum: ["0ec1899a4f5989292e07380f96b97498"] + python-version: ["3.10"] # , "3.11", "3.12"] + fail-fast: false + env: + TZ: America/New_York + + defaults: + run: + shell: bash -l {0} + + steps: + - name: Set env vars + run: | + export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} # just the repo, as opposed to org/repo + echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV + + export PYTHONVER=$(echo ${{ matrix.python-version }} | sed 's/\.//g') + echo "PYTHONVER=${PYTHONVER}" >> $GITHUB_ENV + + export CONDA_ENV_NAME="${{ matrix.conda-env-version }}-py${PYTHONVER}-tiled" + echo "CONDA_ENV_NAME=${CONDA_ENV_NAME}" >> $GITHUB_ENV + + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Setup umamba + uses: mamba-org/setup-micromamba@v1 + + - name: Setup umamba + run: | + set -vxeo pipefail + url="https://zenodo.org/record/${{ matrix.zenodo-id }}/files/${CONDA_ENV_NAME}.tar.gz?download=1" + wget --progress=dot:giga ${url} -O ${CONDA_ENV_NAME}.tar.gz + status=$? + if [ $status -gt 0 ]; then + echo "Cannot download from ${url}. Exit code: ${status}" + exit $status + fi + echo "${{ matrix.md5-checksum }} ${CONDA_ENV_NAME}.tar.gz" > checksum.txt + md5sum --check checksum.txt + mkdir -p $HOME/${CONDA_ENV_NAME} + tar -xf ${CONDA_ENV_NAME}.tar.gz -C $HOME/${CONDA_ENV_NAME} + conda activate $HOME/${CONDA_ENV_NAME} + conda unpack + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.6.0 + + - name: Prepare databroker config + run: | + set -vxeuo pipefail + mkdir -v -p $HOME/.config/databroker/ + cp -v configs/databroker/local.yml $HOME/.config/databroker/ + + - name: Start epics-containers + run: | + set -vxeuo pipefail + git clone --depth 1 --branch CMS-IOCs https://github.com/NSLS2/cms-epics-containers.git ~/cms-epics-containers + source ~/cms-epics-containers/environment.sh + cd ~/cms-epics-containers + docker-compose up -d + sleep 20 + + - name: Start caproto IOC + run: | + set -vxeuo pipefail + source ~/cms-epics-containers/environment.sh + echo "\n" | python iocs/spoof_beamline.py --list-pvs + sleep 20 + + - name: Test the code + run: | + set -vxeuo pipefail + # This is what IPython does internally to load the startup files: + command=" + import os + import glob + ip = get_ipython() + startup_files = sorted(glob.glob(os.path.join(os.getcwd(), 'startup/*.py'))) + if os.path.isfile('.ci/drop-in.py'): + startup_files.append('.ci/drop-in.py') + if not startup_files: + raise SystemExit(f'Cannot find any startup files in {os.getcwd()}') + for f in startup_files: + if not os.path.isfile(f): + raise FileNotFoundError(f'File {f} cannot be found.') + print(f'Executing {f} in CI') + ip.parent._exec_file(f)" + + ipython --profile=test -c "$command" From dcd8c3abfa2984ed9f299487e78684b24272d9f7 Mon Sep 17 00:00:00 2001 From: Max Rakitin Date: Tue, 4 Mar 2025 14:06:40 -0500 Subject: [PATCH 11/18] CI: enable IOC tests (#3) * CI: fix conda/mamba command * CI: switch to conda * CI: add missing configs * CI: use mrakitin fork for epics-services with minor fixes * CI: install docker-compose * CI: install docker-compose from GH release * CI: send caproto process to background * CI: source env vars for IPython profile startup * CI: run beamline specific steps * CI: fix the config file name * CI: configure beamline redis * CI: more diag checks of the redis address * CI: stop execution in scripts if there are failures * CI: use sudo for making dirs * CI: non-blocking ping * CI: run redis addr diag after the service start * CI: more tests in `drop-in.py` * CI: mount data directory where AD will write the data * CI: fix the target for symlink for the detector data dir * CI: check the dir tree with the saved files after triggering the detector * CI: check dirs simpler * CI: uncomment the check of /nsls2/ * CI: fix the symlink to /nsls/data/ * CI: check `/nsls2/data/` contents * CI: `docker-compose` -> `docker compose`; install `xorg` pkgs * CI: more complex matrix * CI: hardcode the python version to 3.10 in the base conda env * CI: check bluesky logs after execution * CI: use 2025-1.0 conda envs (fixed pyolog / configparser) * CI: do not start docker systemd unit - it should be running already * CI: move some configuration into scripts * CI: start docker-based services earlier * CI: update EPICS auto env var * CI: check if EPICS vars got propagated * CI: add env var for `DISPLAY` as well * CI: run source command on every step involving EPICS * CI: do not add EPICS env vars to GITHUB_ENV --- .ci/bl-specific.sh | 19 ++++- .ci/drop-in.py | 10 ++- .ci/kafka-config.sh | 17 ++++ .ci/tiled-config.sh | 2 + .github/workflows/testing.yml | 141 +++++++++++++++++++++++++--------- 5 files changed, 151 insertions(+), 38 deletions(-) mode change 100644 => 100755 .ci/bl-specific.sh create mode 100755 .ci/kafka-config.sh diff --git a/.ci/bl-specific.sh b/.ci/bl-specific.sh old mode 100644 new mode 100755 index f00b1ca..71fe435 --- a/.ci/bl-specific.sh +++ b/.ci/bl-specific.sh @@ -1,3 +1,20 @@ #!/bin/bash -# cp -v <...> ~/.ipython/profile_${TEST_PROFILE}/ +set -veo pipefail + +# Per https://stackoverflow.com/a/66982842: +sudo echo "127.0.0.1 info.cms.nsls2.bnl.gov" | sudo tee -a /etc/hosts +cat /etc/hosts + +ping -c 5 info.cms.nsls2.bnl.gov +echo "" | telnet info.cms.nsls2.bnl.gov 6379 + +# Copy config files into the dummy IPython profile: +mkdir -v -p ~/.ipython/profile_test/startup/ +cp -v startup/.cms_config ~/.ipython/profile_test/startup/ + +# Create pilatus and /nsls2 dir trees: +sudo mkdir -v -p /nsls2/ +sudo chown -R -v $USER: /nsls2/ +mkdir -v -p $HOME/cms-epics-containers/pilatus-data/data/ +ln -sv $HOME/cms-epics-containers/pilatus-data/data/ /nsls2/ diff --git a/.ci/drop-in.py b/.ci/drop-in.py index d26eb14..06f6a2d 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -1,3 +1,5 @@ +import numpy as np + caput(beam.mono_bragg_pv, 1.03953) pilatus2M.tiff.create_directory.set(-20) sam = SampleGISAXS('test') @@ -5,4 +7,10 @@ pilatus2M.cam.num_images.put(1) -RE(bp.count([pilatus2M], num=3)) +uid, = RE(bp.count([pilatus2M], num=3)) + +hdr = db[uid] +print(hdr.table(fill=True)) + +data = np.array(list(hdr.data("pilatus2M_image"))) +print(f"{data = }\n{data.shape = }") diff --git a/.ci/kafka-config.sh b/.ci/kafka-config.sh new file mode 100755 index 0000000..baf11fb --- /dev/null +++ b/.ci/kafka-config.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -veo pipefail + +# Kafka config: +cat << EOF > kafka.yml +--- +abort_run_on_kafka_exception: false +bootstrap_servers: + - localhost:9092 +runengine_producer_config: + security.protocol: PLAINTEXT +EOF + +mkdir -v -p $HOME/.config/bluesky/ +mv -v kafka.yml $HOME/.config/bluesky/kafka.yml +cat $HOME/.config/bluesky/kafka.yml diff --git a/.ci/tiled-config.sh b/.ci/tiled-config.sh index 9277949..21f27bf 100755 --- a/.ci/tiled-config.sh +++ b/.ci/tiled-config.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -veo pipefail + # Tiled profile config: tiled_profiles_dir="$HOME/.config/tiled/profiles/" diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a41e6b3..06f83de 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -14,17 +14,24 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - conda-env-version: ["2024-3.0"] - zenodo-id: ["14019710"] - md5-checksum: ["0ec1899a4f5989292e07380f96b97498"] - python-version: ["3.10"] # , "3.11", "3.12"] + zenodo-deposition: + # https://zenodo.org/records/14862443 + - env-version: "2025-1.0" + zenodo-id: "14862443" + artifact: + - python-version: "3.10" + md5-checksum: "d031ed24ad7beebb836f93712d8def2e" + - python-version: "3.11" + md5-checksum: "3b1074c0827ee3baf0cdcd64917b9afa" + - python-version: "3.12" + md5-checksum: "624239ed0846ef95d8334ea100581b83" fail-fast: false env: TZ: America/New_York defaults: run: - shell: bash -l {0} + shell: bash -leo pipefail {0} steps: - name: Set env vars @@ -32,63 +39,109 @@ jobs: export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} # just the repo, as opposed to org/repo echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV - export PYTHONVER=$(echo ${{ matrix.python-version }} | sed 's/\.//g') + export PYTHONVER=$(echo ${{ matrix.artifact.python-version }} | sed 's/\.//g') echo "PYTHONVER=${PYTHONVER}" >> $GITHUB_ENV - export CONDA_ENV_NAME="${{ matrix.conda-env-version }}-py${PYTHONVER}-tiled" + export CONDA_ENV_NAME="${{ matrix.zenodo-deposition.env-version }}-py${PYTHONVER}-tiled" echo "CONDA_ENV_NAME=${CONDA_ENV_NAME}" >> $GITHUB_ENV + export BLUESKY_KAFKA_CONFIG_PATH="$HOME/.config/bluesky/kafka.yml" + echo "BLUESKY_KAFKA_CONFIG_PATH=${BLUESKY_KAFKA_CONFIG_PATH}" >> $GITHUB_ENV + + - name: Install system packages + run: | + sudo apt-get update + sudo apt-get install -y xorg openbox + + - name: Check Docker version + run: | + docker version + docker compose --version + + - name: Start Redis + uses: supercharge/redis-github-action@1.0.0 + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.6.0 + - name: Checkout the code uses: actions/checkout@v4 - - name: Setup umamba - uses: mamba-org/setup-micromamba@v1 + - name: Configure epics-containers + run: | + # git clone --depth 1 --branch CMS-IOCs https://github.com/NSLS2/cms-epics-containers.git $HOME/cms-epics-containers + git clone --depth 1 --branch ci-gha-profile https://github.com/mrakitin/epics-containers-example-services.git $HOME/cms-epics-containers + + # source $HOME/cms-epics-containers/environment.sh + # echo "EPICS_CA_ADDR_LIST=${EPICS_CA_ADDR_LIST}" >> $GITHUB_ENV + + - name: Prepare Tiled and Kafka configs + run: | + .ci/tiled-config.sh + .ci/kafka-config.sh + + - name: Run beamline-specific setup + run: | + .ci/bl-specific.sh + + - name: Start epics-containers + run: | + cd $HOME/cms-epics-containers + source $HOME/cms-epics-containers/environment.sh + docker compose up -d + sleep 20 - - name: Setup umamba + - name: Check Docker state run: | - set -vxeo pipefail - url="https://zenodo.org/record/${{ matrix.zenodo-id }}/files/${CONDA_ENV_NAME}.tar.gz?download=1" + docker images + docker ps -a + docker network ls + docker volume ls + + - name: Check Docker logs + run: | + cd $HOME/cms-epics-containers + docker compose logs + + - uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: ${{ env.REPOSITORY_NAME }} + auto-update-conda: true + miniforge-version: latest + python-version: "3.10" + conda-remove-defaults: true + + - name: Create a conda env from Zenodo + run: | + url="https://zenodo.org/record/${{ matrix.zenodo-deposition.zenodo-id }}/files/${CONDA_ENV_NAME}.tar.gz?download=1" wget --progress=dot:giga ${url} -O ${CONDA_ENV_NAME}.tar.gz status=$? if [ $status -gt 0 ]; then echo "Cannot download from ${url}. Exit code: ${status}" exit $status fi - echo "${{ matrix.md5-checksum }} ${CONDA_ENV_NAME}.tar.gz" > checksum.txt + echo "${{ matrix.artifact.md5-checksum }} ${CONDA_ENV_NAME}.tar.gz" > checksum.txt md5sum --check checksum.txt mkdir -p $HOME/${CONDA_ENV_NAME} + echo "Unarchiving the tarball..." tar -xf ${CONDA_ENV_NAME}.tar.gz -C $HOME/${CONDA_ENV_NAME} conda activate $HOME/${CONDA_ENV_NAME} - conda unpack - - - name: Start MongoDB - uses: supercharge/mongodb-github-action@1.6.0 - - - name: Prepare databroker config - run: | - set -vxeuo pipefail - mkdir -v -p $HOME/.config/databroker/ - cp -v configs/databroker/local.yml $HOME/.config/databroker/ - - - name: Start epics-containers - run: | - set -vxeuo pipefail - git clone --depth 1 --branch CMS-IOCs https://github.com/NSLS2/cms-epics-containers.git ~/cms-epics-containers - source ~/cms-epics-containers/environment.sh - cd ~/cms-epics-containers - docker-compose up -d - sleep 20 + conda-unpack - name: Start caproto IOC run: | - set -vxeuo pipefail - source ~/cms-epics-containers/environment.sh - echo "\n" | python iocs/spoof_beamline.py --list-pvs + source $HOME/cms-epics-containers/environment.sh + conda activate $HOME/${CONDA_ENV_NAME} + echo "\n" | python iocs/spoof_beamline.py --list-pvs & sleep 20 - name: Test the code run: | - set -vxeuo pipefail + # Start Xvfb + # (from https://developercommunity.visualstudio.com/content/problem/336288/headless-testing-using-xvfb-on-hosted-ubuntu-1604.html) + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 + export DISPLAY=:99 + # This is what IPython does internally to load the startup files: command=" import os @@ -105,4 +158,20 @@ jobs: print(f'Executing {f} in CI') ip.parent._exec_file(f)" + conda activate $HOME/${CONDA_ENV_NAME} + + source $HOME/cms-epics-containers/environment.sh + env | grep EPICS ipython --profile=test -c "$command" + + - name: Show bluesky logs + if: always() + run: | + cat $HOME/.cache/bluesky/log/bluesky.log + + - name: Check dir tree + if: always() + run: | + ls -laF /nsls2/ + tree -a $HOME/cms-epics-containers/pilatus-data/ + tree -a /nsls2/data/ From 85a445661115753890783ec76ed9b3eaf1df038d Mon Sep 17 00:00:00 2001 From: Noah van der Vleuten <54477889+NoahVl@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:07:38 -0500 Subject: [PATCH 12/18] Add scan_id key to RE to prevent KeyError (#2) * Add scan_id key to RE to prevent KeyError * Change scan_id to 1 --- .ci/drop-in.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci/drop-in.py b/.ci/drop-in.py index 06f6a2d..655b676 100644 --- a/.ci/drop-in.py +++ b/.ci/drop-in.py @@ -5,6 +5,8 @@ sam = SampleGISAXS('test') detselect(pilatus2M) +RE.md.update({'scan_id': 1}) + pilatus2M.cam.num_images.put(1) uid, = RE(bp.count([pilatus2M], num=3)) From 2bf8800d0afe288a3b23b84cb5b577bd67e46dd2 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 11 Mar 2025 16:51:02 -0400 Subject: [PATCH 13/18] Remove breaking changes to profile --- user_TSAXSWAXS.py => .ci/user_TSAXSWAXS.py | 0 startup/20-area-detectors.py | 20 ++++++++++---------- startup/81-beam.py | 2 +- startup/94-sample.py | 2 -- 4 files changed, 11 insertions(+), 13 deletions(-) rename user_TSAXSWAXS.py => .ci/user_TSAXSWAXS.py (100%) diff --git a/user_TSAXSWAXS.py b/.ci/user_TSAXSWAXS.py similarity index 100% rename from user_TSAXSWAXS.py rename to .ci/user_TSAXSWAXS.py diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index 495f177..a4b6a67 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -506,16 +506,16 @@ def stage(self): # for cam_number, fs in zip([1,2,3,4], [fs1, fs2, fs3, fs4]): - # for cam_number, fs in zip([2, 3, 4], [fs2, fs3, fs4]): - # G, port_dict = fs.get_asyn_digraph() - # cam = port_dict["cam{:02}".format(cam_number)] - # for v in port_dict.values(): - # try: - # if v.nd_array_port.get() == "CAM": - # v.nd_array_port.set("cam{:02}".format(cam_number)) - # except AttributeError: - # pass - # fs.validate_asyn_ports() + for cam_number, fs in zip([2, 3, 4], [fs2, fs3, fs4]): + G, port_dict = fs.get_asyn_digraph() + cam = port_dict["cam{:02}".format(cam_number)] + for v in port_dict.values(): + try: + if v.nd_array_port.get() == "CAM": + v.nd_array_port.set("cam{:02}".format(cam_number)) + except AttributeError: + pass + fs.validate_asyn_ports() diff --git a/startup/81-beam.py b/startup/81-beam.py index 9aa3651..e6430f8 100644 --- a/startup/81-beam.py +++ b/startup/81-beam.py @@ -1097,7 +1097,7 @@ def wavelength(self, verbosity=3): # (planck constant * speed of light)/(electronic charge) - energy_eV = self.hc_over_e / (wavelength_m + 1e-8) + energy_eV = self.hc_over_e / wavelength_m energy_keV = energy_eV / 1000.0 if verbosity >= 3: diff --git a/startup/94-sample.py b/startup/94-sample.py index 90d3f74..40b53df 100644 --- a/startup/94-sample.py +++ b/startup/94-sample.py @@ -2123,8 +2123,6 @@ def _expose_test(self, exposure_time=None, extra=None, handlefile=True, verbosit status *= 0 elif detector.name is "pilatus2M": if caget("XF:11BMB-ES{Det:PIL2M}:cam1:Acquire") == 1: - print('pilatus2M is done') - print(status) status *= 0 elif detector.name is "pilatus800": if caget("XF:11BMB-ES{Det:PIL800K}:cam1:Acquire") == 1: From 60e3c971e417a5057faad3a828055f41a880845d Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 18 Mar 2025 17:20:48 -0400 Subject: [PATCH 14/18] WIP: fixing ports --- iocs/spoof_beamline.py | 14 +++++++++----- startup/20-area-detectors.py | 22 +++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index 6461089..1f1b469 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -99,12 +99,16 @@ def fabricate_channel(self, key): if pattern.search(key): return ChannelString(value=val) elif 'ArrayPort' in key: - return ChannelString(value="PIL") + return ChannelString(value="cam1") elif 'PortName' in key: - port_name = key.split(':')[-2] - if port_name == "cam1": - port_name = "PIL" - return ChannelString(value=port_name) + # Extract port name from key format: :PortName + # Use regex to find the last component before :PortName + match = re.search(r"[^:}_\-]+(?=:PortName)", key) + if match: + port_name = match.group(0) + return ChannelString(value=port_name) + # Fallback if regex doesn't match + return ChannelString(value=key) elif 'name' in key.lower(): return ChannelString(value=key) elif 'EnableCallbacks' in key: diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index a4b6a67..e4f5e23 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -505,23 +505,27 @@ def stage(self): all_standard_pros = [fs2, fs3, fs4] - # for cam_number, fs in zip([1,2,3,4], [fs1, fs2, fs3, fs4]): - for cam_number, fs in zip([2, 3, 4], [fs2, fs3, fs4]): + for fs in all_standard_pros: G, port_dict = fs.get_asyn_digraph() - cam = port_dict["cam{:02}".format(cam_number)] + print("\nArea Detector Port Graph:") + print("------------------------") + print("Nodes:") + for node in sorted(G.nodes()): + print(f" • {node}") + print("\nConnections:") + for edge in sorted(G.edges()): + print(f" • {edge[0]} → {edge[1]}") + print("------------------------\n") + + cam = port_dict["cam1"] for v in port_dict.values(): try: if v.nd_array_port.get() == "CAM": - v.nd_array_port.set("cam{:02}".format(cam_number)) + v.nd_array_port.set("cam1") except AttributeError: pass fs.validate_asyn_ports() - - - - - # class StandardsimDetectorV33(SingleTriggerV33, ProsilicaDetector): ## tiff = Cpt(TIFFPluginWithFileStore, ## suffix='TIFF1:', From e8b93a6506da63bb1dbef16d1a0839a60125845f Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 21 Mar 2025 14:33:51 -0400 Subject: [PATCH 15/18] Working version for CMS sim --- startup/20-area-detectors.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index e4f5e23..d84c5ce 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -29,6 +29,18 @@ from ophyd.areadetector.plugins import HDF5Plugin #,register_plugin,PluginBase +def print_area_detector_port_graph(G): + print("\nArea Detector Port Graph:") + print("------------------------") + print("Nodes:") + for node in sorted(G.nodes()): + print(f" • {node}") + print("\nConnections:") + for edge in sorted(G.edges()): + print(f" • {edge[0]} → {edge[1]}") + print("------------------------\n") + + print(f'Loading {__file__}') # import filestore.api as fs @@ -507,16 +519,7 @@ def stage(self): for fs in all_standard_pros: G, port_dict = fs.get_asyn_digraph() - print("\nArea Detector Port Graph:") - print("------------------------") - print("Nodes:") - for node in sorted(G.nodes()): - print(f" • {node}") - print("\nConnections:") - for edge in sorted(G.edges()): - print(f" • {edge[0]} → {edge[1]}") - print("------------------------\n") - + #print_area_detector_port_graph(G) cam = port_dict["cam1"] for v in port_dict.values(): try: From aae502a0e01a09f3ae513929ce2f83aec4d84627 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 24 Mar 2025 16:39:21 -0400 Subject: [PATCH 16/18] Autosettings for AD plugin graph --- .ci/apply-autosettings.sh | 16 ++++++++ .ci/auto_settings.sav | 77 ++++++++++++++++++++++++++++++++++++ startup/20-area-detectors.py | 10 +++-- 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100755 .ci/apply-autosettings.sh create mode 100644 .ci/auto_settings.sav diff --git a/.ci/apply-autosettings.sh b/.ci/apply-autosettings.sh new file mode 100755 index 0000000..a75f0b3 --- /dev/null +++ b/.ci/apply-autosettings.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Check if the file exists +if [ ! -f ".ci/auto_settings.sav" ]; then + echo "Error: auto_settings.sav file not found" + exit 1 +fi + +# Read the file line by line +while IFS= read -r line; do + # Skip empty lines + if [ -n "$line" ]; then + # Execute caput command with the line contents as arguments + caput $line + fi +done < ".ci/auto_settings.sav" diff --git a/.ci/auto_settings.sav b/.ci/auto_settings.sav new file mode 100644 index 0000000..e92055c --- /dev/null +++ b/.ci/auto_settings.sav @@ -0,0 +1,77 @@ +XF:11BMA-BI{FS:1-Cam:1}cam1:PortName cam01 +XF:11BMA-BI{FS:2-Cam:1}cam1:PortName cam02 +XF:11BMB-BI{FS:3-Cam:1}cam1:PortName cam03 +XF:11BMB-BI{FS:4-Cam:1}cam1:PortName cam04 +XF:11BMB-BI{OnAxis-Cam:1}cam1:PortName cam05 +XF:11BMA-BI{FS:2-Cam:1}Proc1:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Proc1:PortName Proc1 +XF:11BMA-BI{FS:2-Cam:1}ROI1:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}ROI1:PortName ROI1 +XF:11BMA-BI{FS:2-Cam:1}ROI2:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}ROI2:PortName ROI2 +XF:11BMA-BI{FS:2-Cam:1}ROI3:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}ROI3:PortName ROI3 +XF:11BMA-BI{FS:2-Cam:1}ROI4:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}ROI4:PortName ROI4 +XF:11BMA-BI{FS:2-Cam:1}Stats1:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Stats1:PortName Stats1 +XF:11BMA-BI{FS:2-Cam:1}Stats2:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Stats2:PortName Stats2 +XF:11BMA-BI{FS:2-Cam:1}Stats3:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Stats3:PortName Stats3 +XF:11BMA-BI{FS:2-Cam:1}Stats4:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Stats4:PortName Stats4 +XF:11BMA-BI{FS:2-Cam:1}Stats5:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Stats5:PortName Stats5 +XF:11BMA-BI{FS:2-Cam:1}Trans1:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}Trans1:PortName Trans1 +XF:11BMA-BI{FS:2-Cam:1}image1:NDArrayPort cam02 +XF:11BMA-BI{FS:2-Cam:1}image1:PortName image1 +XF:11BMB-BI{FS:3-Cam:1}Proc1:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Proc1:PortName Proc1 +XF:11BMB-BI{FS:3-Cam:1}ROI1:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}ROI1:PortName ROI1 +XF:11BMB-BI{FS:3-Cam:1}ROI2:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}ROI2:PortName ROI2 +XF:11BMB-BI{FS:3-Cam:1}ROI3:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}ROI3:PortName ROI3 +XF:11BMB-BI{FS:3-Cam:1}ROI4:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}ROI4:PortName ROI4 +XF:11BMB-BI{FS:3-Cam:1}Stats1:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Stats1:PortName Stats1 +XF:11BMB-BI{FS:3-Cam:1}Stats2:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Stats2:PortName Stats2 +XF:11BMB-BI{FS:3-Cam:1}Stats3:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Stats3:PortName Stats3 +XF:11BMB-BI{FS:3-Cam:1}Stats4:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Stats4:PortName Stats4 +XF:11BMB-BI{FS:3-Cam:1}Stats5:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Stats5:PortName Stats5 +XF:11BMB-BI{FS:3-Cam:1}Trans1:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}Trans1:PortName Trans1 +XF:11BMB-BI{FS:3-Cam:1}image1:NDArrayPort cam03 +XF:11BMB-BI{FS:3-Cam:1}image1:PortName image1 +XF:11BMB-BI{FS:4-Cam:1}Proc1:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Proc1:PortName Proc1 +XF:11BMB-BI{FS:4-Cam:1}ROI1:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}ROI1:PortName ROI1 +XF:11BMB-BI{FS:4-Cam:1}ROI2:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}ROI2:PortName ROI2 +XF:11BMB-BI{FS:4-Cam:1}ROI3:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}ROI3:PortName ROI3 +XF:11BMB-BI{FS:4-Cam:1}ROI4:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}ROI4:PortName ROI4 +XF:11BMB-BI{FS:4-Cam:1}Stats1:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Stats1:PortName Stats1 +XF:11BMB-BI{FS:4-Cam:1}Stats2:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Stats2:PortName Stats2 +XF:11BMB-BI{FS:4-Cam:1}Stats3:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Stats3:PortName Stats3 +XF:11BMB-BI{FS:4-Cam:1}Stats4:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Stats4:PortName Stats4 +XF:11BMB-BI{FS:4-Cam:1}Stats5:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Stats5:PortName Stats5 +XF:11BMB-BI{FS:4-Cam:1}Trans1:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}Trans1:PortName Trans1 +XF:11BMB-BI{FS:4-Cam:1}image1:NDArrayPort cam04 +XF:11BMB-BI{FS:4-Cam:1}image1:PortName image1 diff --git a/startup/20-area-detectors.py b/startup/20-area-detectors.py index d84c5ce..31afe22 100644 --- a/startup/20-area-detectors.py +++ b/startup/20-area-detectors.py @@ -517,14 +517,16 @@ def stage(self): all_standard_pros = [fs2, fs3, fs4] - for fs in all_standard_pros: + for cam_number, fs in zip([2, 3, 4], all_standard_pros): G, port_dict = fs.get_asyn_digraph() - #print_area_detector_port_graph(G) - cam = port_dict["cam1"] + print(fs.cam.port_name.get()) + print_area_detector_port_graph(G) + print(port_dict) + cam = port_dict["cam{:02}".format(cam_number)] for v in port_dict.values(): try: if v.nd_array_port.get() == "CAM": - v.nd_array_port.set("cam1") + v.nd_array_port.set("cam{:02}".format(cam_number)) except AttributeError: pass fs.validate_asyn_ports() From 3475bccadf46ad51d9ca4f3582dfbe3ee1f3ea50 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 24 Mar 2025 16:40:58 -0400 Subject: [PATCH 17/18] Added auto settings to workflow --- .github/workflows/testing.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 06f83de..c055e39 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -135,6 +135,12 @@ jobs: echo "\n" | python iocs/spoof_beamline.py --list-pvs & sleep 20 + - name: Apply auto settings + run: | + echo "::group::Applying auto settings" + .ci/apply-autosettings.sh + echo "::endgroup::" + - name: Test the code run: | # Start Xvfb From 995dddebbe85b6a1e3f930e9f7450c740375cb2d Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 28 Mar 2025 16:49:09 -0400 Subject: [PATCH 18/18] Don't assume array ports and port names --- iocs/spoof_beamline.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/iocs/spoof_beamline.py b/iocs/spoof_beamline.py index 1f1b469..406f60a 100644 --- a/iocs/spoof_beamline.py +++ b/iocs/spoof_beamline.py @@ -99,15 +99,8 @@ def fabricate_channel(self, key): if pattern.search(key): return ChannelString(value=val) elif 'ArrayPort' in key: - return ChannelString(value="cam1") + return ChannelString(value=key) elif 'PortName' in key: - # Extract port name from key format: :PortName - # Use regex to find the last component before :PortName - match = re.search(r"[^:}_\-]+(?=:PortName)", key) - if match: - port_name = match.group(0) - return ChannelString(value=port_name) - # Fallback if regex doesn't match return ChannelString(value=key) elif 'name' in key.lower(): return ChannelString(value=key)