diff --git a/queue-server/startup_bl531/00_base.py b/queue-server/startup_bl531/00_base.py index 6691648..bec17b2 100644 --- a/queue-server/startup_bl531/00_base.py +++ b/queue-server/startup_bl531/00_base.py @@ -36,7 +36,23 @@ from bluesky.callbacks.best_effort import BestEffortCallback +from bluesky.callbacks.tiled_writer import TiledWriter +from tiled.server import SimpleTiledServer +from tiled.client import from_uri +#load the api key from env var +import os +api_key = os.getenv("TILED_SINGLE_USER_API_KEY") +if not api_key: + raise ValueError("TILED_SINGLE_USER_API_KEY environment variable is not set.") + +# Initialize the Tiled server and client +tiled_client = from_uri("http://127.0.0.1:8000", api_key=api_key) +tw = TiledWriter(tiled_client) +RE.subscribe(tw) + bec = BestEffortCallback() +bec.disable_plots() + # Send all metadata/data captured to the BestEffortCallback. RE.subscribe(bec) \ No newline at end of file diff --git a/queue-server/startup_bl531/01_devices.py b/queue-server/startup_bl531/01_devices.py index 036f451..7906461 100644 --- a/queue-server/startup_bl531/01_devices.py +++ b/queue-server/startup_bl531/01_devices.py @@ -1,4 +1,4 @@ -from ophyd import Component, Device, EpicsSignal, EpicsSignalRO +from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsMotor #This method of grouping devices should be used for the final production """ class M101(Device): m101_pitch_mm = Component(EpicsSignalRO, 'm101_pitch_mm') @@ -9,20 +9,26 @@ #Temporary: Manually add each device as a signal -m101_pitch = EpicsSignal("bl531_esp300:m101_pitch_mm", name="m101_pitch") -m101_bend = EpicsSignal("bl531_esp300:m101_bend_um", name="m101_bend") +# m101_pitch = EpicsSignal("bl531_esp300:m101_pitch_mm", name="m101_pitch") +# m101_bend = EpicsSignal("bl531_esp300:m101_bend_um", name="m101_bend") -dcm_angle = EpicsSignal("bl531_xps1:mono_angle_deg", name="dcm_angle") -dcm_height = EpicsSignal("bl531_xps1:mono_height_mm", name="dcm_height") +# dcm_angle = EpicsSignal("bl531_xps1:mono_angle_deg", name="dcm_angle") +# dcm_height = EpicsSignal("bl531_xps1:mono_height_mm", name="dcm_height") -beamstop_horiz = EpicsSignal("bl531_xps2:beamstop_x_mm", name="beamstop_horizon") -beamstop_vert = EpicsSignal("bl531_xps2:beamstop_y_mm", name="beamstop_vert") +beamstop_diode = EpicsSignal("bl201-beamstop:current", name="beamstop_diode") -endstation_slit_inboard = EpicsSignal("DMC02:E", name="endstation_slit_inboard") -endstation_slit_outboard = EpicsSignal("DMC02:F", name="endstation_slit_outboard") -endstation_slit_top = EpicsSignal("DMC02:G", name="endstation_slit_top") -endstation_slit_bottom = EpicsSignal("DMC02:H", name="endstation_slit_bottom") -harm_slit_inboard = EpicsSignal("DMC01:A", name="harm_slit_inboard") -harm_slit_outboard = EpicsSignal("DMC01:B", name="harm_slit_outboard") -harm_slit_top = EpicsSignal("DMC01:C", name="harm_slit_top") -harm_slit_bottom = EpicsSignal("DMC01:D", name="harm_slit_bottom") +beamstop_horiz = EpicsMotor("bl531_xps2:beamstop_x_mm", name="beamstop_horizon") +beamstop_vert = EpicsMotor("bl531_xps2:beamstop_y_mm", name="beamstop_vert") + +sampleholder_x = EpicsMotor("bl531_xps2:sample_x_mm", name="sampleholder_x") +sampleholder_y = EpicsMotor("bl531_xps2:sample_y_mm", name="sampleholder_y") + + +# endstation_slit_inboard = EpicsSignal("DMC02:E", name="endstation_slit_inboard") +# endstation_slit_outboard = EpicsSignal("DMC02:F", name="endstation_slit_outboard") +# endstation_slit_top = EpicsSignal("DMC02:G", name="endstation_slit_top") +# endstation_slit_bottom = EpicsSignal("DMC02:H", name="endstation_slit_bottom") +# harm_slit_inboard = EpicsSignal("DMC01:A", name="harm_slit_inboard") +# harm_slit_outboard = EpicsSignal("DMC01:B", name="harm_slit_outboard") +# harm_slit_top = EpicsSignal("DMC01:C", name="harm_slit_top") +# harm_slit_bottom = EpicsSignal("DMC01:D", name="harm_slit_bottom") diff --git a/queue-server/startup_bl531/02_detectors.py b/queue-server/startup_bl531/02_detectors.py new file mode 100644 index 0000000..38fa47b --- /dev/null +++ b/queue-server/startup_bl531/02_detectors.py @@ -0,0 +1,35 @@ +# # Connect to Pilatus detector +#Leave this commented out for now, there's some issue with the detector and we don't want this affecting anything +# import ophyd +# import os +# import numpy as np +# from ophyd import ADComponent +# from ophyd import ImagePlugin +# from ophyd import PilatusDetector +# from ophyd import SingleTrigger +# from ophyd.areadetector.filestore_mixins import FileStoreHDF5IterativeWrite +# from ophyd.areadetector.plugins import HDF5Plugin_V34 +# from ophyd import EpicsSignalRO + +# # File path configuration +# PILATUS_FILES_ROOT = "/mnt/data531" +# BLUESKY_FILES_ROOT = "/mnt/data531" +# TEST_IMAGE_DIR = "test/pilatus/%Y/%m/%d/" + +# # Custom HDF5 plugin with file store integration +# class MyHDF5Plugin(FileStoreHDF5IterativeWrite, HDF5Plugin_V34): +# pass + +# # Custom Pilatus detector class +# class MyPilatusDetector(SingleTrigger, PilatusDetector): +# """Pilatus detector with HDF5 file writing capability""" +# image = ADComponent(ImagePlugin, "image1:") +# hdf1 = ADComponent( +# MyHDF5Plugin, +# "HDF1:", +# write_path_template=os.path.join(PILATUS_FILES_ROOT, TEST_IMAGE_DIR), +# read_path_template=os.path.join(BLUESKY_FILES_ROOT, TEST_IMAGE_DIR), +# ) + +# # Create detector instance +# det = MyPilatusDetector("13PIL1:", name="det") diff --git a/queue-server/startup_bl531/15_plans.py b/queue-server/startup_bl531/15_plans.py index 465e3ea..a827cd7 100644 --- a/queue-server/startup_bl531/15_plans.py +++ b/queue-server/startup_bl531/15_plans.py @@ -36,6 +36,87 @@ x2x_scan as _x2x_scan, ) +# 2D grid scan for spectroscopy +@parameter_annotation_decorator({ + "description": "Scan over a 2d grid to perform spectroscopy", + "parameters": { + "detectors": { + "description": "Required. List of detectors", + "annotation": "typing.List[str]", + "convert_device_names": True, + + }, + "motor1": { + "description": "Required. First inidividual motor that is moved between the start and stop positions.", + "annotation": "typing.Any", + "convert_device_names": True, + + }, + "motor1_start": { + "description": "Required. The start position for motor #1, uses the default units of the motor", + "default": 0.0, + "min": 0, + "max": 20, + "step": 0.1, + + }, + "motor1_stop": { + "description": "Required. The stop position for motor #1, uses the default units of the motor", + "default": 20.0, + "min": 0, + "max": 20, + "step": 0.1, + + }, + "motor1_num": { + "description": "Required. The number of points that motor #1 will stop at between the start and stop.", + "default": 10, + "min": 0, + "max": 30, + "step": 1, + + }, + "motor2": { + "description": "Required. Second inidividual motor that is moved between the start and stop positions.", + "annotation": "typing.Any", + "convert_device_names": True, + + }, + "motor2_start": { + "description": "Required. The start position for motor #2, uses the default units of the motor", + "default": 0.0, + "min": 0, + "max": 20, + "step": 0.1, + + }, + "motor2_stop": { + "description": "Required. The stop position for motor #2, uses the default units of the motor", + "default": 20.0, + "min": 0, + "max": 20, + "step": 0.1, + + }, + "motor2_num": { + "description": "Required. The number of points that motor #2 will stop at between the start and stop.", + "default": 10, + "min": 0, + "max": 30, + "step": 1, + + }, + "snake_axes": { + "description": "Optional boolean. Should the motors follow a snake pattern when moving through the selected locations? Default=True", + "annotation": "bool", + "default": True, + }, + } +}) +def grid_scan(detectors, motor1, motor2, motor1_start:float=0.0, motor2_start:float=0.0, motor1_stop:float=20.0, motor2_stop:float=20.0, motor1_num:int=10, motor2_num:int=10, snake_axes:bool=True, *, md:dict=None): + + yield from _grid_scan(detectors, motor1, motor1_start, motor1_stop, motor1_num, motor2, motor2_start, motor2_stop, motor2_num, snake_axes, md=md) + # 1D scan for endstation x, z or filters @parameter_annotation_decorator({ "description": "Scan over one multi-motor trajectory.", diff --git a/queue-server/startup_bl531/existing_plans_and_devices.yaml b/queue-server/startup_bl531/existing_plans_and_devices.yaml new file mode 100644 index 0000000..a28d4cd --- /dev/null +++ b/queue-server/startup_bl531/existing_plans_and_devices.yaml @@ -0,0 +1,2433 @@ +# This file is automatically generated. Edit at your own risk. +existing_devices: + beamstop_diode: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + beamstop_horiz: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor + beamstop_vert: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor + sampleholder_x: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor + sampleholder_y: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor +existing_plans: + _adaptive_scan: + description: Scan over one variable with adaptively tuned step size. + module: bluesky.plans + name: adaptive_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: target_field + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: starting position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: ending position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: float + description: smallest step for fast-changing regions + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: min_step + - annotation: + type: float + description: largest step for slow-chaning regions + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: max_step + - annotation: + type: float + description: desired fractional change in detector signal between steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: target_delta + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some + motors + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: backstep + - annotation: + type: typing.Optional[float] + default: '0.8' + description: threshold for going backward and rescanning a region, default is + 0.8 + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: threshold + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _fly: + description: Perform a fly scan with one or more 'flyers'. + module: bluesky.plans + name: fly + parameters: + - annotation: + type: list[__FLYABLE__] + convert_device_names: true + description: objects that support the flyer interface + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: flyers + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _grid_scan: + description: Scan over a mesh; each motor is on an independent trajectory. + module: bluesky.plans + name: grid_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: typing.Union[collections.abc.Iterable, bool, NoneType] + default: None + description: 'which axes should be snaked, either ``False`` (do not snake any + axes), + + ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis + + is defined as following snake-like, winding trajectory instead of a + + simple left-to-right trajectory. The elements of the list are motors + + that are listed in `args`. The list must not contain the slowest + + (first) motor, since it can''t be snaked.' + kind: + name: KEYWORD_ONLY + value: 3 + name: snake_axes + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _inner_product_scan: + module: bluesky.plans + name: inner_product_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: int + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _list_grid_scan: + description: Scan over a mesh; each motor is on an independent trajectory. + module: bluesky.plans + name: list_grid_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "patterned like (``motor1, position_list1,``\n ``motor2,\ + \ position_list2,``\n ``motor3, position_list3,``\n \ + \ ``...,``\n ``motorN, position_listN``)\n\nThe first\ + \ motor is the \"slowest\", the outer loop. ``position_list``'s\nare lists\ + \ of positions, all lists must have the same length. Motors\ncan be any 'settable'\ + \ object (motor, temp controller, etc.)." + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: bool + default: 'False' + description: 'which axes should be snaked, either ``False`` (do not snake any + axes), + + ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis + + is defined as following snake-like, winding trajectory instead of a + + simple left-to-right trajectory.The elements of the list are motors + + that are listed in `args`. The list must not contain the slowest + + (first) motor, since it can''t be snaked.' + kind: + name: KEYWORD_ONLY + value: 3 + name: snake_axes + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _list_scan: + description: Scan over one or more variables in steps simultaneously (inner product). + module: bluesky.plans + name: list_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: tuple[typing.Union[__MOVABLE__, typing.Any], list[typing.Any]] + convert_device_names: true + description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ + \n.. code-block:: python\n\n motor1, [point1, point2, ...],\n motor2,\ + \ [point1, point2, ...],\n ...,\n motorN, [point1, point2, ...]\n\n\ + Motors can be any 'settable' object (motor, temp controller, etc.)" + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - default: None + description: 'hook for customizing action of inner loop (messages per step) + + Expected signature: + + ``f(detectors, motor, step) -> plan (a generator)``' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _log_scan: + description: Scan over one variable in log-spaced steps. + module: bluesky.plans + name: log_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: starting position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: ending position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: int + description: number of steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - default: None + description: 'hook for customizing action of inner loop (messages per step) + + Expected signature: ``f(detectors, motor, step)``' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _ramp_plan: + description: Take data while ramping one or more positioners. + module: bluesky.plans + name: ramp_plan + parameters: + - description: 'plan to start the ramp. This will be run inside of a open/close + + run. + + + This plan must return a `ophyd.StatusBase` object.' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: go_plan + - annotation: + type: __READABLE__ + convert_device_names: true + description: signal to be monitored + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: monitor_sig + - description: 'generator which takes no input + + + This will be called for every data point. This should create + + one or more events.' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: inner_plan_func + - annotation: + type: bool + default: 'True' + description: If True, add a pre data at beginning + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: take_pre_data + - annotation: + type: typing.Optional[float] + default: None + description: 'If not None, the maximum time the ramp can run. + + + In seconds' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: timeout + - annotation: + type: typing.Optional[float] + default: None + description: 'If not None, take data no faster than this. If None, take + + data as fast as possible + + + If running the inner plan takes longer than `period` than take + + data with no dead time. + + + In seconds.' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: period + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: md + properties: + is_generator: true + _rel_adaptive_scan: + description: Relative scan over one variable with adaptively tuned step size. + module: bluesky.plans + name: rel_adaptive_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: target_field + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: starting position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: ending position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: float + description: smallest step for fast-changing regions + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: min_step + - annotation: + type: float + description: largest step for slow-chaning regions + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: max_step + - annotation: + type: float + description: desired fractional change in detector signal between steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: target_delta + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some + motors + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: backstep + - annotation: + type: typing.Optional[float] + default: '0.8' + description: threshold for going backward and rescanning a region, default is + 0.8 + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: threshold + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_grid_scan: + description: Scan over a mesh relative to current position. + module: bluesky.plans + name: rel_grid_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: typing.Union[collections.abc.Iterable, bool, NoneType] + default: None + description: 'which axes should be snaked, either ``False`` (do not snake any + axes), + + ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis + + is defined as following snake-like, winding trajectory instead of a + + simple left-to-right trajectory. The elements of the list are motors + + that are listed in `args`. The list must not contain the slowest + + (first) motor, since it can''t be snaked.' + kind: + name: KEYWORD_ONLY + value: 3 + name: snake_axes + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_list_grid_scan: + description: 'Scan over a mesh; each motor is on an independent trajectory. Each + point is + + relative to the current position.' + module: bluesky.plans + name: rel_list_grid_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "patterned like (``motor1, position_list1,``\n ``motor2,\ + \ position_list2,``\n ``motor3, position_list3,``\n \ + \ ``...,``\n ``motorN, position_listN``)\n\nThe first\ + \ motor is the \"slowest\", the outer loop. ``position_list``'s\nare lists\ + \ of positions, all lists must have the same length. Motors\ncan be any 'settable'\ + \ object (motor, temp controller, etc.)." + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: bool + default: 'False' + description: 'which axes should be snaked, either ``False`` (do not snake any + axes), + + ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis + + is defined as following snake-like, winding trajectory instead of a + + simple left-to-right trajectory.The elements of the list are motors + + that are listed in `args`. The list must not contain the slowest + + (first) motor, since it can''t be snaked.' + kind: + name: KEYWORD_ONLY + value: 3 + name: snake_axes + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_list_scan: + description: Scan over one variable in steps relative to current position. + module: bluesky.plans + name: rel_list_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ + \n.. code-block:: python\n\n motor1, [point1, point2, ...],\n motor2,\ + \ [point1, point2, ...],\n ...,\n motorN, [point1, point2, ...]\n\n\ + Motors can be any 'settable' object (motor, temp controller, etc.)\npoint1,\ + \ point2 etc are relative to the current location." + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - default: None + description: 'hook for customizing action of inner loop (messages per step) + + Expected signature: ``f(detectors, motor, step)``' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_log_scan: + description: Scan over one variable in log-spaced steps relative to current position. + module: bluesky.plans + name: rel_log_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: starting position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: ending position of motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: int + description: number of steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - default: None + description: 'hook for customizing action of inner loop (messages per step) + + Expected signature: ``f(detectors, motor, step)``' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_scan: + description: Scan over one multi-motor trajectory relative to current position. + module: bluesky.plans + name: rel_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ + \ code-block:: python\n\n motor1, start1, stop1,\n motor2, start2, start2,\n\ + \ ...,\n motorN, startN, stopN,\n\nMotors can be any 'settable' object\ + \ (motor, temp controller, etc.)" + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - default: None + description: number of points + kind: + name: KEYWORD_ONLY + value: 3 + name: num + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_spiral: + description: Relative spiral scan + module: bluesky.plans + name: rel_spiral + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: dr + - annotation: + type: float + description: Number of theta steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: nth + - annotation: + type: typing.Optional[float] + default: None + description: 'Delta radius along the major axis of the ellipse. If None, it + + defaults to dr.' + kind: + name: KEYWORD_ONLY + value: 3 + name: dr_y + - annotation: + type: float + default: '0.0' + description: Tilt angle in radians, default 0.0 + kind: + name: KEYWORD_ONLY + value: 3 + name: tilt + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_spiral_fermat: + description: Relative fermat spiral scan + module: bluesky.plans + name: rel_spiral_fermat + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: delta radius + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: dr + - annotation: + type: float + description: radius gets divided by this + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: factor + - annotation: + type: typing.Optional[float] + default: None + description: 'Delta radius along the major axis of the ellipse, if not specifed + + defaults to dr' + kind: + name: KEYWORD_ONLY + value: 3 + name: dr_y + - annotation: + type: typing.Optional[float] + default: '0.0' + description: Tilt angle in radians, default 0.0 + kind: + name: KEYWORD_ONLY + value: 3 + name: tilt + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _rel_spiral_square: + description: Relative square spiral scan, centered around current (x, y) position. + module: bluesky.plans + name: rel_spiral_square + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: number of x axis points + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_num + - annotation: + type: float + description: Number of y axis points. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_num + - default: None + description: 'hook for cutomizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plans.one_nd_step` (the default) for + + details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _relative_inner_product_scan: + module: bluesky.plans + name: relative_inner_product_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: int + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _scan: + description: Scan over one multi-motor trajectory. + module: bluesky.plans + name: scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ + \ code-block:: python\n\n motor1, start1, stop1,\n motor2, start2, stop2,\n\ + \ ...,\n motorN, startN, stopN\n\nMotors can be any 'settable' object\ + \ (motor, temp controller, etc.)" + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: typing.Optional[int] + default: None + description: number of points + kind: + name: KEYWORD_ONLY + value: 3 + name: num + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _scan_nd: + description: Scan over an arbitrary N-dimensional trajectory. + module: bluesky.plans + name: scan_nd + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: cycler.Cycler object mapping movable interfaces to positions + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: cycler + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _spiral: + description: Spiral scan, centered around (x_start, y_start) + module: bluesky.plans + name: spiral + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_start + - annotation: + type: float + description: y center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_start + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: dr + - annotation: + type: float + description: Number of theta steps + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: nth + - annotation: + type: typing.Optional[float] + default: None + description: 'Delta radius along the major axis of the ellipse. If None, defaults + to + + dr.' + kind: + name: KEYWORD_ONLY + value: 3 + name: dr_y + - annotation: + type: typing.Optional[float] + default: '0.0' + description: Tilt angle in radians, default 0.0 + kind: + name: KEYWORD_ONLY + value: 3 + name: tilt + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _spiral_fermat: + description: Absolute fermat spiral scan, centered around (x_start, y_start) + module: bluesky.plans + name: spiral_fermat + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_start + - annotation: + type: float + description: y center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_start + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: delta radius + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: dr + - annotation: + type: float + description: radius gets divided by this + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: factor + - annotation: + type: typing.Optional[float] + default: None + description: 'Delta radius along the major axis of the ellipse, if not specifed + + defaults to dr.' + kind: + name: KEYWORD_ONLY + value: 3 + name: dr_y + - annotation: + type: typing.Optional[float] + default: '0.0' + description: Tilt angle in radians, default 0.0 + kind: + name: KEYWORD_ONLY + value: 3 + name: tilt + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _spiral_square: + description: Absolute square spiral scan, centered around (x_center, y_center) + module: bluesky.plans + name: spiral_square + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_motor + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_motor + - annotation: + type: float + description: x center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_center + - annotation: + type: float + description: y center + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_center + - annotation: + type: float + description: x width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_range + - annotation: + type: float + description: y width of spiral + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_range + - annotation: + type: float + description: number of x axis points + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: x_num + - annotation: + type: float + description: Number of y axis points. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: y_num + - default: None + description: 'hook for cutomizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plans.one_nd_step` (the default) for + + details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _tune_centroid: + description: 'plan: tune a motor to the centroid of signal(motor)' + module: bluesky.plans + name: tune_centroid + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: str + description: detector field whose output is to maximize + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: signal + - description: any 'settable' object (motor, temp controller, etc.) + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: start of range + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: 'end of range, note: start < stop' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: float + description: smallest step size to use. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: min_step + - annotation: + type: int + default: '10' + description: number of points with each traversal, default = 10 + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - annotation: + type: float + default: '3.0' + description: 'used in calculating new range after each pass + + + note: step_factor > 1.0, default = 3' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: step_factor + - annotation: + type: bool + default: 'False' + description: if False (default), always scan from start to stop + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: snake + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _tweak: + description: Move and motor and read a detector with an interactive prompt. + module: bluesky.plans + name: tweak + parameters: + - annotation: + type: __READABLE__ + convert_device_names: true + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detector + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: target_field + - kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + description: initial suggestion for step size + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + _x2x_scan: + description: Relatively scan over two motors in a 2:1 ratio + module: bluesky.plans + name: x2x_scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - description: The second motor will move half as much as the first + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor1 + - description: The second motor will move half as much as the first + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor2 + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor + + will move between ``start / 2`` and ``stop / 2``' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor + + will move between ``start / 2`` and ``stop / 2``' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: stop + - annotation: + type: int + description: number of steps in the scan + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - default: None + description: 'hook for cutomizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + count: + description: Take one or more readings from detectors. + module: bluesky.plans + name: count + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Optional[int] + default: '1' + description: 'number of readings to take; default is 1 + + + If None, capture data until canceled' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num + - annotation: + type: typing.Union[float, collections.abc.Iterable[float]] + default: '0.0' + description: Time delay in seconds between successive readings; default is 0. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: delay + - default: None + description: "hook for customizing action of inner loop (messages per step)\n\ + Expected signature ::\n\n def f(detectors: Iterable[OphydObj]) -> Generator[Msg]:\n\ + \ ..." + kind: + name: KEYWORD_ONLY + value: 3 + name: per_shot + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None + description: metadata + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + grid_scan: + description: Scan over a 2d grid to perform spectroscopy + module: __main__ + name: grid_scan + parameters: + - annotation: + type: typing.List[str] + convert_device_names: true + description: Required. List of detectors + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Any + convert_device_names: true + description: Required. First inidividual motor that is moved between the start + and stop positions. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor1 + - annotation: + type: typing.Any + convert_device_names: true + description: Required. Second inidividual motor that is moved between the start + and stop positions. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor2 + - annotation: + type: float + default: '0.0' + default_defined_in_decorator: true + description: 'Required. The start position for motor #1, uses the default units + of the motor' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '20' + min: '0' + name: motor1_start + step: '0.1' + - annotation: + type: float + default: '0.0' + default_defined_in_decorator: true + description: 'Required. The start position for motor #2, uses the default units + of the motor' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '20' + min: '0' + name: motor2_start + step: '0.1' + - annotation: + type: float + default: '20.0' + default_defined_in_decorator: true + description: 'Required. The stop position for motor #1, uses the default units + of the motor' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '20' + min: '0' + name: motor1_stop + step: '0.1' + - annotation: + type: float + default: '20.0' + default_defined_in_decorator: true + description: 'Required. The stop position for motor #2, uses the default units + of the motor' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '20' + min: '0' + name: motor2_stop + step: '0.1' + - annotation: + type: int + default: '10' + default_defined_in_decorator: true + description: 'Required. The number of points that motor #1 will stop at between + the start and stop.' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '30' + min: '0' + name: motor1_num + step: '1' + - annotation: + type: int + default: '10' + default_defined_in_decorator: true + description: 'Required. The number of points that motor #2 will stop at between + the start and stop.' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '30' + min: '0' + name: motor2_num + step: '1' + - annotation: + type: bool + default: 'True' + default_defined_in_decorator: true + description: Optional boolean. Should the motors follow a snake pattern when + moving through the selected locations? Default=True + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: snake_axes + - annotation: + type: dict + default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + scan: + description: Scan over one multi-motor trajectory. + module: __main__ + name: scan + parameters: + - annotation: + type: typing.List[str] + convert_device_names: true + description: Required. List of detectors + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Any + convert_device_names: true + description: Required. Inidividual motor that is moved between the start and + stop positions. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - annotation: + type: float + default: '0.0' + default_defined_in_decorator: true + description: Required. The start position for the motor, uses the default units + of the motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '4000' + min: '-4000' + name: start + step: '0.1' + - annotation: + type: float + default: '0.0' + default_defined_in_decorator: true + description: Required. The stop position for the motor, uses the default units + of the motor + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '4000' + min: '-4000' + name: stop + step: '0.1' + - annotation: + type: int + default: '10' + default_defined_in_decorator: true + description: Required. The number of points that motor will stop at between + the start and stop. + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + max: '200' + min: '0' + name: num + step: '1' + - annotation: + type: dict + default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true diff --git a/queue-server/startup_blank/01_devices.py b/queue-server/startup_blank/01_devices.py deleted file mode 100644 index f760a99..0000000 --- a/queue-server/startup_blank/01_devices.py +++ /dev/null @@ -1,22 +0,0 @@ -from ophyd import Component, Device, EpicsSignal, EpicsSignalRO - -""" class SimulatedMotor(Device): - Offset = Component(EpicsSignal, 'Offset') - Resolution = Component(EpicsSignal, 'Resolution') - Direction = Component(EpicsSignal, 'Direction') - -sim_motor_m2 = SimulatedMotor('IOC:m2', name='sim_motor_m2') - - - -class SimulatedMotor(Device): - m1 = Component(EpicsSignal, 'm1') - -sim_m1 = SimulatedMotor('IOC:', name='sim_m1') """ - -#Devices can be added as shown -#sim_m1 = EpicsSignal("IOC:m1", name="sim_m1") -#sim_m2 = EpicsSignal("IOC:m2", name="sim_m2") -#sim_m3 = EpicsSignal("IOC:m3", name="sim_m3") -#sim_m4 = EpicsSignal("IOC:m4", name="sim_m4") - diff --git a/queue-server/startup_blank/15_plans.py b/queue-server/startup_blank/15_plans.py deleted file mode 100644 index a9f54a5..0000000 --- a/queue-server/startup_blank/15_plans.py +++ /dev/null @@ -1,84 +0,0 @@ -# flake8: noqa -print(f"Loading file {__file__!r}") - -from typing import Any, Dict, List, Optional - - -from bluesky_queueserver.manager.annotation_decorator import parameter_annotation_decorator - -from bluesky.plans import ( - adaptive_scan as _adaptive_scan, - count, - fly as _fly, - grid_scan as _grid_scan, - inner_product_scan as _inner_product_scan, - list_grid_scan as _list_grid_scan, - list_scan as _list_scan, - log_scan as _log_scan, - ramp_plan as _ramp_plan, - rel_adaptive_scan as _rel_adaptive_scan, - rel_grid_scan as _rel_grid_scan, - rel_list_grid_scan as _rel_list_grid_scan, - rel_list_scan as _rel_list_scan, - rel_log_scan as _rel_log_scan, - rel_scan as _rel_scan, - rel_spiral as _rel_spiral, - rel_spiral_fermat as _rel_spiral_fermat, - rel_spiral_square as _rel_spiral_square, - relative_inner_product_scan as _relative_inner_product_scan, - scan as _scan, - scan_nd as _scan_nd, - spiral as _spiral, - spiral_fermat as _spiral_fermat, - spiral_square as _spiral_square, - tune_centroid as _tune_centroid, - tweak as _tweak, - x2x_scan as _x2x_scan, -) - -# 1D scan for endstation x, z or filters -@parameter_annotation_decorator({ - "description": "Scan over one multi-motor trajectory.", - "parameters": { - "detectors": { - "description": "Required. List of detectors", - "annotation": "typing.List[str]", - "convert_device_names": True, - - }, - "motor": { - "description": "Required. Inidividual motor that is moved between the start and stop positions.", - "annotation": "typing.Any", - "convert_device_names": True, - - }, - "start": { - "description": "Required. The start position for the motor, uses the default units of the motor", - "default": 0.0, - "min": -4000, - "max": 4000, - "step": 0.1, - - }, - "stop": { - "description": "Required. The stop position for the motor, uses the default units of the motor", - "default": 0.0, - "min": -4000, - "max": 4000, - "step": 0.1, - - }, - "num": { - "description": "Required. The number of points that motor will stop at between the start and stop.", - "default": 10, - "min": 0, - "max": 200, - "step": 1, - - }, - } -}) -def scan(detectors, motor, start:float=0.0, stop:float=0.0, num:int=10, *, md:dict=None): - - yield from _scan(detectors, motor, start, stop, num,md=md) - diff --git a/queue-server/startup_blank/00_base.py b/queue-server/startup_bolt/00_base.py similarity index 71% rename from queue-server/startup_blank/00_base.py rename to queue-server/startup_bolt/00_base.py index f2c6bfe..38f84f3 100644 --- a/queue-server/startup_blank/00_base.py +++ b/queue-server/startup_bolt/00_base.py @@ -16,9 +16,21 @@ """ from bluesky import RunEngine - +import os RE = RunEngine({}) +from tiled.client import from_uri +from bluesky.callbacks.tiled_writer import TiledWriter +tiled_uri = os.getenv("TILED_URI", "http://localhost:8000") +tiled_api_key = os.getenv("TILED_API_KEY", "ca6ae384c9f944e1465176b7e7274046b710dc7e2703dc33369f7c900d69bd64") + +tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + +# TiledWriter needs a specific container, not the root client +tw = TiledWriter(tiled_client) +RE.subscribe(tw) + + from databroker.v2 import temp db = temp() diff --git a/queue-server/startup_bolt/01_devices.py b/queue-server/startup_bolt/01_devices.py new file mode 100644 index 0000000..1c05857 --- /dev/null +++ b/queue-server/startup_bolt/01_devices.py @@ -0,0 +1,53 @@ +from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsMotor +from ophyd.areadetector.plugins import PluginBase +from ophyd.areadetector import AreaDetector, ADComponent, ImagePlugin, JPEGPlugin, TIFFPlugin +from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.trigger_mixins import SingleTrigger + + + +# Custom PVA Plugin for Area Detector +class PvaPlugin(PluginBase): + _suffix = 'Pva1:' + _plugin_type = 'NDPluginPva' + _default_read_attrs = ['enable'] + _default_configuration_attrs = ['enable'] + array_callbacks = ADComponent(EpicsSignal, 'ArrayCallbacks') + +# Motors +rotation_motor = EpicsMotor("DMC01:A", name="rotation_motor") +linear_stage = EpicsMotor("DMC01:D", name="linear_stage") + +# Area Detector Camera (matching your existing setup) +class MyCamera(AreaDetector): + cam = ADComponent(AreaDetectorCam, 'cam1:') + image = ADComponent(ImagePlugin, 'image1:') + tiff = ADComponent(TIFFPlugin, 'TIFF1:') + pva = ADComponent(PvaPlugin, 'Pva1:') + +# Instantiate the camera +camera = MyCamera('13ARV1:', name='camera') + +# Configure camera staging signals (from your script) +camera.stage_sigs[camera.cam.acquire] = 0 + +# IMAGE OPTIONS +camera.stage_sigs[camera.image.enable] = 1 +camera.stage_sigs[camera.image.queue_size] = 2000 + +# TIFF OPTIONS +camera.stage_sigs[camera.tiff.enable] = 1 +camera.stage_sigs[camera.tiff.auto_save] = 1 +camera.stage_sigs[camera.tiff.file_write_mode] = 0 +camera.stage_sigs[camera.tiff.nd_array_port] = 'SP1' +camera.stage_sigs[camera.tiff.auto_increment] = 1 + +# PVA OPTIONS +camera.stage_sigs[camera.pva.enable] = 1 +camera.stage_sigs[camera.pva.blocking_callbacks] = 'No' +camera.stage_sigs[camera.pva.queue_size] = 2000 +camera.stage_sigs[camera.pva.nd_array_port] = 'SP1' +camera.stage_sigs[camera.pva.array_callbacks] = 0 + +# Simple signals for backwards compatibility +acquire_signal = EpicsSignal('13ARV1:cam1:Acquire', name='acquire_signal') diff --git a/queue-server/startup_bolt/15_plans.py b/queue-server/startup_bolt/15_plans.py new file mode 100644 index 0000000..11b0413 --- /dev/null +++ b/queue-server/startup_bolt/15_plans.py @@ -0,0 +1,266 @@ +# flake8: noqa +print(f"Loading file {__file__!r}") + +from tkinter import S +from typing import Any, Dict, List, Optional +from ophyd import EpicsSignal +from bluesky_queueserver.manager.annotation_decorator import parameter_annotation_decorator +from datetime import datetime + +from bluesky.plans import ( + adaptive_scan as _adaptive_scan, + count, + fly as _fly, + grid_scan as _grid_scan, + inner_product_scan as _inner_product_scan, + list_grid_scan as _list_grid_scan, + list_scan as _list_scan, + log_scan as _log_scan, + ramp_plan as _ramp_plan, + rel_adaptive_scan as _rel_adaptive_scan, + rel_grid_scan as _rel_grid_scan, + rel_list_grid_scan as _rel_list_grid_scan, + rel_list_scan as _rel_list_scan, + rel_log_scan as _rel_log_scan, + rel_scan as _rel_scan, + rel_spiral as _rel_spiral, + rel_spiral_fermat as _rel_spiral_fermat, + rel_spiral_square as _rel_spiral_square, + relative_inner_product_scan as _relative_inner_product_scan, + scan as _scan, + scan_nd as _scan_nd, + spiral as _spiral, + spiral_fermat as _spiral_fermat, + spiral_square as _spiral_square, + tune_centroid as _tune_centroid, + tweak as _tweak, + x2x_scan as _x2x_scan, +) + +# Import mv and rd inside functions to hide them from queue server detection +# 1D scan for endstation x, z or filters +@parameter_annotation_decorator({ + "description": "Scan over one multi-motor trajectory.", + "parameters": { + "detectors": { + "description": "Required. List of detectors", + "annotation": "typing.List[str]", + "convert_device_names": True, + + }, + "motor": { + "description": "Required. Inidividual motor that is moved between the start and stop positions.", + "annotation": "typing.Any", + "convert_device_names": True, + + }, + "start": { + "description": "Required. The start position for the motor, uses the default units of the motor", + "default": 0.0, + "min": -4000, + "max": 4000, + "step": 0.1, + + }, + "stop": { + "description": "Required. The stop position for the motor, uses the default units of the motor", + "default": 0.0, + "min": -4000, + "max": 4000, + "step": 0.1, + + }, + "num": { + "description": "Required. The number of points that motor will stop at between the start and stop.", + "default": 10, + "min": 0, + "max": 200, + "step": 1, + + }, + } +}) +def scan(detectors, motor, start:float=0.0, stop:float=0.0, num:int=10, *, md:dict=None): + + yield from _scan(detectors, motor, start, stop, num,md=md) + +# Move motor certian amount +@parameter_annotation_decorator({ + "description": "Move motor to specified position", + "parameters": { + "motor": { + "description": "Required. Inidividual motor that is moved to desired position", + "annotation": "typing.Any", + "convert_device_names": True, + + }, + "position": { + "description": "Required. The position for the motor, uses the default units of the motor", + "default": 0.0, + "min": -4000, + "max": 4000, + "step": 20, + + } + } +}) +def move_motor(motor, position=0.0, *, md=None): + from bluesky import plan_stubs as bps + import math + # Convert position to float to handle string inputs + position_move = float(int(position) / 2.8125) + + yield from bps.mv(motor, position_move) + + reading = yield from bps.read(motor) + + position_match = math.isclose(reading[motor.name]["value"], position_move, rel_tol=1e-3) + md = { + "run_result": "success" if position_match else "failure" + } + yield from bps.open_run(md=md) + yield from bps.create() + yield from bps.save() + yield from bps.close_run() # Close the run + + +# Measure current motor position + +@parameter_annotation_decorator({ + "description": "Read motor angle and store in Tiled using TiledWriter", + "parameters": { + "motor": { + "description": "Required. Motor device to read", + "annotation": "typing.Any", + "convert_device_names": True, + } + } +}) +def get_angle(motor, *, md=None): + from bluesky import plan_stubs as bps + reading = yield from bps.read(motor) + md = { + "motor_angle": reading[motor.name]["value"], + "motor_name": motor.name, + "angle_degrees": reading[motor.name]["value"] * 2.8125, + "timestamp": datetime.now().isoformat() + } + + yield from bps.open_run(md=md) + yield from bps.create() + yield from bps.save() + yield from bps.close_run() # Close the run + + # Return the reading data + return reading + + + + + + +# Simple image acquisition using TiledWriter (recommended approach) +@parameter_annotation_decorator({ + "description": "Capture single image and store directly in Tiled using TiledWriter", + "parameters": { + "camera": { + "description": "Required. Area Detector camera device", + "annotation": "typing.Any", + "convert_device_names": True, + }, + "motor": { + "description": "Required. Motor device to read", + "annotation": "typing.Any", + "convert_device_names": True, + } + } +}) +def camera_acquire(camera, motor, *, md=None): + import subprocess + from bluesky import plan_stubs as bps + reading = yield from get_angle(motor) + angle_value = float(reading[motor.name]["value"]) + + angle = f"{(angle_value*2.8125):.2f}" + run_id = yield from bps.open_run(md=md) + run_id = run_id + "_" + cmd = ["python", "take_measurement.py", angle, run_id] + #cmd = ["pwd"] + result = subprocess.run(cmd, capture_output=True, text=True) + print(result.stdout) + print(result.stderr) + print(result.returncode) + + yield from bps.close_run() + + return "image_captured" + + +@parameter_annotation_decorator({ + "description": "Perform rotation scan with camera acquisition, for testing", + "parameters": { + "start_angle": { + "description": "Starting angle in degrees", + "annotation": "str" + }, + "end_angle": { + "description": "Ending angle in degrees", + "annotation": "str" + }, + "num_points": { + "description": "Number of measurement points", + "annotation": "str" + }, + "save_dir": { + "description": "Directory to save the images", + "annotation": "str" + } + } +}) +def rotation_scan(start_angle="0", end_angle="90", num_points="10", save_dir="default", *, md=None): + import subprocess + yield from bps.open_run(md=md) + cmd = ["python", "run_photogrammetry_scan.py", str(start_angle), str(end_angle), str(num_points), save_dir] + result = subprocess.run(cmd, capture_output=True, text=True) + print(result.stdout) + print(result.stderr) + print(result.returncode) + + yield from bps.close_run() + + return "scan_completed" + +@parameter_annotation_decorator({ + "description": "Perform reconstruction algorithm, on a given set of images", + "parameters": { + "image_dir": { + "description": "Directory to save the images", + "annotation": "str" + } + } +}) +def reconstruct_object(image_dir="default", *, md=None): + import subprocess + yield from bps.open_run(md=md) + cmd = ["python", "reconstruction.py", image_dir] + result = subprocess.run(cmd, capture_output=True, text=True) + print(result.stdout) + print(result.stderr) + print(result.returncode) + + yield from bps.close_run() + + return "Reconstruction completed" + +def analyze_ply_quality(image_dir="default", *, md=None): + import subprocess + yield from bps.open_run(md=md) + cmd = ["echo", "Analyzing ply quality"] + result = subprocess.run(cmd, capture_output=True, text=True) + print(result.stdout) + print(result.stderr) + print(result.returncode) + + yield from bps.close_run() + + return "Reconstruction completed" \ No newline at end of file diff --git a/queue-server/startup_blank/99_custom.py b/queue-server/startup_bolt/99_custom.py similarity index 100% rename from queue-server/startup_blank/99_custom.py rename to queue-server/startup_bolt/99_custom.py diff --git a/queue-server/startup_blank/existing_plans_and_devices.yaml b/queue-server/startup_bolt/existing_plans_and_devices.yaml similarity index 71% rename from queue-server/startup_blank/existing_plans_and_devices.yaml rename to queue-server/startup_bolt/existing_plans_and_devices.yaml index 35f9085..7d70772 100644 --- a/queue-server/startup_blank/existing_plans_and_devices.yaml +++ b/queue-server/startup_bolt/existing_plans_and_devices.yaml @@ -19,12 +19,24 @@ existing_devices: is_movable: false is_readable: true module: ophyd.sim + acquire_signal: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal bool_sig: classname: Signal is_flyable: false is_movable: true is_readable: true module: ophyd.signal + camera: + classname: MyCamera + is_flyable: false + is_movable: false + is_readable: true + module: __main__ custom_test_device: classname: Device is_flyable: false @@ -524,35 +536,240 @@ existing_devices: is_movable: true is_readable: true module: ophyd.sim + linear_stage: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor motor: - classname: SynAxis + classname: EpicsMotor components: acceleration: - classname: Signal + classname: EpicsSignal is_flyable: false is_movable: true is_readable: true module: ophyd.signal - readback: - classname: _ReadbackSignal + direction_of_travel: + classname: EpicsSignal is_flyable: false is_movable: true is_readable: true - module: ophyd.sim - setpoint: - classname: _SetpointSignal + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO is_flyable: false is_movable: true is_readable: true - module: ophyd.sim - unused: - classname: Signal + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal is_flyable: false is_movable: true is_readable: true module: ophyd.signal velocity: - classname: Signal + classname: EpicsSignal is_flyable: false is_movable: true is_readable: true @@ -560,7 +777,7 @@ existing_devices: is_flyable: false is_movable: true is_readable: true - module: ophyd.sim + module: ophyd.epics_motor motor1: classname: SynAxis components: @@ -1050,6 +1267,127 @@ existing_devices: is_movable: true is_readable: true module: ophyd.sim + rotation_motor: + classname: EpicsMotor + components: + acceleration: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + direction_of_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + high_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_forward: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + home_reverse: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_switch: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + low_limit_travel: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_done_move: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_egu: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_is_moving: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + motor_stop: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + offset_freeze_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + set_use_switch: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_offset_dir: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_readback: + classname: EpicsSignalRO + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + user_setpoint: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + velocity: + classname: EpicsSignal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.signal + is_flyable: false + is_movable: true + is_readable: true + module: ophyd.epics_motor sig: classname: Signal is_flyable: false @@ -1510,12 +1848,17 @@ existing_plans: module: bluesky.plans name: adaptive_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1525,45 +1868,61 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step for fast-changing regions + - annotation: + type: float + description: smallest step for fast-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - description: largest step for slow-chaning regions + - annotation: + type: float + description: largest step for slow-chaning regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: max_step - - description: desired fractional change in detector signal between steps + - annotation: + type: float + description: desired fractional change in detector signal between steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: target_delta - - description: whether backward steps are allowed -- this is concern with some + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some motors kind: name: POSITIONAL_OR_KEYWORD value: 1 name: backstep - - default: '0.8' + - annotation: + type: typing.Optional[float] + default: '0.8' description: threshold for going backward and rescanning a region, default is 0.8 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: threshold - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1576,12 +1935,17 @@ existing_plans: module: bluesky.plans name: fly parameters: - - description: objects that support the flyer interface + - annotation: + type: list[__FLYABLE__] + convert_device_names: true + description: objects that support the flyer interface kind: name: POSITIONAL_OR_KEYWORD value: 1 name: flyers - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1594,7 +1958,10 @@ existing_plans: module: bluesky.plans name: grid_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1603,7 +1970,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: None + - annotation: + type: typing.Union[collections.abc.Iterable, bool, NoneType] + default: None description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -1630,7 +1999,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1642,15 +2013,23 @@ existing_plans: module: bluesky.plans name: inner_product_scan parameters: - - kind: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - kind: + - annotation: + type: int + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - kind: + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: name: VAR_POSITIONAL value: 2 name: args @@ -1659,7 +2038,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: KEYWORD_ONLY value: 3 @@ -1671,12 +2052,18 @@ existing_plans: module: bluesky.plans name: list_grid_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "patterned like (``motor1, position_list1,``\n ``motor2,\ + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "patterned like (``motor1, position_list1,``\n ``motor2,\ \ position_list2,``\n ``motor3, position_list3,``\n \ \ ``...,``\n ``motorN, position_listN``)\n\nThe first\ \ motor is the \"slowest\", the outer loop. ``position_list``'s\nare lists\ @@ -1686,7 +2073,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: 'False' + - annotation: + type: bool + default: 'False' description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -1713,7 +2102,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1726,12 +2117,18 @@ existing_plans: module: bluesky.plans name: list_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ + - annotation: + type: tuple[typing.Union[__MOVABLE__, typing.Any], list[typing.Any]] + convert_device_names: true + description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ \n.. code-block:: python\n\n motor1, [point1, point2, ...],\n motor2,\ \ [point1, point2, ...],\n ...,\n motorN, [point1, point2, ...]\n\n\ Motors can be any 'settable' object (motor, temp controller, etc.)" @@ -1749,7 +2146,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1762,7 +2161,10 @@ existing_plans: module: bluesky.plans name: log_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1772,17 +2174,23 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: number of steps + - annotation: + type: int + description: number of steps kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1818,7 +2226,11 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: go_plan - - kind: + - annotation: + type: __READABLE__ + convert_device_names: true + description: signal to be monitored + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: monitor_sig @@ -1832,13 +2244,17 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: inner_plan_func - - default: 'True' + - annotation: + type: bool + default: 'True' description: If True, add a pre data at beginning kind: name: POSITIONAL_OR_KEYWORD value: 1 name: take_pre_data - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'If not None, the maximum time the ramp can run. @@ -1847,7 +2263,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: timeout - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'If not None, take data no faster than this. If None, take data as fast as possible @@ -1863,7 +2281,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: period - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1875,60 +2295,84 @@ existing_plans: module: bluesky.plans name: rel_adaptive_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 name: target_field - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step for fast-changing regions + - annotation: + type: float + description: smallest step for fast-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - description: largest step for slow-chaning regions + - annotation: + type: float + description: largest step for slow-chaning regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: max_step - - description: desired fractional change in detector signal between steps + - annotation: + type: float + description: desired fractional change in detector signal between steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: target_delta - - description: whether backward steps are allowed -- this is concern with some + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some motors kind: name: POSITIONAL_OR_KEYWORD value: 1 name: backstep - - default: '0.8' + - annotation: + type: typing.Optional[float] + default: '0.8' description: threshold for going backward and rescanning a region, default is 0.8 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: threshold - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1941,16 +2385,24 @@ existing_plans: module: bluesky.plans name: rel_grid_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - kind: + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: name: VAR_POSITIONAL value: 2 name: args - - default: None + - annotation: + type: typing.Union[collections.abc.Iterable, bool, NoneType] + default: None description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -1977,7 +2429,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1993,12 +2447,18 @@ existing_plans: module: bluesky.plans name: rel_list_grid_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "patterned like (``motor1, position_list1,``\n ``motor2,\ + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "patterned like (``motor1, position_list1,``\n ``motor2,\ \ position_list2,``\n ``motor3, position_list3,``\n \ \ ``...,``\n ``motorN, position_listN``)\n\nThe first\ \ motor is the \"slowest\", the outer loop. ``position_list``'s\nare lists\ @@ -2008,7 +2468,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: 'False' + - annotation: + type: bool + default: 'False' description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -2035,7 +2497,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2048,12 +2512,18 @@ existing_plans: module: bluesky.plans name: rel_list_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, [point1, point2, ....]``.\nIn general:\n\ \n.. code-block:: python\n\n motor1, [point1, point2, ...],\n motor2,\ \ [point1, point2, ...],\n ...,\n motorN, [point1, point2, ...]\n\n\ Motors can be any 'settable' object (motor, temp controller, etc.)\npoint1,\ @@ -2070,7 +2540,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2083,27 +2555,39 @@ existing_plans: module: bluesky.plans name: rel_log_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: number of steps + - annotation: + type: int + description: number of steps kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2116,7 +2600,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2129,12 +2615,18 @@ existing_plans: module: bluesky.plans name: rel_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ \ code-block:: python\n\n motor1, start1, stop1,\n motor2, start2, start2,\n\ \ ...,\n motorN, startN, stopN,\n\nMotors can be any 'settable' object\ \ (motor, temp controller, etc.)" @@ -2158,7 +2650,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2171,41 +2665,60 @@ existing_plans: module: bluesky.plans name: rel_spiral parameters: - - kind: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_motor - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: Delta radius along the minor axis of the ellipse. + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: Number of theta steps + - annotation: + type: float + description: Number of theta steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: nth - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse. If None, it defaults to dr.' @@ -2213,7 +2726,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: float + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2229,7 +2744,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2242,42 +2759,61 @@ existing_plans: module: bluesky.plans name: rel_spiral_fermat parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_motor - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: delta radius + - annotation: + type: float + description: delta radius kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: radius gets divided by this + - annotation: + type: float + description: radius gets divided by this kind: name: POSITIONAL_OR_KEYWORD value: 1 name: factor - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse, if not specifed defaults to dr' @@ -2285,7 +2821,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2301,7 +2839,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2314,37 +2854,54 @@ existing_plans: module: bluesky.plans name: rel_spiral_square parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_motor - - description: any 'settable' object (motor, temp controller, etc.) + - annotation: + type: __MOVABLE__ + convert_device_names: true + description: any 'settable' object (motor, temp controller, etc.) kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: number of x axis points + - annotation: + type: float + description: number of x axis points kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_num - - description: Number of y axis points. + - annotation: + type: float + description: Number of y axis points. kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2359,7 +2916,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2371,15 +2930,23 @@ existing_plans: module: bluesky.plans name: relative_inner_product_scan parameters: - - kind: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - kind: + - annotation: + type: int + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - kind: + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + kind: name: VAR_POSITIONAL value: 2 name: args @@ -2388,7 +2955,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: KEYWORD_ONLY value: 3 @@ -2400,12 +2969,18 @@ existing_plans: module: bluesky.plans name: scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ \ code-block:: python\n\n motor1, start1, stop1,\n motor2, start2, stop2,\n\ \ ...,\n motorN, startN, stopN\n\nMotors can be any 'settable' object\ \ (motor, temp controller, etc.)" @@ -2413,7 +2988,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: None + - annotation: + type: typing.Optional[int] + default: None description: number of points kind: name: KEYWORD_ONLY @@ -2429,7 +3006,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2442,7 +3021,10 @@ existing_plans: module: bluesky.plans name: scan_nd parameters: - - kind: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors @@ -2461,7 +3043,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2474,7 +3058,10 @@ existing_plans: module: bluesky.plans name: spiral parameters: - - kind: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors @@ -2488,37 +3075,51 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_start - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_start - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: Delta radius along the minor axis of the ellipse. + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: Number of theta steps + - annotation: + type: float + description: Number of theta steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: nth - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse. If None, defaults to @@ -2527,7 +3128,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2543,7 +3146,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2556,7 +3161,10 @@ existing_plans: module: bluesky.plans name: spiral_fermat parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2571,37 +3179,51 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_start - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_start - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: delta radius + - annotation: + type: float + description: delta radius kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: radius gets divided by this + - annotation: + type: float + description: radius gets divided by this kind: name: POSITIONAL_OR_KEYWORD value: 1 name: factor - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse, if not specifed defaults to dr.' @@ -2609,7 +3231,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2625,7 +3249,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2638,7 +3264,10 @@ existing_plans: module: bluesky.plans name: spiral_square parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2653,32 +3282,44 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_center - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_center - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: number of x axis points + - annotation: + type: float + description: number of x axis points kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_num - - description: Number of y axis points. + - annotation: + type: float + description: Number of y axis points. kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2693,7 +3334,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2706,12 +3349,17 @@ existing_plans: module: bluesky.plans name: tune_centroid parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: detector field whose output is to maximize + - annotation: + type: str + description: detector field whose output is to maximize kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2721,28 +3369,38 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: start of range + - annotation: + type: float + description: start of range kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: 'end of range, note: start < stop' + - annotation: + type: float + description: 'end of range, note: start < stop' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step size to use. + - annotation: + type: float + description: smallest step size to use. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - default: '10' + - annotation: + type: int + default: '10' description: number of points with each traversal, default = 10 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - default: '3.0' + - annotation: + type: float + default: '3.0' description: 'used in calculating new range after each pass @@ -2751,13 +3409,17 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: step_factor - - default: 'False' + - annotation: + type: bool + default: 'False' description: if False (default), always scan from start to stop kind: name: POSITIONAL_OR_KEYWORD value: 1 name: snake - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2770,11 +3432,16 @@ existing_plans: module: bluesky.plans name: tweak parameters: - - kind: + - annotation: + type: __READABLE__ + convert_device_names: true + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detector - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2783,12 +3450,16 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: initial suggestion for step size + - annotation: + type: float + description: initial suggestion for step size kind: name: POSITIONAL_OR_KEYWORD value: 1 name: step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2801,7 +3472,10 @@ existing_plans: module: bluesky.plans name: x2x_scan parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2816,21 +3490,28 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor2 - - description: 'The relative limits of the first motor. The second motor + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor will move between ``start / 2`` and ``stop / 2``' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: 'The relative limits of the first motor. The second motor + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor will move between ``start / 2`` and ``stop / 2``' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - kind: + - annotation: + type: int + description: number of steps in the scan + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num @@ -2844,7 +3525,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2852,17 +3535,80 @@ existing_plans: name: md properties: is_generator: true + acquire: + module: __main__ + name: acquire + parameters: + - kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: angle + - kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: save_dir + properties: + is_generator: true + analyze_ply_quality: + module: __main__ + name: analyze_ply_quality + parameters: + - default: '''default''' + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: image_dir + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + camera_acquire: + description: Capture single image and store directly in Tiled using TiledWriter + module: __main__ + name: camera_acquire + parameters: + - annotation: + type: typing.Any + convert_device_names: true + description: Required. Area Detector camera device + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: camera + - annotation: + type: typing.Any + convert_device_names: true + description: Required. Motor device to read + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: motor + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true count: description: Take one or more readings from detectors. module: bluesky.plans name: count parameters: - - description: list of 'readable' objects + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects kind: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - default: '1' + - annotation: + type: typing.Optional[int] + default: '1' description: 'number of readings to take; default is 1 @@ -2871,7 +3617,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - default: None + - annotation: + type: typing.Union[float, collections.abc.Iterable[float]] + default: '0.0' description: Time delay in seconds between successive readings; default is 0. kind: name: POSITIONAL_OR_KEYWORD @@ -2885,7 +3633,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_shot - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2893,70 +3643,167 @@ existing_plans: name: md properties: is_generator: true - scan: - description: Scan over one multi-motor trajectory. + get_angle: + description: Read motor angle and store in Tiled using TiledWriter module: __main__ - name: scan + name: get_angle parameters: - annotation: - type: typing.List[str] + type: typing.Any convert_device_names: true - description: Required. List of detectors + description: Required. Motor device to read kind: name: POSITIONAL_OR_KEYWORD value: 1 - name: detectors + name: motor + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + move_motor: + description: Move motor to specified position + module: __main__ + name: move_motor + parameters: - annotation: type: typing.Any convert_device_names: true - description: Required. Inidividual motor that is moved between the start and - stop positions. + description: Required. Inidividual motor that is moved to desired position kind: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - annotation: - type: float - default: '0.0' + - default: '0.0' default_defined_in_decorator: true - description: Required. The start position for the motor, uses the default units - of the motor + description: Required. The position for the motor, uses the default units of + the motor kind: name: POSITIONAL_OR_KEYWORD value: 1 max: '4000' min: '-4000' - name: start - step: '0.1' + name: position + step: '20' + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + reconstruct_object: + description: Perform reconstruction algorithm, on a given set of images + module: __main__ + name: reconstruct_object + parameters: - annotation: - type: float - default: '0.0' - default_defined_in_decorator: true - description: Required. The stop position for the motor, uses the default units - of the motor + type: str + default: '''default''' + description: Directory to save the images kind: name: POSITIONAL_OR_KEYWORD value: 1 - max: '4000' - min: '-4000' - name: stop - step: '0.1' + name: image_dir + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + rotation_scan: + description: Perform rotation scan with camera acquisition, for testing + module: __main__ + name: rotation_scan + parameters: - annotation: - type: int - default: '10' - default_defined_in_decorator: true - description: Required. The number of points that motor will stop at between - the start and stop. + type: str + default: '''0''' + description: Starting angle in degrees + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: start_angle + - annotation: + type: str + default: '''90''' + description: Ending angle in degrees + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: end_angle + - annotation: + type: str + default: '''10''' + description: Number of measurement points + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: num_points + - annotation: + type: str + default: '''default''' + description: Directory to save the images kind: name: POSITIONAL_OR_KEYWORD value: 1 - max: '200' - min: '0' + name: save_dir + - default: None + kind: + name: KEYWORD_ONLY + value: 3 + name: md + properties: + is_generator: true + scan: + description: Scan over one multi-motor trajectory. + module: bluesky.plans + name: scan + parameters: + - annotation: + type: collections.abc.Sequence[__READABLE__] + convert_device_names: true + description: list of 'readable' objects + kind: + name: POSITIONAL_OR_KEYWORD + value: 1 + name: detectors + - annotation: + type: typing.Union[__MOVABLE__, typing.Any] + convert_device_names: true + description: "For one dimension, ``motor, start, stop``.\nIn general:\n\n..\ + \ code-block:: python\n\n motor1, start1, stop1,\n motor2, start2, stop2,\n\ + \ ...,\n motorN, startN, stopN\n\nMotors can be any 'settable' object\ + \ (motor, temp controller, etc.)" + kind: + name: VAR_POSITIONAL + value: 2 + name: args + - annotation: + type: typing.Optional[int] + default: None + description: number of points + kind: + name: KEYWORD_ONLY + value: 3 name: num - step: '1' + - default: None + description: 'hook for customizing action of inner loop (messages per step). + + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + + for details.' + kind: + name: KEYWORD_ONLY + value: 3 + name: per_step - annotation: - type: dict + type: typing.Optional[dict[str, typing.Any]] default: None + description: metadata kind: name: KEYWORD_ONLY value: 3 diff --git a/queue-server/startup_blank/user_group_permissions.yaml b/queue-server/startup_bolt/user_group_permissions.yaml similarity index 100% rename from queue-server/startup_blank/user_group_permissions.yaml rename to queue-server/startup_bolt/user_group_permissions.yaml diff --git a/queue-server/startup_sim/00_base.py b/queue-server/startup_sim/00_base.py index f2c6bfe..6935722 100644 --- a/queue-server/startup_sim/00_base.py +++ b/queue-server/startup_sim/00_base.py @@ -16,6 +16,8 @@ """ from bluesky import RunEngine +from bluesky.callbacks.tiled_writer import TiledWriter +from tiled.client import from_uri RE = RunEngine({}) @@ -35,6 +37,12 @@ # Send all metadata/data captured to the BestEffortCallback. RE.subscribe(bec) + +tiled_client = from_uri("http://127.0.0.1:8000?api_key=830cddd39db2cf102e563364b5bd8a4eed428085d130f0569a84244357e09a89") +tw = TiledWriter(tiled_client) +RE.subscribe(tw) + + # flake8: noqa print(f"Loading file {__file__!r}") diff --git a/queue-server/startup_sim/existing_plans_and_devices.yaml b/queue-server/startup_sim/existing_plans_and_devices.yaml index 527aca9..6e8aed9 100644 --- a/queue-server/startup_sim/existing_plans_and_devices.yaml +++ b/queue-server/startup_sim/existing_plans_and_devices.yaml @@ -1085,7 +1085,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1095,45 +1097,61 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step for fast-changing regions + - annotation: + type: float + description: smallest step for fast-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - description: largest step for slow-chaning regions + - annotation: + type: float + description: largest step for slow-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: max_step - - description: desired fractional change in detector signal between steps + - annotation: + type: float + description: desired fractional change in detector signal between steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: target_delta - - description: whether backward steps are allowed -- this is concern with some + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some motors kind: name: POSITIONAL_OR_KEYWORD value: 1 name: backstep - - default: '0.8' + - annotation: + type: typing.Optional[float] + default: '0.8' description: threshold for going backward and rescanning a region, default is 0.8 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: threshold - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1151,7 +1169,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: flyers - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1200,7 +1220,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1216,7 +1238,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - kind: + - annotation: + type: int + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num @@ -1229,7 +1253,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: KEYWORD_ONLY value: 3 @@ -1256,7 +1282,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: 'False' + - annotation: + type: bool + default: 'False' description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -1283,7 +1311,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1319,7 +1349,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1342,17 +1374,23 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: number of steps + - annotation: + type: int + description: number of steps kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1388,7 +1426,8 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: go_plan - - kind: + - description: signal to be monitored + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: monitor_sig @@ -1402,13 +1441,17 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: inner_plan_func - - default: 'True' + - annotation: + type: bool + default: 'True' description: If True, add a pre data at beginning kind: name: POSITIONAL_OR_KEYWORD value: 1 name: take_pre_data - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'If not None, the maximum time the ramp can run. @@ -1417,7 +1460,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: timeout - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'If not None, take data no faster than this. If None, take data as fast as possible @@ -1433,7 +1478,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: period - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1450,7 +1497,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1460,45 +1509,61 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor, relative to the current position. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor, relative to the current position. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step for fast-changing regions + - annotation: + type: float + description: smallest step for fast-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - description: largest step for slow-chaning regions + - annotation: + type: float + description: largest step for slow-changing regions kind: name: POSITIONAL_OR_KEYWORD value: 1 name: max_step - - description: desired fractional change in detector signal between steps + - annotation: + type: float + description: desired fractional change in detector signal between steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: target_delta - - description: whether backward steps are allowed -- this is concern with some + - annotation: + type: bool + description: whether backward steps are allowed -- this is concern with some motors kind: name: POSITIONAL_OR_KEYWORD value: 1 name: backstep - - default: '0.8' + - annotation: + type: typing.Optional[float] + default: '0.8' description: threshold for going backward and rescanning a region, default is 0.8 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: threshold - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1547,7 +1612,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1578,7 +1645,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: 'False' + - annotation: + type: bool + default: 'False' description: 'which axes should be snaked, either ``False`` (do not snake any axes), @@ -1605,7 +1674,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1640,7 +1711,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1663,17 +1736,23 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: starting position of motor + - annotation: + type: float + description: starting position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: ending position of motor + - annotation: + type: float + description: ending position of motor kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: number of steps + - annotation: + type: int + description: number of steps kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1686,7 +1765,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1728,7 +1809,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1755,27 +1838,37 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: Delta radius along the minor axis of the ellipse. + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: Number of theta steps + - annotation: + type: float + description: Number of theta steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: nth - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse. If None, it defaults to dr.' @@ -1783,7 +1876,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: float + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -1799,7 +1894,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1827,27 +1924,37 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: delta radius + - annotation: + type: float + description: delta radius kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: radius gets divided by this + - annotation: + type: float + description: radius gets divided by this kind: name: POSITIONAL_OR_KEYWORD value: 1 name: factor - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse, if not specifed defaults to dr' @@ -1855,7 +1962,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -1871,7 +1980,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1899,22 +2010,30 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: number of x axis points + - annotation: + type: float + description: number of x axis points kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_num - - description: Number of y axis points. + - annotation: + type: float + description: Number of y axis points. kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -1929,7 +2048,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -1945,7 +2066,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - kind: + - annotation: + type: int + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num @@ -1958,7 +2081,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None kind: name: KEYWORD_ONLY value: 3 @@ -1983,7 +2108,9 @@ existing_plans: name: VAR_POSITIONAL value: 2 name: args - - default: None + - annotation: + type: typing.Optional[int] + default: None description: number of points kind: name: KEYWORD_ONLY @@ -1999,7 +2126,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2031,7 +2160,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2058,37 +2189,51 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_start - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_start - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: Delta radius along the minor axis of the ellipse. + - annotation: + type: float + description: Delta radius along the minor axis of the ellipse. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: Number of theta steps + - annotation: + type: float + description: Number of theta steps kind: name: POSITIONAL_OR_KEYWORD value: 1 name: nth - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse. If None, defaults to @@ -2097,7 +2242,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2113,7 +2260,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2141,37 +2290,51 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_start - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_start - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: delta radius + - annotation: + type: float + description: delta radius kind: name: POSITIONAL_OR_KEYWORD value: 1 name: dr - - description: radius gets divided by this + - annotation: + type: float + description: radius gets divided by this kind: name: POSITIONAL_OR_KEYWORD value: 1 name: factor - - default: None + - annotation: + type: typing.Optional[float] + default: None description: 'Delta radius along the major axis of the ellipse, if not specifed defaults to dr.' @@ -2179,7 +2342,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: dr_y - - default: '0.0' + - annotation: + type: typing.Optional[float] + default: '0.0' description: Tilt angle in radians, default 0.0 kind: name: KEYWORD_ONLY @@ -2195,7 +2360,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2223,32 +2390,44 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: y_motor - - description: x center + - annotation: + type: float + description: x center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_center - - description: y center + - annotation: + type: float + description: y center kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_center - - description: x width of spiral + - annotation: + type: float + description: x width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_range - - description: y width of spiral + - annotation: + type: float + description: y width of spiral kind: name: POSITIONAL_OR_KEYWORD value: 1 name: y_range - - description: number of x axis points + - annotation: + type: float + description: number of x axis points kind: name: POSITIONAL_OR_KEYWORD value: 1 name: x_num - - description: Number of y axis points. + - annotation: + type: float + description: Number of y axis points. kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2263,7 +2442,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2281,7 +2462,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - description: detector field whose output is to maximize + - annotation: + type: str + description: detector field whose output is to maximize kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2291,28 +2474,38 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: start of range + - annotation: + type: float + description: start of range kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: 'end of range, note: start < stop' + - annotation: + type: float + description: 'end of range, note: start < stop' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - description: smallest step size to use. + - annotation: + type: float + description: smallest step size to use. kind: name: POSITIONAL_OR_KEYWORD value: 1 name: min_step - - default: '10' + - annotation: + type: int + default: '10' description: number of points with each traversal, default = 10 kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - default: '3.0' + - annotation: + type: float + default: '3.0' description: 'used in calculating new range after each pass @@ -2321,13 +2514,17 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: step_factor - - default: 'False' + - annotation: + type: bool + default: 'False' description: if False (default), always scan from start to stop kind: name: POSITIONAL_OR_KEYWORD value: 1 name: snake - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2344,7 +2541,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detector - - description: data field whose output is the focus of the adaptive tuning + - annotation: + type: str + description: data field whose output is the focus of the adaptive tuning kind: name: POSITIONAL_OR_KEYWORD value: 1 @@ -2353,12 +2552,16 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor - - description: initial suggestion for step size + - annotation: + type: float + description: initial suggestion for step size kind: name: POSITIONAL_OR_KEYWORD value: 1 name: step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2386,21 +2589,28 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: motor2 - - description: 'The relative limits of the first motor. The second motor + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor will move between ``start / 2`` and ``stop / 2``' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: start - - description: 'The relative limits of the first motor. The second motor + - annotation: + type: float + description: 'The relative limits of the first motor. The second motor will move between ``start / 2`` and ``stop / 2``' kind: name: POSITIONAL_OR_KEYWORD value: 1 name: stop - - kind: + - annotation: + type: int + description: number of steps in the scan + kind: name: POSITIONAL_OR_KEYWORD value: 1 name: num @@ -2414,7 +2624,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_step - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY @@ -2432,7 +2644,9 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: detectors - - default: '1' + - annotation: + type: typing.Optional[int] + default: '1' description: 'number of readings to take; default is 1 @@ -2441,7 +2655,7 @@ existing_plans: name: POSITIONAL_OR_KEYWORD value: 1 name: num - - default: None + - default: '0.0' description: Time delay in seconds between successive readings; default is 0. kind: name: POSITIONAL_OR_KEYWORD @@ -2455,7 +2669,9 @@ existing_plans: name: KEYWORD_ONLY value: 3 name: per_shot - - default: None + - annotation: + type: typing.Optional[dict[str, typing.Any]] + default: None description: metadata kind: name: KEYWORD_ONLY diff --git a/reconstruction.py b/reconstruction.py new file mode 100644 index 0000000..7626481 --- /dev/null +++ b/reconstruction.py @@ -0,0 +1,274 @@ +import shutil, sys, subprocess, os, time +from PIL import Image + +def ensure_directories(*paths: str) -> None: + """ Create directories if they don't exist.""" + for path in paths: + os.makedirs(path, exist_ok=True) + +def feature_extraction(imageDir: str, databasePath: str, colmapPath: str): + """ Run feature extraction. """ + subprocess.run([ + colmapPath, "feature_extractor", + "--database_path", databasePath, + "--image_path", imageDir, + "--SiftExtraction.use_gpu", "0", + "--SiftExtraction.num_threads", "6" # or 1 to be safe + ], check=True) + +def feature_matching(databasePath: str, colmapPath: str): + """ + Run feature matching. + """ + subprocess.run([ + colmapPath, "exhaustive_matcher", + "--database_path", databasePath, + "--SiftMatching.use_gpu", "0", + ], check=True) + +def sparse_reconstruction(imageDir: str, databasePath: str, colmapPath: str, sparseDir: str): + """ + Run sparse reconstruction. + """ + subprocess.run([ + colmapPath, "mapper", + "--database_path", databasePath, + "--image_path", imageDir, + "--output_path", sparseDir + ], check=True) + +def image_undistorter(imageDir: str, denseDir: str, colmapPath: str, sparseDir: str): + """ + Run image undistorter, which outputs acccording + to quality of point detection. + """ + subprocess.run([ + colmapPath, "image_undistorter", + "--image_path", imageDir, + "--input_path", sparseDir, "0", + "--output_path", denseDir, + "--output_type", "COLMAP" + ], check=True) + +def interface_colmap(workspace_dir: str, imageDir: str, basePath: str, sceneMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Run interfaceCOLMAP, which converts gathered data from + colmap and output scene_mvs, containing necessary scene + for next reconstruction steps. + """ + subprocess.run([ + os.path.join(mvs_bin_dir, "InterfaceCOLMAP"), + "-i", os.path.join(workspace_dir, "dense"), #Needed for manual colmap + #"-i", os.path.join(workspace_dir, "dense", "0"), Needed for automaticReconstruction combo + "-o", sceneMVS, + "--image-folder", imageDir + ],cwd=denseDir) + +def densify_point_cloud(sceneMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Densify point cloud connects points by creating more points + in between already created points. """ + subprocess.run([ + os.path.join(mvs_bin_dir, "DensifyPointCloud"), + sceneMVS + ], cwd=denseDir) + +def reconstruct_mesh(denseMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Creates mesh of object, lacking color but connecting points. + """ + subprocess.run([ + os.path.join(mvs_bin_dir, "ReconstructMesh"), + denseMVS + ], cwd=denseDir) + +def texture_mesh(denseDir: str, sceneMVS: str, mvs_bin_dir: str): + """ + Texture mesh mixes data from both sceneMVS and the mesh + + Note: This is time consuming and should be skipped if mesh quality is subpar + """ + + subprocess.run([ + os.path.join(mvs_bin_dir, "TextureMesh"), + sceneMVS, + "-m", "scene_dense_mesh.ply" + ],cwd=denseDir) + +def run_colmap_pipeline(image_dir: str, workspace_dir: str, colmap_path: str = "colmap") -> None: + """ + Full COLMAP pipeline: feature extraction → matching → sparse reconstruction → undistortion. + """ + print(f"Running COLMAP pipeline on {image_dir} -> {workspace_dir}") + + # Validate input directory exists + if not os.path.exists(image_dir): + raise FileNotFoundError(f"Image directory not found: {image_dir}") + + database_path = os.path.join(workspace_dir, "database.db") + sparse_dir = os.path.join(workspace_dir, "sparse") + dense_dir = os.path.join(workspace_dir, "dense") + + ensure_directories(workspace_dir, sparse_dir, dense_dir) + + feature_extraction(image_dir, database_path, colmap_path) + feature_matching(database_path, colmap_path) + sparse_reconstruction(image_dir, database_path, colmap_path, sparse_dir) + image_undistorter(image_dir, dense_dir, colmap_path, os.path.join(sparse_dir, "0")) + + print("COLMAP pipeline completed.") + +def run_openmvs_pipeline(base_path: str, image_dir: str, workspace_dir: str, mvs_bin_dir: str, image_file_name: str) -> None: + """ + Run OpenMVS conversion and mesh reconstruction from COLMAP output. + """ + + dense_dir = os.path.join(workspace_dir, "dense") + scene_mvs = os.path.join(dense_dir, "scene.mvs") + dense_mvs = os.path.join(dense_dir, "scene_dense.mvs") + ensure_directories(dense_dir) + + interface_colmap(workspace_dir, image_dir, base_path, scene_mvs, dense_dir, mvs_bin_dir) + densify_point_cloud(scene_mvs, dense_dir, mvs_bin_dir) + reconstruct_mesh(dense_mvs, dense_dir, mvs_bin_dir) + texture_mesh(dense_dir, scene_mvs, mvs_bin_dir) + + print("OpenMVS pipeline completed.") + +def automatic_reconstruction(image_dir: str, workspace_dir: str, colmap_path: str = "colmap"): + """ + Run COLMAP's automatic reconstruction pipeline. + """ + database_path = os.path.join(workspace_dir, "database_dir") + sparse_dir = os.path.join(workspace_dir, "sparse") + dense_dir = os.path.join(workspace_dir, "dense") + + ensure_directories(workspace_dir, sparse_dir, dense_dir) + + subprocess.run([ + colmap_path, "automatic_reconstructor", + "--workspace_path", workspace_dir, + "--image_path", image_dir, + "--data_type", "individual", # or 'video' depending on your input + "--quality", "medium", # can be 'low', 'medium', or 'high' + "--sparse", "true", + "--dense", "true", + ], check=True) + + print("Automatic reconstruction completed.") + +def check_colmap_availability(): + """Check if COLMAP is available and working.""" + try: + result = subprocess.run(['colmap', 'help'], capture_output=True, text=True, check=True) + print(" COLMAP is available and working") + return True + except subprocess.CalledProcessError as e: + print(f" COLMAP command failed: {e}") + return False + except FileNotFoundError: + print(" COLMAP not found in PATH") + return False + +def check_openmvs_availability(): + """Check if OpenMVS tools are available and working.""" + mvs_bin_path = "/usr/local/bin/OpenMVS/" + required_tools = ["InterfaceCOLMAP", "DensifyPointCloud", "ReconstructMesh", "TextureMesh"] + + if not os.path.exists(mvs_bin_path): + print(f"āŒ OpenMVS directory not found: {mvs_bin_path}") + return False + + missing_tools = [] + for tool in required_tools: + tool_path = os.path.join(mvs_bin_path, tool) + if not os.path.exists(tool_path): + missing_tools.append(tool) + + if missing_tools: + print(f"āŒ Missing OpenMVS tools: {', '.join(missing_tools)}") + return False + + print("āœ… OpenMVS tools are available and working") + return True + +def validate_reconstruction_paths(base_path: str, image_folder: str): + """Validate that all required paths exist for reconstruction.""" + image_dir = os.path.join(base_path, image_folder, "images_png") + workspace_dir = os.path.join(base_path, image_folder, "workspace") + + if not os.path.exists(image_dir): + print(f"Image directory not found: {image_dir}") + return False + + print(f"Image directory found: {image_dir}") + print(f"Workspace will be created at: {workspace_dir}") + return True + + +if __name__ == "__main__": + #File path names + if len(sys.argv) < 2: + print("Usage: python reconstruction.py [--automatic]") + print(" --automatic: Use COLMAP automatic reconstruction instead of manual pipeline") + sys.exit(1) + + image_file_name = sys.argv[1] + use_automatic = "--automatic" in sys.argv + + #Where reconstructions and image data is stored + base_path = "/home/user/tmpData/AI_scan/" + + #Depending on where openMVS is ran + mvs_bin_path = "/usr/local/bin/OpenMVS/" + + # First check COLMAP availability + if not check_colmap_availability(): + print("Cannot proceed without COLMAP") + sys.exit(1) + + # Check OpenMVS availability + if not check_openmvs_availability(): + print("OpenMVS not available - will only run COLMAP pipeline") + run_openmvs = False + else: + run_openmvs = True + + # Validate paths + if not validate_reconstruction_paths(base_path, image_file_name): + print("Path validation failed") + sys.exit(1) + + image_dir = os.path.join(base_path, image_file_name, "images_png") + workspace_dir = os.path.join(base_path, image_file_name, "workspace") + + print("Starting reconstruction pipeline...") + t0 = time.time() + + try: + if use_automatic: + # Run COLMAP automatic reconstruction + print("Using COLMAP automatic reconstruction...") + automatic_reconstruction(image_dir, workspace_dir) + t1 = time.time() + print(f"COLMAP automatic reconstruction completed in {t1 - t0:.2f}s") + else: + # Run manual COLMAP pipeline + run_colmap_pipeline(image_dir, workspace_dir) + t1 = time.time() + print(f"COLMAP pipeline completed in {t1 - t0:.2f}s") + + # Run OpenMVS pipeline if available + if run_openmvs: + print("Starting OpenMVS pipeline...") + t2 = time.time() + run_openmvs_pipeline(base_path, image_dir, workspace_dir, mvs_bin_path, image_file_name) + t3 = time.time() + print(f"OpenMVS pipeline completed in {t3 - t2:.2f}s") + print(f"Total reconstruction time: {t3 - t0:.2f}s") + else: + print("OpenMVS pipeline skipped - COLMAP results available in workspace") + + except Exception as e: + print(f"Reconstruction failed: {e}") + sys.exit(1) diff --git a/reconstruction_copy.py b/reconstruction_copy.py new file mode 100644 index 0000000..2f79acd --- /dev/null +++ b/reconstruction_copy.py @@ -0,0 +1,189 @@ +import shutil, sys, subprocess, os, time +from PIL import Image + +def ensure_directories(*paths: str) -> None: + """ Create directories if they don't exist.""" + for path in paths: + os.makedirs(path, exist_ok=True) + +def feature_extraction(imageDir: str, databasePath: str, colmapPath: str): + """ Run feature extraction. """ + subprocess.run([ + colmapPath, "feature_extractor", + "--database_path", databasePath, + "--image_path", imageDir, + "--SiftExtraction.use_gpu", "0", + "--SiftExtraction.num_threads", "6" # or 1 to be safe + ], check=True) + +def feature_matching(databasePath: str, colmapPath: str): + """ + Run feature matching. + """ + subprocess.run([ + colmapPath, "exhaustive_matcher", + "--database_path", databasePath, + "--SiftMatching.use_gpu", "0", + ], check=True) + +def sparse_reconstruction(imageDir: str, databasePath: str, colmapPath: str, sparseDir: str): + """ + Run sparse reconstruction. + """ + subprocess.run([ + colmapPath, "mapper", + "--database_path", databasePath, + "--image_path", imageDir, + "--output_path", sparseDir + ], check=True) + +def image_undistorter(imageDir: str, denseDir: str, colmapPath: str, sparseDir: str): + """ + Run image undistorter, which outputs acccording + to quality of point detection. + """ + subprocess.run([ + colmapPath, "image_undistorter", + "--image_path", imageDir, + "--input_path", sparseDir, "0", + "--output_path", denseDir, + "--output_type", "COLMAP" + ], check=True) + +def interface_colmap(workspace_dir: str, imageDir: str, basePath: str, sceneMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Run interfaceCOLMAP, which converts gathered data from + colmap and output scene_mvs, containing necessary scene + for next reconstruction steps. + """ + subprocess.run([ + os.path.join(mvs_bin_dir, "InterfaceCOLMAP"), + "-i", os.path.join(workspace_dir, "dense"), #Needed for manual colmap + #"-i", os.path.join(workspace_dir, "dense", "0"), Needed for automaticReconstruction combo + "-o", sceneMVS, + "--image-folder", imageDir + ],cwd=denseDir) + +def densify_point_cloud(sceneMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Densify point cloud connects points by creating more points + in between already created points. """ + subprocess.run([ + os.path.join(mvs_bin_dir, "DensifyPointCloud"), + sceneMVS + ], cwd=denseDir) + +def reconstruct_mesh(denseMVS: str, denseDir: str, mvs_bin_dir: str): + """ + Creates mesh of object, lacking color but connecting points. + """ + subprocess.run([ + os.path.join(mvs_bin_dir, "ReconstructMesh"), + denseMVS + ], cwd=denseDir) + +def texture_mesh(denseDir: str, sceneMVS: str, mvs_bin_dir: str): + """ + Texture mesh mixes data from both sceneMVS and the mesh + + Note: This is time consuming and should be skipped if mesh quality is subpar + """ + + subprocess.run([ + os.path.join(mvs_bin_dir, "TextureMesh"), + sceneMVS, + "-m", "scene_dense_mesh.ply" + ],cwd=denseDir) + +def run_colmap_pipeline(image_dir: str, workspace_dir: str, colmap_path: str = "colmap") -> None: + """ + Full COLMAP pipeline: feature extraction → matching → sparse reconstruction → undistortion. + """ + print(f"Running COLMAP pipeline on {image_dir} -> {workspace_dir}") + database_path = os.path.join(workspace_dir, "database.db") + sparse_dir = os.path.join(workspace_dir, "sparse") + dense_dir = os.path.join(workspace_dir, "dense") + + ensure_directories(workspace_dir, sparse_dir, dense_dir) + + feature_extraction(image_dir, database_path, colmap_path) + feature_matching(database_path, colmap_path) + sparse_reconstruction(image_dir, database_path, colmap_path, sparse_dir) + image_undistorter(image_dir, dense_dir, colmap_path, os.path.join(sparse_dir, "0")) + + print("COLMAP pipeline completed.") + +def run_openmvs_pipeline(base_path: str, image_dir: str, workspace_dir: str, mvs_bin_dir: str, image_file_name: str) -> None: + """ + Run OpenMVS conversion and mesh reconstruction from COLMAP output. + """ + + dense_dir = os.path.join(workspace_dir, "dense") + scene_mvs = os.path.join(dense_dir, "scene.mvs") + dense_mvs = os.path.join(dense_dir, "scene_dense.mvs") + ensure_directories(dense_dir) + + interface_colmap(workspace_dir, image_dir, base_path, scene_mvs, dense_dir, mvs_bin_dir) + densify_point_cloud(scene_mvs, dense_dir, mvs_bin_dir) + reconstruct_mesh(dense_mvs, dense_dir, mvs_bin_dir) + texture_mesh(dense_dir, scene_mvs, mvs_bin_dir) + + print("OpenMVS pipeline completed.") + +def automatic_reconstruction(image_dir: str, workspace_dir: str, colmap_path: str = "colmap"): + """ + Run COLMAP's automatic reconstruction pipeline. + """ + database_path = os.path.join(workspace_dir, "database.db") + sparse_dir = os.path.join(workspace_dir, "sparse") + dense_dir = os.path.join(workspace_dir, "dense") + + ensure_directories(workspace_dir, sparse_dir, dense_dir) + + subprocess.run([ + colmap_path, "automatic_reconstructor", + "--workspace_path", workspace_dir, + "--image_path", image_dir, + "--data_type", "individual", # or 'video' depending on your input + "--quality", "medium", # can be 'low', 'medium', or 'high' + "--sparse", "true", + "--dense", "true", + ], check=True) + + print("Automatic reconstruction completed.") + + +if __name__ == "__main__": + #File path names + image_file_name = sys.argv[1] + + #Where reconstructions and image data is stored + base_path = "/home/user/tmpData/AI_scan/" + + #Depending on where openMVS is ran + mvs_bin_path = "/usr/local/bin/OpenMVS/" + + image_dir = os.path.join(base_path, image_file_name, "images_png") + + if ((os.path.exists(image_dir)) == 0): + #Default images folder + + #Default workspace folder + workspace_dir = os.path.join(base_path, image_file_name, "workspace") + print("Made it here") + t0 = time.time() + run_colmap_pipeline(image_dir, workspace_dir) + #automatic_reconstruction(image_dir, workspace_dir) + t1 = time.time() + + t2 = time.time() + run_openmvs_pipeline(base_path, image_dir, workspace_dir, mvs_bin_path, image_file_name) + t3= time.time() + + #Timing check + print(f"COLMAP time: {t1 - t0:.2f}s") + print(f"OpenMVS time: {t3 - t2:.2f}s") + print(f"Total time: {t3 - t0:.2f}s") + else: + print("This did not work") + diff --git a/run_photogrammetry_scan copy.py b/run_photogrammetry_scan copy.py new file mode 100644 index 0000000..c22ac40 --- /dev/null +++ b/run_photogrammetry_scan copy.py @@ -0,0 +1,461 @@ +from ophyd import EpicsMotor, EpicsSignal +from ophyd.areadetector.plugins import PluginBase +from ophyd.areadetector import AreaDetector, ADComponent, ImagePlugin, TIFFPlugin +from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.cam import CamBase +from bluesky import RunEngine +from databroker import Broker, temp +from bluesky.plans import scan, count +import bluesky.plan_stubs as bps +import numpy as np +from datetime import datetime +from pathlib import Path +from PIL import Image +import time, sys, os, subprocess +from collections import defaultdict + +# Manual Tiled writing imports (similar to store.txt approach) +try: + from tiled.client import from_uri + TILED_AVAILABLE = True +except ImportError: + TILED_AVAILABLE = False + print("Warning: Tiled client not available, running without Tiled integration") +class PvaPlugin(PluginBase): + _suffix = 'Pva1:' + _plugin_type = 'NDPluginPva' + _default_read_attrs = ['enable'] + _default_configuration_attrs = ['enable'] + + array_callbacks = ADComponent(EpicsSignal, 'ArrayCallbacks') + +# Create RunEngine +RE = RunEngine({}) + +# Set up manual Tiled connection (similar to store.txt approach) +tiled_client = None +if TILED_AVAILABLE: + try: + tiled_uri = "http://localhost:8000" + tiled_api_key = "ca6ae384c9f944e1465176b7e7274046b710dc7e2703dc33369f7c900d69bd64" + + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + print(f"āœ… Tiled client connected to {tiled_uri}") + except Exception as e: + print(f"āš ļø Failed to connect to Tiled: {e}") + TILED_AVAILABLE = False + tiled_client = None + +# Define the motor +motor = EpicsMotor('DMC01:A', name='motor') + +# Define the camera deviceca +class MyCamera(AreaDetector): + cam = ADComponent(AreaDetectorCam, 'cam1:') #Fixed the single camera issue? + image = ADComponent(ImagePlugin, 'image1:') + tiff = ADComponent(TIFFPlugin, 'TIFF1:') + pva = ADComponent(PvaPlugin, 'Pva1:') + +# Instantiate the camera +camera = MyCamera('13ARV1:', name='camera') +camera.wait_for_connection() + +#CAM OPTIONS +camera.stage_sigs[camera.cam.acquire] = 0 +camera.stage_sigs[camera.cam.image_mode] = 0 # single multiple continuous +camera.stage_sigs[camera.cam.trigger_mode] = 0 # internal external + +#IMAGE OPTIONS +camera.stage_sigs[camera.image.enable] = 1 # pva plugin +camera.stage_sigs[camera.image.queue_size] = 2000 + +#TIFF OPTIONS +camera.stage_sigs[camera.tiff.enable] = 1 +camera.stage_sigs[camera.tiff.auto_save] = 1 +camera.stage_sigs[camera.tiff.file_write_mode] = 0 # Or 'Single' works too +camera.stage_sigs[camera.tiff.nd_array_port] = 'SP1' +camera.stage_sigs[camera.tiff.auto_increment] = 1 #Doesn't work, must be ignored + +#PVA OPTIONS +camera.stage_sigs[camera.pva.enable] = 1 +camera.stage_sigs[camera.pva.blocking_callbacks] = 'No' +camera.stage_sigs[camera.pva.queue_size] = 2000 # or higher +camera.stage_sigs[camera.pva.nd_array_port] = 'SP1' +camera.stage_sigs[camera.pva.array_callbacks] = 0 # disable during scan + +def wait_for_file(filepath, timeout=5.0, poll_interval=0.1): + """Wait until a file appears on disk, or timeout.""" + start = time.time() + while not os.path.exists(filepath): + if time.time() - start > timeout: + raise TimeoutError(f"Timed out waiting for file: {filepath}") + time.sleep(poll_interval) + +def scan_with_saves(start_pos, end_pos, num_points): + #Requirements for image capturing + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + callbacks_signal = EpicsSignal('13ARV1:image1:EnableCallbacks', name='callbacks_signal') + acquire_signal = EpicsSignal('13ARV1:cam1:Acquire', name='acquire_signal') + + yield from bps.mv(callbacks_signal, 0) + max_retries = 50 + positions = np.linspace(start_pos, end_pos, num_points) + yield from bps.open_run() + camera.cam.array_callbacks.put(0, wait=True) + + print("\n--- Staging camera ---") + yield from bps.stage(camera) + + current_number = camera.tiff.file_number.get() + + NUM_IMAGES_PER_POS = 20 + + for i, pos in enumerate(positions): + print(f"\nMoving to pos={pos}") + yield from bps.mv(motor, pos) + yield from bps.sleep(2.0) + yield from bps.mv(acquire_signal, 0) # Triggers a single image + + #for img_idx in range(NUM_IMAGES_PER_POS): + filename = f'scan_{timestamp}_pos_{i}_shot_angle_{pos * 2.8125}' + current_number += 1 + filepath = os.path.join(save_dir, f"{filename}_{current_number}.tiff") + + yield from bps.mv(camera.tiff.file_name, filename) + yield from bps.mv(camera.tiff.file_number, current_number) + + for attempt in range(1, max_retries + 1): + + try: + print(f"[Attempt {attempt}] Capturing → {filepath}") + yield from bps.mv(acquire_signal, 1) # Triggers a single image + yield from bps.sleep(1) + + # Wait for file to appear + wait_for_file(filepath, timeout=5.0) + + print(f"āœ“ Image saved at {filepath}") + break # Exit retry loop if successful + + except TimeoutError: + print(f"--Timeout waiting for image at {filepath}") + if attempt == max_retries: + print(f"--Failed after {max_retries} attempts, skipping position {pos}") + else: + print("↻ Retrying acquisition...") + yield from bps.mv(acquire_signal, 0) # Triggers a single image + yield from bps.sleep(0.5) + + print("\n--- Unstaging camera ---") + yield from bps.unstage(camera) + + yield from bps.mv(motor, 0.0) + yield from bps.close_run() + +def cropImages(inputDir): + crop_box = (800, 800, 1600, 1500) + output_dir = inputDir.replace('raw_images/', 'images/') + + os.makedirs(output_dir, exist_ok=True) + + for filename in os.listdir(inputDir): + if filename.endswith('.tiff'): + image_path = os.path.join(inputDir, filename) + img = Image.open(image_path) + cropped = img.crop(crop_box) + cropped.save(os.path.join(output_dir, filename)) + +def convert_image_format(image_dir: str, output_image_dir: str): + os.makedirs(output_image_dir, exist_ok=True) + + for filename in os.listdir(image_dir): + if filename.lower().endswith(".tiff") or filename.lower().endswith(".tif"): + tiff_path = os.path.join(image_dir, filename) + png_filename = os.path.splitext(filename)[0] + ".png" + png_path = os.path.join(output_image_dir, png_filename) + + with Image.open(tiff_path) as im: + im.save(png_path, format="PNG") + +def save_photogrammetry_scan_metadata_to_tiled(scan_params, scan_timestamp): + """Save photogrammetry scan metadata to Tiled""" + if not TILED_AVAILABLE or tiled_client is None: + print("āš ļø Tiled not available for scan metadata upload") + return False + + try: + print(f"šŸ“¤ Uploading photogrammetry scan metadata to Tiled...") + + # Create container for photogrammetry scan metadata + container_path = "photogrammetry_scan_metadata" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + # Create metadata array + start_pos, end_pos, num_points, scan_type = scan_params + metadata_array = np.array([start_pos, end_pos, num_points]) + + # Create unique key with random component to avoid conflicts + import random + random_suffix = random.randint(1000, 9999) + unique_key = f"photogrammetry_scan_{scan_timestamp}_r{random_suffix}" + + # Check if key exists and delete if needed + if unique_key in container: + print(f"šŸ”„ Key {unique_key} exists, deleting...") + del container[unique_key] + + # Store metadata array with fallback for ZARR issues + try: + container.write_array( + metadata_array, + metadata={ + "description": f"Photogrammetry scan from {start_pos}° to {end_pos}° with {num_points} points", + "timestamp": scan_timestamp, + "start_position": start_pos, + "end_position": end_pos, + "num_points": num_points, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scan_range_degrees": (end_pos - start_pos), + "step_size": (end_pos - start_pos) / (num_points - 1) if num_points > 1 else 0 + }, + key=unique_key + ) + print(f"āœ… Photogrammetry scan metadata uploaded to Tiled with key: {unique_key}") + return True + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + print(f"āš ļø ZARR storage failed for scan metadata, trying alternative approach...") + + # Alternative: Store as simple scalar + container.write_array( + np.array([1.0]), # Simple scalar to avoid ZARR + metadata={ + "description": f"Photogrammetry scan from {start_pos}° to {end_pos}° with {num_points} points", + "timestamp": scan_timestamp, + "start_position": start_pos, + "end_position": end_pos, + "num_points": num_points, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scan_range_degrees": (end_pos - start_pos), + "step_size": (end_pos - start_pos) / (num_points - 1) if num_points > 1 else 0, + "fallback_mode": "simplified" + }, + key=unique_key + ) + print(f"āœ… Photogrammetry scan metadata uploaded to Tiled with key: {unique_key} (fallback mode)") + return True + else: + raise zarr_error + + except Exception as e: + print(f"āš ļø Failed to upload photogrammetry scan metadata to Tiled: {e}") + return False + +def upload_photogrammetry_images_to_tiled(image_dirs, scan_timestamp, scan_params): + """Upload all processed photogrammetry images to Tiled""" + if not TILED_AVAILABLE or tiled_client is None: + print("āš ļø Tiled not available for image upload") + return False + + try: + print(f"šŸ“¤ Uploading photogrammetry images to Tiled...") + + # Extract scan type from scan_params for container organization + start_pos, end_pos, num_points, scan_type = scan_params + + # Create run-specific container with descriptive name + container_path = f"photogrammetry_images_{scan_type}" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + uploaded_count = 0 + total_images = 0 + + # Process each image directory + for dir_type, image_dir in image_dirs.items(): + if not os.path.exists(image_dir): + print(f"āš ļø Directory not found: {image_dir}") + continue + + print(f"šŸ“ Processing {dir_type} images from: {image_dir}") + + # Get image files + if dir_type == "png": + image_files = [f for f in os.listdir(image_dir) if f.lower().endswith('.png')] + else: + image_files = [f for f in os.listdir(image_dir) if f.lower().endswith(('.tiff', '.tif'))] + + image_files.sort() # Sort for consistent ordering + total_images += len(image_files) + + for img_file in image_files: + img_path = os.path.join(image_dir, img_file) + + try: + # Load image as numpy array + img = Image.open(img_path) + img_array = np.array(img) + + # Extract position info from filename if possible + position_index = 0 + angle_value = 0.0 + if '_pos_' in img_file: + try: + parts = img_file.split('_') + pos_idx = next((i for i, part in enumerate(parts) if part == 'pos'), -1) + if pos_idx >= 0 and pos_idx + 1 < len(parts): + position_index = int(parts[pos_idx + 1]) + + # Try to extract angle if available + if 'angle' in img_file.lower(): + angle_idx = next((i for i, part in enumerate(parts) if 'angle' in part.lower()), -1) + if angle_idx >= 0 and angle_idx + 1 < len(parts): + angle_value = float(parts[angle_idx + 1]) + except (ValueError, IndexError): + pass + + # Create unique key for this image with random component + import random + random_suffix = random.randint(1000, 9999) + unique_key = f"pos_{position_index:03d}_{scan_timestamp}_r{random_suffix}" + + # Check if key exists and delete if needed + if unique_key in container: + print(f"šŸ”„ Key {unique_key} exists, deleting...") + del container[unique_key] + + # Create metadata for image + img_metadata = { + "description": f"Photogrammetry {dir_type} image at position {position_index}", + "timestamp": scan_timestamp, + "position_index": position_index, + "angle_degrees": angle_value, + "filename": img_file, + "image_type": f"photogrammetry_{dir_type}", + "scan_params": scan_params, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scroll_wheel_enabled": True, + "directory_type": dir_type, + "container_path": container_path + } + + # Upload to Tiled with fallback handling + try: + container.write_array( + img_array.astype(np.float32), + metadata=img_metadata, + key=unique_key + ) + uploaded_count += 1 + if uploaded_count % 10 == 0: # Progress update every 10 images + print(f" šŸ“¤ Uploaded {uploaded_count}/{total_images} images...") + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + # Fallback: Store metadata with image info + container.write_array( + np.array([position_index, angle_value, len(img_array.flatten())]), + metadata={**img_metadata, "fallback_mode": "metadata_only", "image_path": img_path, "image_shape": img_array.shape}, + key=unique_key + ) + uploaded_count += 1 + else: + raise zarr_error + + except Exception as e: + print(f"āš ļø Failed to upload {img_file}: {e}") + + print(f"āœ… Successfully uploaded {uploaded_count}/{total_images} photogrammetry images to Tiled") + print(f"šŸ“ Images stored in container: {container_path}") + return uploaded_count > 0 + + except Exception as e: + print(f"āš ļø Failed to upload photogrammetry images to Tiled: {e}") + return False + +if __name__ == "__main__": + # Run scan + try: + print("Starting script") + + # Check Tiled availability + if TILED_AVAILABLE and tiled_client: + print("āœ… Tiled integration enabled") + else: + print("āš ļø Running without Tiled integration") + + # File configuration + base_path = '/home/user/tmpData/AI_scan/' + sys.argv[4] + save_dir = base_path + '/raw_images/' + + # Ensure the directory exists + os.makedirs(save_dir, exist_ok=True) + # Then set the path in EPICS + + start_pos = float(sys.argv[1]) + end_pos = float(sys.argv[2]) + num_points = int(sys.argv[3]) + scan_type = sys.argv[4] + + # Generate timestamp for this scan + scan_timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f') + + camera.tiff.file_path.put(save_dir) + camera.tiff.file_template.put('%s%s_%d.tiff') + + print(f"šŸ”„ Starting photogrammetry scan: {start_pos}° to {end_pos}° ({num_points} points)") + RE(scan_with_saves(start_pos, end_pos, num_points)) + print(f"āœ… Photogrammetry scan completed") + + print(f"āœ‚ļø Cropping images...") + cropImages(save_dir) + print(f"āœ… Image cropping completed") + + image_dir_preprocess = os.path.join(base_path, "images") + image_dir = os.path.join(base_path, "images_png") + + if ((os.path.exists(image_dir)) == 0): + print(f"šŸ”„ Converting images to PNG format...") + convert_image_format(image_dir_preprocess, image_dir) + print(f"āœ… Image format conversion completed") + + # Upload everything to Tiled after all processing is complete + if TILED_AVAILABLE and tiled_client: + print(f"\nšŸ“¤ Starting Tiled uploads for photogrammetry scan...") + + # Upload scan metadata + scan_params = (start_pos, end_pos, num_points, scan_type) + save_photogrammetry_scan_metadata_to_tiled(scan_params, scan_timestamp) + + # Only upload the final PNG images from images_png folder + if os.path.exists(image_dir): + print(f"šŸ“ Uploading only PNG images from: {image_dir}") + image_dirs = {"png": image_dir} + upload_photogrammetry_images_to_tiled(image_dirs, scan_timestamp, scan_params) + else: + print("āš ļø PNG images directory not found for upload") + + print(f"āœ… Tiled uploads completed!") + else: + print(f"āš ļø Skipping Tiled uploads (Tiled not available)") + + #cropped_dir = save_dir.replace('images_uncropped/', 'images/') + #average_output_dir = os.path.join(cropped_dir, 'averaged') + #average_images_per_position(cropped_dir, average_output_dir) + + except KeyboardInterrupt: + print("\nScan interrupted by user") + RE.stop() + except Exception as e: + print(f"\nError during scan: {e}") + #RE.stop() \ No newline at end of file diff --git a/run_photogrammetry_scan.py b/run_photogrammetry_scan.py new file mode 100644 index 0000000..afc0140 --- /dev/null +++ b/run_photogrammetry_scan.py @@ -0,0 +1,611 @@ +from ophyd import EpicsMotor, EpicsSignal +from ophyd.areadetector.plugins import PluginBase +from ophyd.areadetector import AreaDetector, ADComponent, ImagePlugin, TIFFPlugin +from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.cam import CamBase +from bluesky import RunEngine +from databroker import Broker, temp +from bluesky.plans import scan, count +import bluesky.plan_stubs as bps +import numpy as np +from datetime import datetime +from pathlib import Path +from PIL import Image +import time, sys, os, subprocess +from collections import defaultdict + +# Manual Tiled writing imports (similar to store.txt approach) +try: + from tiled.client import from_uri + TILED_AVAILABLE = True +except ImportError: + TILED_AVAILABLE = False + print("Warning: Tiled client not available, running without Tiled integration") +class PvaPlugin(PluginBase): + _suffix = 'Pva1:' + _plugin_type = 'NDPluginPva' + _default_read_attrs = ['enable'] + _default_configuration_attrs = ['enable'] + + array_callbacks = ADComponent(EpicsSignal, 'ArrayCallbacks') + +# Create RunEngine +RE = RunEngine({}) + +# Set up manual Tiled connection (similar to store.txt approach) +tiled_client = None +if TILED_AVAILABLE: + try: + tiled_uri = "http://localhost:8000" + tiled_api_key = "ca6ae384c9f944e1465176b7e7274046b710dc7e2703dc33369f7c900d69bd64" + + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + print(f"āœ“ Tiled client connected to {tiled_uri}") + except Exception as e: + print(f" Failed to connect to Tiled: {e}") + TILED_AVAILABLE = False + tiled_client = None + +# Define the motor +motor = EpicsMotor('DMC01:A', name='motor') + +# Define the camera deviceca +class MyCamera(AreaDetector): + cam = ADComponent(AreaDetectorCam, 'cam1:') #Fixed the single camera issue? + image = ADComponent(ImagePlugin, 'image1:') + tiff = ADComponent(TIFFPlugin, 'TIFF1:') + pva = ADComponent(PvaPlugin, 'Pva1:') + +# Instantiate the camera +camera = MyCamera('13ARV1:', name='camera') +camera.wait_for_connection() + +#CAM OPTIONS +camera.stage_sigs[camera.cam.acquire] = 0 +camera.stage_sigs[camera.cam.image_mode] = 0 # single multiple continuous +camera.stage_sigs[camera.cam.trigger_mode] = 0 # internal external + +#IMAGE OPTIONS +camera.stage_sigs[camera.image.enable] = 1 # pva plugin +camera.stage_sigs[camera.image.queue_size] = 2000 + +#TIFF OPTIONS +camera.stage_sigs[camera.tiff.enable] = 1 +camera.stage_sigs[camera.tiff.auto_save] = 1 +camera.stage_sigs[camera.tiff.file_write_mode] = 0 # Or 'Single' works too +camera.stage_sigs[camera.tiff.nd_array_port] = 'SP1' +camera.stage_sigs[camera.tiff.auto_increment] = 1 #Doesn't work, must be ignored + +#PVA OPTIONS +camera.stage_sigs[camera.pva.enable] = 1 +camera.stage_sigs[camera.pva.blocking_callbacks] = 'No' +camera.stage_sigs[camera.pva.queue_size] = 2000 # or higher +camera.stage_sigs[camera.pva.nd_array_port] = 'SP1' +camera.stage_sigs[camera.pva.array_callbacks] = 0 # disable during scan + +def wait_for_file(filepath, timeout=5.0, poll_interval=0.1): + """Wait until a file appears on disk, or timeout.""" + start = time.time() + while not os.path.exists(filepath): + if time.time() - start > timeout: + raise TimeoutError(f"Timed out waiting for file: {filepath}") + time.sleep(poll_interval) + +def scan_with_saves(start_pos, end_pos, num_points): + #Requirements for image capturing + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + callbacks_signal = EpicsSignal('13ARV1:image1:EnableCallbacks', name='callbacks_signal') + acquire_signal = EpicsSignal('13ARV1:cam1:Acquire', name='acquire_signal') + + yield from bps.mv(callbacks_signal, 0) + max_retries = 50 + positions = np.linspace(start_pos, end_pos, num_points) + yield from bps.open_run() + camera.cam.array_callbacks.put(0, wait=True) + + print("\n--- Staging camera ---") + yield from bps.stage(camera) + + current_number = camera.tiff.file_number.get() + + NUM_IMAGES_PER_POS = 20 + + for i, pos in enumerate(positions): + print(f"\nMoving to pos={pos}") + yield from bps.mv(motor, pos / 2.8125) + yield from bps.sleep(2.0) + yield from bps.mv(acquire_signal, 0) # Triggers a single image + + #for img_idx in range(NUM_IMAGES_PER_POS): + filename = f'scan_{timestamp}_pos_{i}_shot_angle_{pos:.2f}' + current_number += 1 + filepath = os.path.join(save_dir, f"{filename}_{current_number}.tiff") + + yield from bps.mv(camera.tiff.file_name, filename) + yield from bps.mv(camera.tiff.file_number, current_number) + + for attempt in range(1, max_retries + 1): + + try: + print(f"[Attempt {attempt}] Capturing → {filepath}") + yield from bps.mv(acquire_signal, 1) # Triggers a single image + yield from bps.sleep(1) + + # Wait for file to appear + wait_for_file(filepath, timeout=5.0) + + print(f"āœ“ Image saved at {filepath}") + break # Exit retry loop if successful + + except TimeoutError: + print(f"--Timeout waiting for image at {filepath}") + if attempt == max_retries: + print(f"--Failed after {max_retries} attempts, skipping position {pos}") + else: + print("↻ Retrying acquisition...") + yield from bps.mv(acquire_signal, 0) # Triggers a single image + yield from bps.sleep(0.5) + + print("\n--- Unstaging camera ---") + yield from bps.unstage(camera) + + yield from bps.mv(motor, 0.0) + yield from bps.close_run() + +def cropImages(inputDir): + crop_box = (800, 800, 1600, 1500) + output_dir = inputDir.replace('raw_images/', 'images/') + + os.makedirs(output_dir, exist_ok=True) + + for filename in os.listdir(inputDir): + if filename.endswith('.tiff'): + image_path = os.path.join(inputDir, filename) + img = Image.open(image_path) + cropped = img.crop(crop_box) + cropped.save(os.path.join(output_dir, filename)) + +def convert_image_format(image_dir: str, output_image_dir: str): + os.makedirs(output_image_dir, exist_ok=True) + + for filename in os.listdir(image_dir): + if filename.lower().endswith(".tiff") or filename.lower().endswith(".tif"): + tiff_path = os.path.join(image_dir, filename) + png_filename = os.path.splitext(filename)[0] + ".png" + png_path = os.path.join(output_image_dir, png_filename) + + with Image.open(tiff_path) as im: + im.save(png_path, format="PNG") + +def save_photogrammetry_scan_metadata_to_tiled(scan_params, scan_timestamp): + """Save photogrammetry scan metadata to Tiled""" + if not TILED_AVAILABLE or tiled_client is None: + print(" Tiled not available for scan metadata upload") + return False + + try: + print(f" Uploading photogrammetry scan metadata to Tiled...") + + # Create container for photogrammetry scan metadata + container_path = "photogrammetry_scan_metadata" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + # Create metadata array + start_pos, end_pos, num_points, scan_type = scan_params + metadata_array = np.array([start_pos, end_pos, num_points]) + + # Create unique key with random component to avoid conflicts + import random + random_suffix = random.randint(1000, 9999) + unique_key = f"photogrammetry_scan_{scan_timestamp}_r{random_suffix}" + + # Check if key exists and delete if needed + if unique_key in container: + print(f" Key {unique_key} exists, deleting...") + del container[unique_key] + + # Store metadata array with fallback for ZARR issues + try: + container.write_array( + metadata_array, + metadata={ + "description": f"Photogrammetry scan from {start_pos}° to {end_pos}° with {num_points} points", + "timestamp": scan_timestamp, + "start_position": start_pos, + "end_position": end_pos, + "num_points": num_points, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scan_range_degrees": (end_pos - start_pos), + "step_size": (end_pos - start_pos) / (num_points - 1) if num_points > 1 else 0 + }, + key=unique_key + ) + print(f"āœ“ Photogrammetry scan metadata uploaded to Tiled with key: {unique_key}") + return True + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + print(f" ZARR storage failed for scan metadata, trying alternative approach...") + + # Alternative: Store as simple scalar + container.write_array( + np.array([1.0]), # Simple scalar to avoid ZARR + metadata={ + "description": f"Photogrammetry scan from {start_pos}° to {end_pos}° with {num_points} points", + "timestamp": scan_timestamp, + "start_position": start_pos, + "end_position": end_pos, + "num_points": num_points, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scan_range_degrees": (end_pos - start_pos), + "step_size": (end_pos - start_pos) / (num_points - 1) if num_points > 1 else 0, + "fallback_mode": "simplified" + }, + key=unique_key + ) + print(f"āœ“ Photogrammetry scan metadata uploaded to Tiled with key: {unique_key} (fallback mode)") + return True + else: + raise zarr_error + + except Exception as e: + print(f" Failed to upload photogrammetry scan metadata to Tiled: {e}") + return False + +def upload_photogrammetry_images_to_tiled(image_dirs, scan_timestamp, scan_params): + """Upload all processed photogrammetry images to Tiled""" + if not TILED_AVAILABLE or tiled_client is None: + print(" Tiled not available for image upload") + return False + + try: + print(f" Uploading photogrammetry images to Tiled...") + + # Extract scan type from scan_params for container organization + start_pos, end_pos, num_points, scan_type = scan_params + + # Create run-specific container with descriptive name + container_path = f"photogrammetry_images_{scan_type}" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + uploaded_count = 0 + total_images = 0 + + # Process each image directory + for dir_type, image_dir in image_dirs.items(): + if not os.path.exists(image_dir): + print(f" Directory not found: {image_dir}") + continue + + print(f" Processing {dir_type} images from: {image_dir}") + + # Get image files + if dir_type == "png": + image_files = [f for f in os.listdir(image_dir) if f.lower().endswith('.png')] + else: + image_files = [f for f in os.listdir(image_dir) if f.lower().endswith(('.tiff', '.tif'))] + + # Sort files by position number for proper ordering (1, 2, 3, 4, 5... not 1, 10, 11, 2, 3...) + def extract_position_number(filename): + """Extract position number from filename for proper numeric sorting""" + try: + if '_pos_' in filename: + parts = filename.split('_') + pos_idx = next((i for i, part in enumerate(parts) if part == 'pos'), -1) + if pos_idx >= 0 and pos_idx + 1 < len(parts): + return int(parts[pos_idx + 1]) + return 0 # Default if no position found + except (ValueError, IndexError): + return 0 + + image_files.sort(key=extract_position_number) # Sort by position number + total_images += len(image_files) + + for img_file in image_files: + img_path = os.path.join(image_dir, img_file) + + try: + # Load image as numpy array + img = Image.open(img_path) + img_array = np.array(img) + + # Extract position info from filename if possible + position_index = 0 + angle_value = 0.0 + if '_pos_' in img_file: + try: + parts = img_file.split('_') + pos_idx = next((i for i, part in enumerate(parts) if part == 'pos'), -1) + if pos_idx >= 0 and pos_idx + 1 < len(parts): + position_index = int(parts[pos_idx + 1]) + + # Try to extract angle if available + if 'angle' in img_file.lower(): + angle_idx = next((i for i, part in enumerate(parts) if 'angle' in part.lower()), -1) + if angle_idx >= 0 and angle_idx + 1 < len(parts): + angle_value = float(parts[angle_idx + 1]) + except (ValueError, IndexError): + pass + + # Create unique key for this image with random component + import random + random_suffix = random.randint(1000, 9999) + unique_key = f"pos_{position_index:03d}_{scan_timestamp}_r{random_suffix}" + + # Check if key exists and delete if needed + if unique_key in container: + print(f" Key {unique_key} exists, deleting...") + del container[unique_key] + + # Create metadata for image + img_metadata = { + "description": f"Photogrammetry {dir_type} image at position {position_index}", + "timestamp": scan_timestamp, + "position_index": position_index, + "angle_degrees": angle_value, + "filename": img_file, + "image_type": f"photogrammetry_{dir_type}", + "scan_params": scan_params, + "scan_type": scan_type, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scroll_wheel_enabled": True, + "directory_type": dir_type, + "container_path": container_path + } + + # Upload to Tiled with fallback handling + try: + container.write_array( + img_array.astype(np.float32), + metadata=img_metadata, + key=unique_key + ) + uploaded_count += 1 + if uploaded_count % 10 == 0: # Progress update every 10 images + print(f" āœ“ Uploaded {uploaded_count}/{total_images} images...") + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + # Fallback: Store metadata with image info + container.write_array( + np.array([position_index, angle_value, len(img_array.flatten())]), + metadata={**img_metadata, "fallback_mode": "metadata_only", "image_path": img_path, "image_shape": img_array.shape}, + key=unique_key + ) + uploaded_count += 1 + else: + raise zarr_error + + except Exception as e: + print(f" Failed to upload {img_file}: {e}") + + print(f"āœ“ Successfully uploaded {uploaded_count}/{total_images} photogrammetry images to Tiled") + print(f" Images stored in container: {container_path}") + return uploaded_count > 0 + + except Exception as e: + print(f" Failed to upload photogrammetry images to Tiled: {e}") + return False + +def upload_photogrammetry_image_stack_to_tiled(image_dir, scan_timestamp, scan_params): + """Upload all PNG images as a 3D array stack for scroll wheel navigation""" + if not TILED_AVAILABLE or tiled_client is None: + print(" Tiled not available for image stack upload") + return False + + try: + print(f"Creating image stack for scroll wheel...") + + # Extract scan type from scan_params + start_pos, end_pos, num_points, scan_type = scan_params + + # Create container for the image stack + stack_container_path = f"photogrammetry_image_stacks_{scan_type}" + if stack_container_path not in tiled_client: + tiled_client.create_container(stack_container_path) + stack_container = tiled_client[stack_container_path] + + if not os.path.exists(image_dir): + print(f" PNG directory not found: {image_dir}") + return False + + # Get all PNG files and sort them by position number + png_files = [f for f in os.listdir(image_dir) if f.lower().endswith('.png')] + + # Sort files by position number for proper ordering (1, 2, 3, 4, 5... not 1, 10, 11, 2, 3...) + def extract_position_number(filename): + """Extract position number from filename for proper numeric sorting""" + try: + if '_pos_' in filename: + parts = filename.split('_') + pos_idx = next((i for i, part in enumerate(parts) if part == 'pos'), -1) + if pos_idx >= 0 and pos_idx + 1 < len(parts): + return int(parts[pos_idx + 1]) + return 0 # Default if no position found + except (ValueError, IndexError): + return 0 + + png_files.sort(key=extract_position_number) # Sort by position number + + if not png_files: + print(f" No PNG files found in {image_dir}") + return False + + print(f" Loading {len(png_files)} PNG images for stack...") + + # Load first image to get dimensions + first_img_path = os.path.join(image_dir, png_files[0]) + first_img = Image.open(first_img_path) + img_height, img_width = first_img.size + + # Create 3D array: [num_images, height, width] + image_stack = np.zeros((len(png_files), img_height, img_width), dtype=np.float32) + + # Load all images into the stack + for i, png_file in enumerate(png_files): + img_path = os.path.join(image_dir, png_file) + img = Image.open(img_path) + img_array = np.array(img) + + # Ensure consistent dimensions + if img_array.shape[:2] == (img_height, img_width): + image_stack[i] = img_array + else: + # Resize if dimensions don't match + img_resized = img.resize((img_width, img_height)) + image_stack[i] = np.array(img_resized) + + if (i + 1) % 5 == 0: # Progress update + print(f" Loaded {i + 1}/{len(png_files)} images...") + + print(f" 3D image stack created: {image_stack.shape}") + + # Create unique key for the stack + import random + random_suffix = random.randint(1000, 9999) + stack_key = f"image_stack_{scan_timestamp}_r{random_suffix}" + + # Check if key exists and delete if needed + if stack_key in stack_container: + print(f" Key {stack_key} exists, deleting...") + del stack_container[stack_key] + + # Create rich metadata for the stack + stack_metadata = { + "description": f"3D photogrammetry image stack with {len(png_files)} images", + "timestamp": scan_timestamp, + "scan_type": scan_type, + "num_images": len(png_files), + "image_dimensions": f"{img_width}x{img_height}", + "stack_shape": image_stack.shape, + "start_position": start_pos, + "end_position": end_pos, + "num_points": num_points, + "motor_name": str(motor), + "datetime": datetime.now().isoformat(), + "scroll_wheel_enabled": True, + "data_type": "3D_image_stack", + "png_files": png_files, + "container_path": stack_container_path + } + + # Upload the 3D stack to Tiled + try: + stack_container.write_array( + image_stack, + metadata=stack_metadata, + key=stack_key + ) + print(f" āœ“ Stack uploaded to Tiled with key: {stack_key}") + print(f" āœ“ Stack stored in container: {stack_container_path}") + print(f" āœ“ Scroll wheel navigation enabled! Use mouse wheel to browse through {len(png_files)} images") + return True + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + print(f"ZARR storage failed for image stack, trying alternative approach...") + + # Fallback: Store as metadata with stack info + stack_container.write_array( + np.array([len(png_files), img_width, img_height]), + metadata={**stack_metadata, "fallback_mode": "metadata_only", "image_dir": image_dir}, + key=stack_key + ) + print(f" Image stack metadata uploaded to Tiled with key: {stack_key} (fallback mode)") + return True + else: + raise zarr_error + + except Exception as e: + print(f" Failed to upload image stack to Tiled: {e}") + return False + +if __name__ == "__main__": + # Run scan + try: + print("Starting script") + + # Check Tiled availability + if TILED_AVAILABLE and tiled_client: + print(" Tiled integration enabled") + else: + print(" Running without Tiled integration") + + # File configuration + base_path = '/home/user/tmpData/AI_scan/' + sys.argv[4] + save_dir = base_path + '/raw_images/' + + # Ensure the directory exists + os.makedirs(save_dir, exist_ok=True) + # Then set the path in EPICS + + start_pos = float(sys.argv[1]) + end_pos = float(sys.argv[2]) + num_points = int(sys.argv[3]) + scan_type = sys.argv[4] + + # Generate timestamp for this scan + scan_timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f') + + camera.tiff.file_path.put(save_dir) + camera.tiff.file_template.put('%s%s_%d.tiff') + + print(f" Starting photogrammetry scan: {start_pos}° to {end_pos}° ({num_points} points)") + RE(scan_with_saves(start_pos, end_pos, num_points)) + print(f" Photogrammetry scan completed") + + print(f" Cropping images...") + cropImages(save_dir) + print(f" Image cropping completed") + + image_dir_preprocess = os.path.join(base_path, "images") + image_dir = os.path.join(base_path, "images_png") + + if ((os.path.exists(image_dir)) == 0): + print(f" Converting images to PNG format...") + convert_image_format(image_dir_preprocess, image_dir) + print(f" āœ“ Image format conversion completed") + + # Upload everything to Tiled after all processing is complete + if TILED_AVAILABLE and tiled_client: + print(f"\n Starting Tiled uploads for photogrammetry scan...") + + # Upload scan metadata + scan_params = (start_pos, end_pos, num_points, scan_type) + save_photogrammetry_scan_metadata_to_tiled(scan_params, scan_timestamp) + + # Only upload the final PNG images from images_png folder + if os.path.exists(image_dir): + print(f" Uploading only PNG images from: {image_dir}") + image_dirs = {"png": image_dir} + upload_photogrammetry_images_to_tiled(image_dirs, scan_timestamp, scan_params) + + # Also upload as a 3D image stack for scroll wheel navigation + print(f"\n Creating scrollable 3D image stack...") + upload_photogrammetry_image_stack_to_tiled(image_dir, scan_timestamp, scan_params) + else: + print(" PNG images directory not found for upload") + + print(f"āœ“ Tiled uploads completed!") + else: + print(f" Skipping Tiled uploads (Tiled not available)") + + #cropped_dir = save_dir.replace('images_uncropped/', 'images/') + #average_output_dir = os.path.join(cropped_dir, 'averaged') + #average_images_per_position(cropped_dir, average_output_dir) + + except KeyboardInterrupt: + print("\nScan interrupted by user") + RE.stop() + except Exception as e: + print(f"\nError during scan: {e}") + #RE.stop() \ No newline at end of file diff --git a/store.txt b/store.txt new file mode 100644 index 0000000..5e29f47 --- /dev/null +++ b/store.txt @@ -0,0 +1,275 @@ +def get_angle(motor, *, md=None): + from bluesky.plan_stubs import rd # Import locally to hide from queue server + import os + import time + import numpy as np + from datetime import datetime + from tiled.client import from_uri + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Read the motor position + angle = yield from rd(motor) + + # Save to Tiled as simple structured data (not DataFrame) + try: + tiled_uri = os.getenv("TILED_URI", "http://localhost:8000") + tiled_api_key = os.getenv("TILED_API_KEY", "9f0336b211425b2631f74685a3aee041e11da9617cdec4d7ee445f006c9b3954") + + if tiled_api_key: + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + + # Create container for motor angles + container_path = "motor_angle_log" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + # Just store the angle value as a simple array + angle_array = np.array([float(angle)]) + + # Use timestamp as unique key + unique_key = f"{motor.name}_{timestamp}" + + container.write_array( + angle_array, + metadata={ + "description": f"Motor angle reading from {motor.name}", + "timestamp": timestamp, + "motor_name": motor.name, + "angle_value": float(angle), + "datetime": datetime.now().isoformat(), + **(md or {}) + }, + key=unique_key + ) + print(f"āœ… Angle {angle} stored with key: {unique_key}") + + except Exception as e: + print(f"āš ļø Failed to save to Tiled: {e}") + + return angle + +# Advanced camera acquisition with Area Detector and Tiled storage +@parameter_annotation_decorator({ + "description": "Acquire image using Area Detector and save to Tiled", + "parameters": { + "camera": { + "description": "Required. Area Detector camera device", + "annotation": "typing.Any", + "convert_device_names": True, + }, + "save_dir": { + "description": "Directory path to save TIFF files", + "annotation": "str", + "default": "/tmp/camera_images", + }, + "image_name": { + "description": "Base name for the image file", + "annotation": "str", + "default": "scan_image", + } + } +}) +def acquire_area_detector_image(camera, save_dir="/tmp/camera_images", image_name="scan_image", *, md=None): + import os + import numpy as np + from datetime import datetime + import time + from bluesky.plan_stubs import sleep + from bluesky.plans import count + from tiled.client import from_uri + from PIL import Image + import tifffile + + # Generate timestamp + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Ensure save directory exists + os.makedirs(save_dir, exist_ok=True) + + try: + print("--- Staging Area Detector camera ---") + + # Configure file saving + camera.tiff.file_path.put(save_dir) + camera.tiff.file_template.put('%s%s_%d.tiff') + + # Get current file number and increment + current_number = camera.tiff.file_number.get() + current_number += 1 + filename = f'{image_name}_{timestamp}' + + camera.tiff.file_name.put(filename) + camera.tiff.file_number.put(current_number) + + # Full file path + filepath = os.path.join(save_dir, f"{filename}_{current_number}.tiff") + + print(f"šŸ“ø Capturing single image → {filepath}") + + # Use Bluesky count for single image (general approach) + print("šŸ“ø Acquiring single image...") + + # Try using the image plugin instead of TIFF plugin + print("šŸ“ø Using image plugin for acquisition...") + uid = yield from count([camera.image], num=1) + + print(f"šŸ“ø Acquisition completed with UID: {uid}") + + # Debug: Check what files exist in the directory + print(f"šŸ” Checking directory: {save_dir}") + if os.path.exists(save_dir): + files = os.listdir(save_dir) + print(f"šŸ“ Files in directory: {files}") + else: + print(f"āŒ Directory does not exist: {save_dir}") + + # Debug: Check camera's current file settings + print(f"šŸ“· Camera file path: {camera.tiff.file_path.get()}") + print(f"šŸ“· Camera file name: {camera.tiff.file_name.get()}") + print(f"šŸ“· Camera file number: {camera.tiff.file_number.get()}") + print(f"šŸ“· Camera file template: {camera.tiff.file_template.get()}") + + # Debug: Check TIFF plugin configuration + print(f"šŸ“· TIFF plugin enabled: {camera.tiff.enable.get()}") + print(f"šŸ“· TIFF auto save: {camera.tiff.auto_save.get()}") + print(f"šŸ“· TIFF file write mode: {camera.tiff.file_write_mode.get()}") + print(f"šŸ“· TIFF nd array port: {camera.tiff.nd_array_port.get()}") + print(f"šŸ“· TIFF array counter: {camera.tiff.array_counter.get()}") + + # Debug: Check if we're using the right detector + print(f"šŸ“· Available detectors: {list(camera.component_names)}") + print(f"šŸ“· Camera stage_sigs: {camera.stage_sigs}") + + # Wait for file to appear + print("ā³ Waiting for TIFF file...") + max_wait = 10 # seconds + wait_time = 0 + while not os.path.exists(filepath) and wait_time < max_wait: + yield from sleep(0.5) + wait_time += 0.5 + + # Check if local file exists + if os.path.exists(filepath): + print(f"āœ“ TIFF saved locally at {filepath}") + local_file_exists = True + else: + print(f"āŒ Local TIFF file not found: {filepath}") + local_file_exists = False + + # Alternative: Try to get image data directly from the UID + print(f"šŸ” Attempting to get image data from UID: {uid}") + try: + from databroker import Broker + db = Broker.named('temp') # Use temporary broker + + # Get the run data + run = db[uid] + print(f"šŸ“Š Run data retrieved: {run}") + + # Try to get image data + if hasattr(run, 'primary'): + data = run.primary.read() + print(f"šŸ“Š Primary data: {data}") + + # Check if we have image data + if 'camera_tiff_image' in data: + image_data = data['camera_tiff_image'] + print(f"šŸ“ø Image data shape: {image_data.shape}") + + # Save this directly to Tiled + print("šŸ“¤ Uploading image data directly to Tiled...") + try: + tiled_uri = os.getenv("TILED_URI", "http://localhost:8000") + tiled_api_key = os.getenv("TILED_API_KEY", "9f0336b211425b2631f74685a3aee041e11da9617cdec4d7ee445f006c9b3954") + + if tiled_api_key: + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + + # Create container + container_path = "measurement_images" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + # Store image data directly + tiled_key = f"{image_name}_{timestamp}_uid_{uid}" + container.write_array( + image_data.astype(np.float32), + metadata={ + "description": "Area Detector captured image (from UID)", + "timestamp": timestamp, + "uid": uid, + "camera_name": camera.name, + "local_file_exists": local_file_exists, + **(md or {}) + }, + key=tiled_key + ) + print(f"āœ… Image uploaded to Tiled with key: {tiled_key}") + + except Exception as e: + print(f"āš ļø Failed to upload to Tiled: {e}") + else: + print("āŒ No image data found in run") + + except Exception as e: + print(f"āš ļø Failed to get data from UID: {e}") + + # Convert to PNG for Tiled storage + png_path = filepath.replace(".tiff", ".png") + try: + # Load TIFF and convert to PNG + arr = tifffile.imread(filepath) + im = Image.fromarray(arr) + im.save(png_path, format="PNG") + print(f"āœ“ PNG created at {png_path}") + + # Save to Tiled + try: + tiled_uri = os.getenv("TILED_URI", "http://localhost:8000") + tiled_api_key = os.getenv("TILED_API_KEY", "9f0336b211425b2631f74685a3aee041e11da9617cdec4d7ee445f006c9b3954") + + if tiled_api_key: + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + + # Create container + container_path = "measurement_images" + if container_path not in tiled_client: + tiled_client.create_container(container_path) + container = tiled_client[container_path] + + # Load image array + image_array = np.array(Image.open(png_path)) + + # Store in Tiled + tiled_key = f"{image_name}_{timestamp}" + container.write_array( + image_array.astype(np.float32), + metadata={ + "description": "Area Detector captured image", + "timestamp": timestamp, + "filename": os.path.basename(filepath), + "png_filename": os.path.basename(png_path), + "camera_name": camera.name, + **(md or {}) + }, + key=tiled_key + ) + print(f"āœ… Image stored in Tiled with key: {tiled_key}") + + # Clean up TIFF file to save space + os.remove(filepath) + print(f"āœ“ Cleaned up TIFF file") + + except Exception as e: + print(f"āš ļø Failed to save to Tiled: {e}") + + except Exception as e: + print(f"āš ļø Failed to convert TIFF to PNG: {e}") + else: + print(f"āŒ File not found after waiting: {filepath}") + + finally: + print("--- Unstaging Area Detector camera ---") + #yield from unstage(camera) \ No newline at end of file diff --git a/take_measurement copy.py b/take_measurement copy.py new file mode 100644 index 0000000..8b49183 --- /dev/null +++ b/take_measurement copy.py @@ -0,0 +1,181 @@ +from ophyd import EpicsMotor, EpicsSignal +from ophyd.areadetector.plugins import PluginBase +from ophyd.areadetector import AreaDetector, ADComponent, ImagePlugin, JPEGPlugin, TIFFPlugin +from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.cam import CamBase +from bluesky import RunEngine +from databroker import Broker, temp +from bluesky.plans import scan, count +import bluesky.plan_stubs as bps +import numpy as np +from datetime import datetime +from pathlib import Path +from PIL import Image +import time, sys, os, subprocess +from collections import defaultdict +import tifffile +class PvaPlugin(PluginBase): + _suffix = 'Pva1:' + _plugin_type = 'NDPluginPva' + _default_read_attrs = ['enable'] + _default_configuration_attrs = ['enable'] + + array_callbacks = ADComponent(EpicsSignal, 'ArrayCallbacks') + +# Create RunEngine +RE = RunEngine({}) + +# Define the motor +motor = EpicsMotor('DMC01:A', name='motor') + +# Define the camera deviceca +class MyCamera(AreaDetector): + cam = ADComponent(AreaDetectorCam, 'cam1:') #Fixed the single camera issue? + image = ADComponent(ImagePlugin, 'image1:') + tiff = ADComponent(TIFFPlugin, 'TIFF1:') + pva = ADComponent(PvaPlugin, 'Pva1:') + +# Instantiate the camera +camera = MyCamera('13ARV1:', name='camera') +camera.wait_for_connection() + +file_path_container = [] + +#CAM OPTIONS +camera.stage_sigs[camera.cam.acquire] = 0 +camera.stage_sigs[camera.cam.image_mode] = 0 # single multiple continuous +camera.stage_sigs[camera.cam.trigger_mode] = 0 # internal external + +#IMAGE OPTIONS +camera.stage_sigs[camera.image.enable] = 1 # pva plugin +camera.stage_sigs[camera.image.queue_size] = 2000 + +#JPEG OPTIONS +camera.stage_sigs[camera.tiff.enable] = 1 +camera.stage_sigs[camera.tiff.auto_save] = 1 +camera.stage_sigs[camera.tiff.file_write_mode] = 0 # Or 'Single' works too +camera.stage_sigs[camera.tiff.nd_array_port] = 'SP1' +camera.stage_sigs[camera.tiff.auto_increment] = 1 #Doesn't work, must be ignored + +#PVA OPTIONS +camera.stage_sigs[camera.pva.enable] = 1 +camera.stage_sigs[camera.pva.blocking_callbacks] = 'No' +camera.stage_sigs[camera.pva.queue_size] = 2000 # or higher +camera.stage_sigs[camera.pva.nd_array_port] = 'SP1' +camera.stage_sigs[camera.pva.array_callbacks] = 0 # disable during scan + + + +def wait_for_file(filepath, timeout=5.0, poll_interval=0.1): + """Wait until a file appears on disk, or timeout.""" + start = time.time() + while not os.path.exists(filepath): + if time.time() - start > timeout: + raise TimeoutError(f"Timed out waiting for file: {filepath}") + time.sleep(poll_interval) + +def acquire(angle, save_dir): + #Requirements for image capturing + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + callbacks_signal = EpicsSignal('13ARV1:image1:EnableCallbacks', name='callbacks_signal') + acquire_signal = EpicsSignal('13ARV1:cam1:Acquire', name='acquire_signal') + + yield from bps.mv(callbacks_signal, 0) + max_retries = 50 + yield from bps.open_run() + camera.cam.array_callbacks.put(0, wait=True) + + print("\n--- Staging camera ---") + yield from bps.stage(camera) + + current_number = camera.tiff.file_number.get() + + yield from bps.mv(acquire_signal, 0) # Triggers a single image + + #for img_idx in range(NUM_IMAGES_PER_POS): + filename = f'scan_{timestamp}_pos_{angle}_shot' + current_number += 1 + filepath = os.path.join(save_dir, f"{filename}_{current_number}.tiff") + + yield from bps.mv(camera.tiff.file_name, filename) + yield from bps.mv(camera.tiff.file_number, current_number) + + for attempt in range(1, max_retries + 1): + + try: + print(f"[Attempt {attempt}] Capturing → {filepath}") + yield from bps.mv(acquire_signal, 1) # Triggers a single image + yield from bps.sleep(1) + + # Wait for file to appear + wait_for_file(filepath, timeout=5.0) + + print(f"āœ“ Image saved at {filepath}") + break # Exit retry loop if successful + + except TimeoutError: + print(f"--Timeout waiting for image at {filepath}") + if attempt == max_retries: + print(f"--Failed after {max_retries} attempts") + else: + print("↻ Retrying acquisition...") + yield from bps.mv(acquire_signal, 0) # Triggers a single image + yield from bps.sleep(0.5) + + print("\n--- Unstaging camera ---") + yield from bps.unstage(camera) + + yield from bps.close_run() + + file_path_container.clear() + file_path_container.append(f"{filepath}") + + return print(f"{filepath}") + +def convert_image_format(input_image_file: str, output_image_file: str): + arr = tifffile.imread(input_image_file) + + im = Image.fromarray(arr) + + im.save(output_image_file, format="PNG") + + return output_image_file + +if __name__ == "__main__": + # Run scan + print("Made it here") + try: + print("Starting script") + # File configuration + save_dir = '/home/user/tmpData/AI_scan/measurements/' + # Ensure the directory exists + print("Making it here") + os.makedirs(save_dir, exist_ok=True) + # Then set the path in EPICS + + angle = float(sys.argv[1]) + + + camera.tiff.file_path.put(save_dir) + camera.tiff.file_template.put('%s%s_%d.tiff') + + file_saved = RE(acquire(angle, save_dir)) + + png_path = file_path_container[0].replace(".tiff", ".png") + + image_path = convert_image_format(file_path_container[0], png_path) + + crop_box = (800, 800, 1600, 1500) # (left, upper, right, lower) + with Image.open(png_path) as img: + cropped = img.crop(crop_box) + cropped.save(png_path) # Overwrite or change name if desired + + os.remove(file_path_container[0]) + + + except KeyboardInterrupt: + print("\nScan interrupted by user") + RE.stop() + except Exception as e: + print(f"\nError during scan: {e}") + #RE.stop() \ No newline at end of file diff --git a/take_measurement.py b/take_measurement.py new file mode 100644 index 0000000..7d708a6 --- /dev/null +++ b/take_measurement.py @@ -0,0 +1,310 @@ +from ophyd import EpicsMotor, EpicsSignal +from ophyd.areadetector.plugins import PluginBase +from ophyd.areadetector import AreaDetector, ADComponent, ImagePlugin, JPEGPlugin, TIFFPlugin +from ophyd.areadetector.cam import AreaDetectorCam +from ophyd.areadetector.cam import CamBase +from bluesky import RunEngine +from databroker import Broker, temp +from bluesky.plans import scan, count +import bluesky.plan_stubs as bps +import numpy as np +from datetime import datetime +from pathlib import Path +from PIL import Image +import time, sys, os, subprocess +from collections import defaultdict +import tifffile + +# Manual Tiled writing imports (similar to store.txt approach) +try: + from tiled.client import from_uri + TILED_AVAILABLE = True +except ImportError: + TILED_AVAILABLE = False + print("Warning: Tiled client not available, running without Tiled integration") +class PvaPlugin(PluginBase): + _suffix = 'Pva1:' + _plugin_type = 'NDPluginPva' + _default_read_attrs = ['enable'] + _default_configuration_attrs = ['enable'] + + array_callbacks = ADComponent(EpicsSignal, 'ArrayCallbacks') + +# Create RunEngine +RE = RunEngine({}) + +# Set up manual Tiled connection (similar to store.txt approach) +tiled_client = None +if TILED_AVAILABLE: + try: + tiled_uri = "http://localhost:8000" + tiled_api_key = "ca6ae384c9f944e1465176b7e7274046b710dc7e2703dc33369f7c900d69bd64" + + tiled_client = from_uri(tiled_uri, api_key=tiled_api_key) + print(f"āœ“ Tiled client connected to {tiled_uri}") + except Exception as e: + print(f" Failed to connect to Tiled: {e}") + TILED_AVAILABLE = False + tiled_client = None + +# Define the motor +motor = EpicsMotor('DMC01:A', name='motor') + +# Define the camera deviceca +class MyCamera(AreaDetector): + cam = ADComponent(AreaDetectorCam, 'cam1:') #Fixed the single camera issue? + image = ADComponent(ImagePlugin, 'image1:') + tiff = ADComponent(TIFFPlugin, 'TIFF1:') + pva = ADComponent(PvaPlugin, 'Pva1:') + +# Instantiate the camera +camera = MyCamera('13ARV1:', name='camera') +camera.wait_for_connection() + +file_path_container = [] + +#CAM OPTIONS +camera.stage_sigs[camera.cam.acquire] = 0 +camera.stage_sigs[camera.cam.image_mode] = 0 # single multiple continuous +camera.stage_sigs[camera.cam.trigger_mode] = 0 # internal external + +#IMAGE OPTIONS +camera.stage_sigs[camera.image.enable] = 1 # pva plugin +camera.stage_sigs[camera.image.queue_size] = 2000 + +#JPEG OPTIONS +camera.stage_sigs[camera.tiff.enable] = 1 +camera.stage_sigs[camera.tiff.auto_save] = 1 +camera.stage_sigs[camera.tiff.file_write_mode] = 0 # Or 'Single' works too +camera.stage_sigs[camera.tiff.nd_array_port] = 'SP1' +camera.stage_sigs[camera.tiff.auto_increment] = 1 #Doesn't work, must be ignored + +#PVA OPTIONS +camera.stage_sigs[camera.pva.enable] = 1 +camera.stage_sigs[camera.pva.blocking_callbacks] = 'No' +camera.stage_sigs[camera.pva.queue_size] = 2000 # or higher +camera.stage_sigs[camera.pva.nd_array_port] = 'SP1' +camera.stage_sigs[camera.pva.array_callbacks] = 0 # disable during scan + + + +def wait_for_file(filepath, timeout=5.0, poll_interval=0.1): + """Wait until a file appears on disk, or timeout.""" + start = time.time() + while not os.path.exists(filepath): + if time.time() - start > timeout: + raise TimeoutError(f"Timed out waiting for file: {filepath}") + time.sleep(poll_interval) + +def acquire(angle, save_dir): + #Requirements for image capturing + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + callbacks_signal = EpicsSignal('13ARV1:image1:EnableCallbacks', name='callbacks_signal') + acquire_signal = EpicsSignal('13ARV1:cam1:Acquire', name='acquire_signal') + + yield from bps.mv(callbacks_signal, 0) + max_retries = 50 + + # Capture the run ID when opening the run + yield from bps.open_run() + + camera.cam.array_callbacks.put(0, wait=True) + + print("\n--- Staging camera ---") + yield from bps.stage(camera) + + current_number = camera.tiff.file_number.get() + + yield from bps.mv(acquire_signal, 0) # Triggers a single image + + #for img_idx in range(NUM_IMAGES_PER_POS): + filename = f'scan_{timestamp}_pos_{angle}_shot' + current_number += 1 + filepath = os.path.join(save_dir, f"{filename}_{current_number}.tiff") + + yield from bps.mv(camera.tiff.file_name, filename) + yield from bps.mv(camera.tiff.file_number, current_number) + + for attempt in range(1, max_retries + 1): + + try: + print(f"[Attempt {attempt}] Capturing → {filepath}") + yield from bps.mv(acquire_signal, 1) # Triggers a single image + yield from bps.sleep(1) + + # Wait for file to appear + wait_for_file(filepath, timeout=5.0) + + print(f"āœ“ Image saved at {filepath}") + break # Exit retry loop if successful + + except TimeoutError: + print(f"--Timeout waiting for image at {filepath}") + if attempt == max_retries: + print(f"--Failed after {max_retries} attempts") + else: + print("↻ Retrying acquisition...") + yield from bps.mv(acquire_signal, 0) # Triggers a single image + yield from bps.sleep(0.5) + + print("\n--- Unstaging camera ---") + yield from bps.unstage(camera) + + run_id = yield from bps.close_run() + + file_path_container.clear() + file_path_container.append(f"{filepath}") + + # Return both the filepath and run_id + return {"filepath": filepath, "run_id": run_id} + +def convert_image_format(input_image_file: str, output_image_file: str): + arr = tifffile.imread(input_image_file) + + im = Image.fromarray(arr) + + im.save(output_image_file, format="PNG") + + return output_image_file + +def save_image_to_tiled(png_path, run_id): + """Save image directly to Tiled using run_id as the key""" + if not TILED_AVAILABLE or tiled_client is None: + print(" Tiled not available for image upload") + return False + + try: + print(f" Uploading image to Tiled: {png_path}") + print(f" Using run_id as key: {run_id}") + + # Load PNG image as numpy array + png_img = Image.open(png_path) + png_array = np.array(png_img) + + # Store image directly with run_id as the key (no container, no complex metadata) + try: + tiled_client.write_array( + png_array.astype(np.float32), + key=str(run_id) + ) + print(f"āœ“ Image uploaded to Tiled with key: {run_id}") + return True + + except Exception as zarr_error: + if "zarr" in str(zarr_error).lower(): + print(f" ZARR storage failed, trying simple fallback...") + # Fallback: store a reference array + tiled_client.write_array( + np.array([png_array.shape[0], png_array.shape[1], 1]), + metadata={"image_path": png_path, "fallback_mode": True}, + key=str(run_id) + ) + print(f"āœ“ Image reference uploaded to Tiled with key: {run_id} (fallback mode)") + return True + else: + raise zarr_error + + except Exception as e: + print(f" Failed to upload image to Tiled: {e}") + return False + + + +if __name__ == "__main__": + # Run scan + try: + print("Starting script") + + # Check Tiled availability + if TILED_AVAILABLE and tiled_client: + print("āœ“ Tiled integration enabled") + + # Test Tiled connection and storage capabilities + try: + print("šŸ” Testing Tiled connection...") + test_container = "test_connection" + if test_container not in tiled_client: + tiled_client.create_container(test_container) + + # Try to write a simple test array + test_container_obj = tiled_client[test_container] + test_key = f"test_key_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}" + + # Clean up any existing test key first + if test_key in test_container_obj: + del test_container_obj[test_key] + + test_container_obj.write_array( + np.array([1, 2, 3]), + metadata={"test": "connection"}, + key=test_key + ) + print("āœ“ Tiled connection test successful") + + # Clean up test + if test_key in test_container_obj: + del test_container_obj[test_key] + + except Exception as e: + print(f" Tiled connection test failed: {e}") + if "zarr" in str(e).lower(): + print("šŸ’” ZARR dependency issue detected - will use fallback storage mode") + else: + print(" Running without Tiled integration") + # File configuration + save_dir = '/home/user/tmpData/AI_scan/measurements/' + # Ensure the directory exists + os.makedirs(save_dir, exist_ok=True) + # Then set the path in EPICS + + angle = float(sys.argv[1]) + + camera.tiff.file_path.put(save_dir) + camera.tiff.file_template.put('%s%s_%d.tiff') + + # Generate timestamp for Tiled uploads with microseconds for uniqueness + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f') + + # Get the run result which includes the run_id + run_result = RE(acquire(angle, save_dir)) + + run_id = sys.argv[2] + + if run_id is None: + print("Warning: Could not extract run_id from bluesky run, using timestamp as fallback") + print(f"Run result type: {type(run_result)}") + print(f"Plan result: {run_result.plan_result if hasattr(run_result, 'plan_result') else 'No plan_result attribute'}") + run_id = f"fallback_{timestamp}" + + print(f"Using run_id for Tiled uploads: {run_id}") + + png_path = file_path_container[0].replace(".tiff", ".png") + + image_path = convert_image_format(file_path_container[0], png_path) + + crop_box = (800, 800, 1600, 1500) # (left, upper, right, lower) + with Image.open(png_path) as img: + cropped = img.crop(crop_box) + cropped.save(png_path) # Overwrite or change name if desired + + time.sleep(10) + # Upload image directly to Tiled using run_id as key + if TILED_AVAILABLE and tiled_client: + print(f"\n Uploading image to Tiled with run_id: {run_id}...") + + # Upload the image directly using run_id as the key + save_image_to_tiled(png_path, run_id) + + print(f"āœ“ Image uploaded to Tiled with key: {run_id}") + else: + print(f" Skipping Tiled upload (Tiled not available)") + + os.remove(file_path_container[0]) + + + except KeyboardInterrupt: + print("\nScan interrupted by user") + RE.stop() + except Exception as e: + print(f"\nError during scan: {e}") + #RE.stop() \ No newline at end of file