From f194bef45336ceddd28c6cad84310211d6d77e57 Mon Sep 17 00:00:00 2001 From: gbischof Date: Thu, 18 May 2023 14:11:45 -0400 Subject: [PATCH 1/3] TernaryDevice --- ophyd/ternary.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 ophyd/ternary.py diff --git a/ophyd/ternary.py b/ophyd/ternary.py new file mode 100644 index 000000000..232045be1 --- /dev/null +++ b/ophyd/ternary.py @@ -0,0 +1,86 @@ +from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, FormattedComponent + + +class TernaryDevice(Device): + """ + A general purpose ophyd device with set and reset signals, and a state signal + with 3 posible signals. + + Example + ------- + class StateEnum(Enum): + In = True + Out = False + Unknown = None + + class ExampleTernary(TernaryDevice): + def __init__(self, index, *args, **kwargs): + super().__init__( + *args, + name=f"Filter{index}", + set_name=f"TernaryArray:device{index}_set", + reset_name=f"TernaryArray:device{index}_reset", + state_name=f"TernaryArray:device{index}_rbv", + state_enum=StateEnum, + **kwargs, + ) + ternary1 = ExampleTernary(1) + """ + + set_cmd = FormattedComponent(EpicsSignal, "{self._set_name}") + reset_cmd = FormattedComponent(EpicsSignal, "{self._reset_name}") + state_rbv = FormattedComponent(EpicsSignalRO, "{self._state_name}", string=True) + + def __init__( + self, *args, set_name, reset_name, state_name, state_enum, **kwargs + ) -> None: + self._state_enum = state_enum + self._set_name = set_name + self._reset_name = reset_name + self._state_name = state_name + self._state = None + super().__init__(*args, **kwargs) + + def set(self, value=True): + if value not in {True, False, 0, 1}: + raise ValueError("value must be one of the following: True, False, 0, 1") + + target_value = bool(value) + + st = DeviceStatus(self) + + # If the device already has the requested state, return a finished status. + if self._state == bool(value): + st._finished() + return st + self._set_st = st + + def state_cb(value, timestamp, **kwargs): + """ + Updates self._state and checks if the status should be marked as finished. + """ + try: + self._state = self._state_enum[value].value + except KeyError: + raise ValueError(f"self._state_enum does not contain value: {value}") + if self._state == target_value: + self._set_st = None + self.state_rbv.clear_sub(state_cb) + st._finished() + + # Subscribe the callback to the readback signal. + # The callback will be called each time the PV value changes. + self.state_rbv.subscribe(state_cb) + + # Write to the signal. + if value: + self.set_cmd.set(1) + else: + self.reset_cmd.set(1) + return st + + def reset(self): + self.set(False) + + def get(self): + return self._state From e11ad3e7bd15f9877acde4c0688c6c3ed6641c7c Mon Sep 17 00:00:00 2001 From: gbischof Date: Thu, 18 May 2023 14:21:45 -0400 Subject: [PATCH 2/3] add ArrayDevice and test --- ophyd/array.py | 60 +++++++++++++ ophyd/tests/test_array.py | 175 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 ophyd/array.py create mode 100644 ophyd/tests/test_array.py diff --git a/ophyd/array.py b/ophyd/array.py new file mode 100644 index 000000000..f5fc48bd2 --- /dev/null +++ b/ophyd/array.py @@ -0,0 +1,60 @@ +from ophyd import Device, DeviceStatus + + +def ArrayDevice(devices, *args, **kwargs): + """ + A function, that behaves like a class init, that dynamically creates an + ArrayDevice class. This is needed to set class attributes before the init. + Adding devices in the init can subvert important ophyd code that + manages sub devices. + + Parameters + ---------- + devices: interable + An iterable of devices with the same type. + + Example + ------- + array_device = ArrayDevice([ExampleTernary(i) for i in range(10)], name='array_device') + """ + + class _ArrayDeviceBase(Device): + """ + An ophyd.Device that is an array of devices. + + The set method takes a list of values. + the get method returns a list of values. + Parameters + ---------- + devices: iterable + The array of ophyd devices. + """ + def set(self, values): + if len(values) != len(self.devices): + raise ValueError( + f"The number of values ({len(values)}) must match " + f"the number of devices ({len(self.devices)})" + ) + + # If the device already has the requested state, return a finished status. + diff = [self.devices[i].get() != value for i, value in enumerate(values)] + if not any(diff): + return DeviceStatus(self)._finished() + + # Set the value of each device and return a union of the statuses. + statuses = [self.devices[i].set(value) for i, value in enumerate(values)] + st = reduce(lambda a, b: a & b, statuses) + return st + + def reset(self): + self.set([0 for i in range(len(self.devices))]) + + def get(self): + return [device.get() for device in self.devices] + + types = {type(device) for device in devices} + if len(types) != 1: + raise TypeError("All devices must have the same type") + + _ArrayDevice = type('ArrayDevice', (_ArrayDeviceBase,), {'devices': devices}) + return _ArrayDevice(*args, **kwargs) diff --git a/ophyd/tests/test_array.py b/ophyd/tests/test_array.py new file mode 100644 index 000000000..585df4036 --- /dev/null +++ b/ophyd/tests/test_array.py @@ -0,0 +1,175 @@ +import asyncio +import subprocess +import sys +import time + +from enum import Enum +from functools import partial, reduce +from collections import OrderedDict +from caproto.server import PVGroup, ioc_arg_parser, pvproperty, run +from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, FormattedComponent +from ophyd.array import ArrayDevice +from ophyd.ternary import TernaryDevice + + +class StateEnum(Enum): + In = True + Out = False + Unknown = None + + +class TernaryDeviceSim: + """ + A device with three states. + + Parameters + ---------- + delay: float, optional + The time it takes for the device to change from state-0 to state-1. + """ + + def __init__(self, delay=0.5): + self._delay = delay + self._state = False + + async def set(self): + if not self._state: + self._state = None + await asyncio.sleep(self._delay) + self._state = True + + async def reset(self): + if self._state or self._state is None: + self._state = None + await asyncio.sleep(self._delay) + self._state = False + + @property + def state(self): + return self._state + + +class TernaryArrayIOC(PVGroup): + """ + Example IOC that has an array of TernaryDevices. + + Parameters + ---------- + count: integer + The number of devices in the array. + """ + + def __init__(self, count=10, *args, **kwargs): + self._devices = [TernaryDeviceSim() for i in range(count)] + + # Dynamically setup the pvs. + for i in range(count): + # Create the set pv. + setattr( + self, + f"device{i}_set", + pvproperty(value=0, dtype=int, name=f"device{i}_set"), + ) + + # Create the set putter. + partial_set = partial(self.set_putter, i) + partial_set.__name__ = f"set_putter{i}" + getattr(self, f"device{i}_set").putter(partial_set) + + # Create the reset pv. + setattr( + self, + f"device{i}_reset", + pvproperty(value=0, dtype=int, name=f"device{i}_reset"), + ) + + # Create the reset putter. + partial_reset = partial(self.reset_putter, i) + partial_reset.__name__ = f"reset_putter{i}" + getattr(self, f"device{i}_reset").putter(partial_reset) + + # Create the readback pv. + setattr( + self, + f"device{i}_rbv", + pvproperty(value="Unknown", dtype=str, name=f"device{i}_rbv"), + ) + + # Create the readback scan. + partial_scan = partial(self.general_scan, i) + partial_scan.__name__ = f"scan{i}" + getattr(self, f"device{i}_rbv").scan(period=0.1)(partial_scan) + + # Unfortunate hack to register the late pvs. + self.__dict__["_pvs_"] = OrderedDict(PVGroup.find_pvproperties(self.__dict__)) + super().__init__(*args, **kwargs) + + async def set_putter(self, index, group, instance, value): + if value: + await self._devices[index].set() + + async def reset_putter(self, index, group, instance, value): + if value: + await self._devices[index].reset() + + async def general_scan(self, index, group, instance, async_lib): + # A hacky way to write to the pv. + await self.pvdb[f"{self.prefix}device{index}_rbv"].write( + StateEnum(self._devices[index].state).name + ) + # This is the normal way to do this, but it doesn't work correctly for this example. + # await getattr(self, f'device{index}_rbv').write(StateEnum(self._devices[index].state).name) + + +class ExampleTernary(TernaryDevice): + """ + This class is an example about how to create a TernaryDevice specialization + for a specific implementation. + """ + + def __init__(self, index, *args, **kwargs): + super().__init__( + *args, + name=f"Filter{index}", + set_name=f"TernaryArray:device{index}_set", + reset_name=f"TernaryArray:device{index}_reset", + state_name=f"TernaryArray:device{index}_rbv", + state_enum=StateEnum, + **kwargs, + ) + + +array_device = ArrayDevice([ExampleTernary(i) for i in range(10)], name='array_device') + + +def start_test_ioc(): + ioc = TernaryArrayIOC(prefix='TernaryArray:') + print("Prefix =", "TernaryArray:") + print("PVs:", list(ioc.pvdb)) + run(ioc.pvdb) + + +def test_ioc(f): + """ + Decorator that starts a test ioc using subproccess, + calls your function and then cleans up the process. + """ + def wrap(): + try: + ps = subprocess.Popen([sys.executable, '-c', 'from ophyd.tests.test_array import start_test_ioc; start_test_ioc()']) + time.sleep(5) + f() + finally: + ps.kill() + return wrap + + +@test_ioc +def test_arraydevice(): + arraydevice = ArrayDevice([ExampleTernary(i) for i in range(10)], + name='arraydevice') + values = [1,1,1,0,0,0,1,1,1,0] + arraydevice.set(values) + time.sleep(1) + print(arraydevice.get()) + assert arraydevice.get() == values From 47ff6291581f065185ab4a4e8c630878acfac27a Mon Sep 17 00:00:00 2001 From: gbischof Date: Thu, 18 May 2023 14:43:43 -0400 Subject: [PATCH 3/3] touch ups --- ophyd/array.py | 1 + ophyd/tests/.coverage | Bin 0 -> 69632 bytes ophyd/tests/test_array.py | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 ophyd/tests/.coverage diff --git a/ophyd/array.py b/ophyd/array.py index f5fc48bd2..7bdbf16b0 100644 --- a/ophyd/array.py +++ b/ophyd/array.py @@ -1,3 +1,4 @@ +from functools import reduce from ophyd import Device, DeviceStatus diff --git a/ophyd/tests/.coverage b/ophyd/tests/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..813f4db410b903dcce721ec728b5b969295e616e GIT binary patch literal 69632 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 gvSh9lKf2kAl