From f3513207d92e077a8a5e919952c3682250e5afa1 Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Wed, 3 May 2023 18:11:27 +0200 Subject: [PATCH 1/3] feat: added otf sim --- ophyd_devices/epics/devices/X07MADevices.py | 18 ++-- ophyd_devices/sim/__init__.py | 1 + ophyd_devices/sim/sim.py | 106 +++++++++++++++++++- 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/epics/devices/X07MADevices.py b/ophyd_devices/epics/devices/X07MADevices.py index 841d275..2123a09 100644 --- a/ophyd_devices/epics/devices/X07MADevices.py +++ b/ophyd_devices/epics/devices/X07MADevices.py @@ -1,19 +1,19 @@ """ ophyd device classes for X07MA beamline """ -from collections import OrderedDict import time +import traceback +from collections import OrderedDict from typing import Any -from ophyd import Component as Cpt -from ophyd import FormattedComponent as FCpt - -from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind, PVPositioner, EpicsMotor -from ophyd.flyers import FlyerInterface -from ophyd.status import DeviceStatus, SubscriptionStatus -from ophyd.pv_positioner import PVPositionerComparator -import traceback from bec_utils import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd import FormattedComponent as FCpt +from ophyd import Kind, PVPositioner +from ophyd.flyers import FlyerInterface +from ophyd.pv_positioner import PVPositionerComparator +from ophyd.status import DeviceStatus, SubscriptionStatus logger = bec_logger.logger diff --git a/ophyd_devices/sim/__init__.py b/ophyd_devices/sim/__init__.py index 464d315..e180c00 100644 --- a/ophyd_devices/sim/__init__.py +++ b/ophyd_devices/sim/__init__.py @@ -1 +1,2 @@ from .sim import SynAxisMonitor, SynAxisOPAAS, SynFlyer, SynSignalRO, SynSLSDetector +from .sim_xtreme import SynXtremeOtf diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index d6f547f..2d85a1a 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -7,7 +7,7 @@ from typing import List import numpy as np from bec_utils import BECMessage, MessageEndpoints, bec_logger from ophyd import Component as Cpt -from ophyd import Device, DeviceStatus, PositionerBase, Signal +from ophyd import Device, DeviceStatus, OphydObject, PositionerBase, Signal from ophyd.sim import _ReadbackSignal, _SetpointSignal from ophyd.utils import LimitError, ReadOnlyError @@ -438,7 +438,109 @@ class SynFlyer(Device, PositionerBase): flyer = threading.Thread(target=produce_data, args=(self, metadata)) flyer.start() - # time.sleep(0.01) + +class SynController(OphydObject): + def on(self): + pass + + def off(self): + pass + + +class SynFlyerLamNI(Device, PositionerBase): + def __init__( + self, + *, + name, + readback_func=None, + value=0, + delay=0, + speed=1, + update_frequency=2, + precision=3, + parent=None, + labels=None, + kind=None, + device_manager=None, + **kwargs, + ): + if readback_func is None: + + def readback_func(x): + return x + + sentinel = object() + loop = kwargs.pop("loop", sentinel) + if loop is not sentinel: + warnings.warn( + f"{self.__class__} no longer takes a loop as input. " + "Your input will be ignored and may raise in the future", + stacklevel=2, + ) + self.sim_state = {} + self._readback_func = readback_func + self.delay = delay + self.precision = precision + self.tolerance = kwargs.pop("tolerance", 0.5) + self.device_manager = device_manager + + # initialize values + self.sim_state["readback"] = readback_func(value) + self.sim_state["readback_ts"] = ttime.time() + + super().__init__(name=name, parent=parent, labels=labels, kind=kind, **kwargs) + self.controller = SynController(name="SynController") + + def kickoff(self, metadata, num_pos, positions, exp_time: float = 0): + positions = np.asarray(positions) + + def produce_data(device, metadata): + buffer_time = 0.2 + elapsed_time = 0 + bundle = BECMessage.BundleMessage() + for ii in range(num_pos): + bundle.append( + BECMessage.DeviceMessage( + signals={ + "syn_flyer_lamni": { + "flyer_samx": {"value": positions[ii, 0], "timestamp": 0}, + "flyer_samy": {"value": positions[ii, 1], "timestamp": 0}, + } + }, + metadata={"pointID": ii, **metadata}, + ).dumps() + ) + ttime.sleep(exp_time) + elapsed_time += exp_time + if elapsed_time > buffer_time: + elapsed_time = 0 + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() + ) + bundle = BECMessage.BundleMessage() + device.device_manager.producer.set_and_publish( + MessageEndpoints.device_status(device.name), + BECMessage.DeviceStatusMessage( + device=device.name, + status=1, + metadata={"pointID": ii, **metadata}, + ).dumps(), + ) + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() + ) + device.device_manager.producer.set_and_publish( + MessageEndpoints.device_status(device.name), + BECMessage.DeviceStatusMessage( + device=device.name, + status=0, + metadata={"pointID": num_pos, **metadata}, + ).dumps(), + ) + print("done") + + flyer = threading.Thread(target=produce_data, args=(self, metadata)) + flyer.start() class SynAxisOPAAS(Device, PositionerBase): From 2bf57ed43331ae138e211f14ece8cfd9a1b79046 Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Tue, 9 May 2023 18:39:34 +0200 Subject: [PATCH 2/3] feat: added missing epics devices for xtreme --- ophyd_devices/epics/db/x07ma_database.yaml | 44 +++++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/ophyd_devices/epics/db/x07ma_database.yaml b/ophyd_devices/epics/db/x07ma_database.yaml index 366478c..1cf9116 100644 --- a/ophyd_devices/epics/db/x07ma_database.yaml +++ b/ophyd_devices/epics/db/x07ma_database.yaml @@ -102,14 +102,46 @@ signals: onFailure: retry status: {enabled: true} -manipulator: - description: 'Sample Manipulator' - deviceClass: X07MASampleManipulator - deviceConfig: {name: manipulator, prefix: 'X07MA-ES1-MAG:'} +sample_hor: + description: 'Horizontal sample position' + deviceClass: EpicsMotor + deviceConfig: {name: sample_hor, prefix: 'X07MA-ES1-AI:TRZS'} acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} onFailure: retry status: {enabled: true} +sample_vert: + description: 'Horizontal sample position' + deviceClass: EpicsMotor + deviceConfig: {name: sample_vert, prefix: 'X07MA-ES1-AI:TRY1'} + acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} + onFailure: retry + status: {enabled: true} + +sample_rot: + description: 'Horizontal sample position' + deviceClass: EpicsMotor + deviceConfig: {name: sample_rot, prefix: 'X07MA-ES1-AI:ROY1'} + acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} + onFailure: retry + status: {enabled: true} + +harmonic: + description: 'ID harmonic' + deviceClass: EpicsSignal + deviceConfig: {name: harmonic, prefix: 'X07MA-ID:HARMONIC'} + acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} + onFailure: retry + status: {enabled: true} + +# manipulator: +# description: 'Sample Manipulator' +# deviceClass: X07MASampleManipulator +# deviceConfig: {name: manipulator, prefix: 'X07MA-ES1-MAG:'} +# acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} +# onFailure: retry +# status: {enabled: true} + temperature: description: 'Temperature controller' deviceClass: X07MATemperatureController @@ -118,10 +150,10 @@ temperature: onFailure: retry status: {enabled: true} -TControl: +tcontrol: description: 'Automatic temperature control' deviceClass: X07MAAutoTemperatureControl - deviceConfig: {name: 'TControl', prefix: 'X07MA-ES1-TEMP:'} + deviceConfig: {name: 'tcontrol', prefix: 'X07MA-ES1-TEMP:'} acquisitionConfig: {acquisitionGroup: monitor, readoutPriority: baseline, schedule: sync} onFailure: retry status: {enabled: true} From 5a7f8ac40781f5ccf48b6ca94a569665592dc15b Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Tue, 9 May 2023 18:42:18 +0200 Subject: [PATCH 3/3] fix: added missing file --- ophyd_devices/sim/sim_xtreme.py | 241 ++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 ophyd_devices/sim/sim_xtreme.py diff --git a/ophyd_devices/sim/sim_xtreme.py b/ophyd_devices/sim/sim_xtreme.py new file mode 100644 index 0000000..47e8f0a --- /dev/null +++ b/ophyd_devices/sim/sim_xtreme.py @@ -0,0 +1,241 @@ +import threading +import time + +import numpy as np +from ophyd import Component as Cpt +from ophyd import Device, Kind, Signal +from ophyd.flyers import FlyerInterface +from ophyd.ophydobj import Kind +from ophyd.status import DeviceStatus, SubscriptionStatus +from ophyd.utils import ReadOnlyError + + +class SynSetpoint(Signal): + def __init__( + self, + name, + *, + value=0, + dtype=float, + timestamp=None, + parent=None, + labels=None, + kind=Kind.hinted, + tolerance=None, + rtolerance=None, + metadata=None, + cl=None, + attr_name="", + auto_monitor=False, + ): + self._dtype = dtype + if self._dtype == str and value == 0: + value = "" + super().__init__( + name=name, + value=value, + timestamp=timestamp, + parent=parent, + labels=labels, + kind=kind, + tolerance=tolerance, + rtolerance=rtolerance, + metadata=metadata, + cl=cl, + attr_name=attr_name, + ) + + def put(self, value, *, timestamp=None, force=False): + old_val = self._readback + self._readback = self._dtype(value) + self._run_subs( + sub_type="value", + old_value=old_val, + value=self._readback, + timestamp=time.time(), + ) + + def get(self): + return self._readback + + def describe(self): + res = super().describe() + # There should be only one key here, but for the sake of generality.... + for k in res: + res[k]["precision"] = self.parent.precision + return res + + +class SynData(Signal): + _default_sub = "value" + + def __init__( + self, + *, + name, + value=0, + timestamp=None, + parent=None, + labels=None, + kind=Kind.hinted, + tolerance=None, + rtolerance=None, + metadata=None, + cl=None, + attr_name="", + auto_monitor=False, + ): + super().__init__( + name=name, + value=value, + timestamp=timestamp, + parent=parent, + labels=labels, + kind=kind, + tolerance=tolerance, + rtolerance=rtolerance, + metadata=metadata, + cl=cl, + attr_name=attr_name, + ) + self._reset_data() + + def _reset_data(self): + self._readback = np.array([]) + + def get(self): + return self._readback + + def append(self, val: float): + self._readback = np.append(self._readback, val) + + def describe(self): + res = super().describe() + # There should be only one key here, but for the sake of + # generality.... + for k in res: + res[k]["precision"] = self.parent.precision + return res + + @property + def timestamp(self): + """Timestamp of the readback value""" + return time.time() + + def put(self, value, *, timestamp=None, force=False): + raise ReadOnlyError("The signal {} is readonly.".format(self.name)) + + def set(self, value, *, timestamp=None, force=False): + raise ReadOnlyError("The signal {} is readonly.".format(self.name)) + + +class SynXtremeOtf(FlyerInterface, Device): + """ + PGM on-the-fly scan + """ + + SUB_VALUE = "value" + SUB_FLYER = "flyer" + _default_sub = SUB_VALUE + + e1 = Cpt(SynSetpoint, kind=Kind.config) + e2 = Cpt(SynSetpoint, kind=Kind.config) + time = Cpt(SynSetpoint, kind=Kind.config) + folder = Cpt(SynSetpoint, dtype=str, value="", kind=Kind.config) + file = Cpt(SynSetpoint, dtype=str, value="", kind=Kind.config) + acquire = Cpt(SynSetpoint, auto_monitor=True) + edata = Cpt(SynData, kind=Kind.hinted, auto_monitor=True) + data = Cpt(SynData, kind=Kind.hinted, auto_monitor=True) + idata = Cpt(SynData, kind=Kind.hinted, auto_monitor=True) + fdata = Cpt(SynData, kind=Kind.hinted, auto_monitor=True) + count = Cpt(SynData, kind=Kind.omitted, auto_monitor=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._start_time = 0 + self.acquire.subscribe(self._update_status, run=False) + self.count.subscribe(self._update_data, run=False) + self._data_event = threading.Event() + self.precision = 3 + + def kickoff(self): + self._start_time = time.time() + self.acquire.put(1) + status = DeviceStatus(self) + status.set_finished() + return status + + def complete(self): + def check_value(*, old_value, value, **kwargs): + return old_value == 1 and value == 0 + + status = SubscriptionStatus(self.acquire, check_value, event_type=self.acquire.SUB_VALUE) + return status + + def collect(self): + data = {"time": self._start_time, "data": {}, "timestamps": {}} + for attr in ("edata", "data", "idata", "fdata"): + obj = getattr(self, attr) + data["data"][obj.name] = obj.get() + data["timestamps"][obj.name] = obj.timestamp + + return data + + def describe_collect(self): + desc = {} + for attr in ("edata", "data", "idata", "fdata"): + desc.update(getattr(self, attr).describe()) + return desc + + def _update_status(self, *, old_value, value, **kwargs): + if old_value == 1 and value == 0: + self._done_acquiring() + return + if old_value == 0 and value == 1: + threading.Thread(target=self._start_acquiring, daemon=True).start() + + def _reset_data(self): + for entry in ("edata", "data", "idata", "fdata"): + getattr(self, entry)._reset_data() + self.count._readback = 0 + self._data_event.clear() + + def _populate_data(self): + self._reset_data() + while not self._data_event.is_set(): + for entry in ("edata", "data", "idata", "fdata"): + getattr(self, entry).append(np.random.rand()) + self.count._readback = len(self.edata.get()) + self.count._run_subs( + sub_type="value", + old_value=self.count._readback - 1, + value=self.count._readback, + timestamp=time.time(), + ) + time.sleep(0.2) + self._data_event.clear() + + def _start_acquiring(self): + threading.Thread(target=self._populate_data, daemon=True).start() + timeout_event = threading.Event() + flag = timeout_event.wait(self.time.get()) + if not flag: + self._data_event.set() + self.acquire.put(0) + + def _update_data(self, value, **kwargs): + if value == 0: + return + data = self.collect() + self._run_subs(sub_type=self.SUB_FLYER, value=data) + + +if __name__ == "__main__": + obj = SynXtremeOtf(name="otf") + status = obj.time.set(4) + status.wait() + status = obj.kickoff() + status.wait() + while obj.acquire.get(): + time.sleep(0.2) + print("done")