From 96a5f1b86a17f099a935c460baf397d9c93bc612 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 9 Feb 2024 19:21:28 +0100 Subject: [PATCH 01/77] refactor: refactored SimMonitor and SimCamera --- ophyd_devices/sim/sim.py | 270 +++++++++++---------------------------- 1 file changed, 74 insertions(+), 196 deletions(-) diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index eb9156d..a0dd91f 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -1,4 +1,3 @@ -from collections import defaultdict import os import threading import time as ttime @@ -9,10 +8,12 @@ from bec_lib import MessageEndpoints, bec_logger, messages from ophyd import Component as Cpt from ophyd import Device, DeviceStatus, Kind from ophyd import DynamicDeviceComponent as Dcpt -from ophyd import OphydObject, PositionerBase, Signal -from ophyd.sim import EnumSignal, SynSignal -from ophyd.utils import LimitError, ReadOnlyError +from ophyd import PositionerBase, Signal +from ophyd.sim import SynSignal +from ophyd.utils import LimitError +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor +from ophyd_devices.sim.sim_additional_devices import DummyController from ophyd_devices.sim.sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal @@ -27,21 +28,20 @@ class SimMonitor(Device): """ A simulated device mimic any 1D Axis (position, temperature, beam). - Readback functionality can be configured + It's readback is a computed signal, which is configurable by the user and from the command line. + The corresponding simulation class is sim_cls=SimulatedDataMonitor, more details on defaults within the simulation class. + + >>> monitor = SimMonitor(name="monitor") Parameters ---------- - name : string, keyword only - value : object, optional - The initial value. Default is 0. - delay : number, optional - Simulates how long it takes the device to "move". Default is 0 seconds. - precision : integer, optional - Digits of precision. Default is 3. - parent : Device, optional - Used internally if this Signal is made part of a larger Device. - kind : a member the Kind IntEnum (or equivalent integer), optional - Default is Kind.normal. See Kind for options. + name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device. + precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits. + sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details. + parent : Parent device, optional, is used internally if this signal/device is part of a larger device. + kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options. + device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically. + """ USER_ACCESS = ["sim"] @@ -55,130 +55,43 @@ class SimMonitor(Device): def __init__( self, - *, name, - value=0, - delay=0, - precision=3, - tolerance: float = 0.5, + *, + precision: int = 3, sim_init: dict = None, parent=None, - labels=None, kind=None, device_manager=None, **kwargs, ): - self.delay = delay self.precision = precision - self.tolerance = tolerance self.init_sim_params = sim_init self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) - super().__init__(name=name, parent=parent, labels=labels, kind=kind, **kwargs) + super().__init__(name=name, parent=parent, kind=kind, **kwargs) self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) self.readback.name = self.name -class SynGaussBEC(Device): - """ - Evaluate a point on a Gaussian based on the value of a motor. +class SimCamera(Device): + """A simulated device mimic any 2D camera. + + It's image is a computed signal, which is configurable by the user and from the command line. + The corresponding simulation class is sim_cls=SimulatedDataCamera, more details on defaults within the simulation class. + + >>> camera = SimCamera(name="camera") Parameters ---------- - name : string - motor : Device - motor_field : string - center : number - center of peak - Imax : number - max intensity of peak - sigma : number, optional - Default is 1. - noise : {'poisson', 'uniform', None}, optional - Add noise to the gaussian peak. - noise_multiplier : float, optional - Only relevant for 'uniform' noise. Multiply the random amount of - noise by 'noise_multiplier' - random_state : numpy random state object, optional - np.random.RandomState(0), to generate random number with given seed + name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device. + precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits. + sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details. + parent : Parent device, optional, is used internally if this signal/device is part of a larger device. + kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options. + device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically. - Example - ------- - motor = SynAxis(name='motor') - det = SynGauss('det', motor, 'motor', center=0, Imax=1, sigma=1) """ - val = Cpt(ComputedReadOnlySignal, value=0, kind=Kind.hinted) - Imax = Cpt(Signal, value=10, kind=Kind.config) - center = Cpt(Signal, value=0, kind=Kind.config) - sigma = Cpt(Signal, value=1, kind=Kind.config) - motor = Cpt(Signal, value="samx", kind=Kind.config) - noise = Cpt( - EnumSignal, - value="none", - kind=Kind.config, - enum_strings=("none", "poisson", "uniform"), - ) - noise_multiplier = Cpt(Signal, value=1, kind=Kind.config) - - def __init__(self, name, *, device_manager=None, random_state=None, **kwargs): - self.device_manager = device_manager - set_later = {} - for k in ("sigma", "noise", "noise_multiplier"): - v = kwargs.pop(k, None) - if v is not None: - set_later[k] = v - self.sim_state = defaultdict(lambda: {}) - super().__init__(name=name, **kwargs) - self.sim_state[self.name] = self.sim_state.pop(self.val.name, None) - self.val.name = self.name - - self.random_state = random_state or np.random - self.precision = 3 - - for k, v in set_later.items(): - getattr(self, k).put(v) - - def _compute_sim_state(self, signal_name: str) -> None: - try: - m = self.device_manager.devices[self.motor.get()].obj.read()[self.motor.get()]["value"] - # we need to do this one at a time because - # - self.read() may be screwed with by the user - # - self.get() would cause infinite recursion - Imax = self.Imax.get() - center = self.center.get() - sigma = self.sigma.get() - noise = self.noise.get() - noise_multiplier = self.noise_multiplier.get() - v = Imax * np.exp(-((m - center) ** 2) / (2 * sigma**2)) - if noise == "poisson": - v = int(self.random_state.poisson(np.round(v), 1)) - elif noise == "uniform": - v += self.random_state.uniform(-1, 1) * noise_multiplier - self.sim_state[signal_name]["value"] = v - self.sim_state[signal_name]["timestamp"] = ttime.time() - except Exception as exc: - logger.warning(f"Failed to compute sim state with exception {exc}") - self.sim_state[signal_name]["value"] = 0 - self.sim_state[signal_name]["timestamp"] = ttime.time() - - def get(self, *args, **kwargs): - self.sim_state["readback"] = self._compute() - self.sim_state["readback_ts"] = ttime.time() - return self.val.get() - - -class _SLSDetectorConfigSignal(Signal): - def put(self, value, *, timestamp=None, force=False): - self._readback = value - self.parent.sim_state[self.name] = value - - def get(self): - self._readback = self.parent.sim_state[self.name] - return self.parent.sim_state[self.name] - - -class SimCamera(Device): USER_ACCESS = ["sim"] sim_cls = SimulatedDataCamera @@ -189,12 +102,8 @@ class SimCamera(Device): exp_time = Cpt(SetableSignal, name="exp_time", value=1, kind=Kind.config) file_path = Cpt(SetableSignal, name="file_path", value="", kind=Kind.config) - file_pattern = Cpt(SetableSignal, name="file_pattern", value="", kind=Kind.config) frames = Cpt(SetableSignal, name="frames", value=1, kind=Kind.config) - burst = Cpt(SetableSignal, name="burst", value=1, kind=Kind.config) - save_file = Cpt(SetableSignal, name="save_file", value=False, kind=Kind.config) - # image shape, only adjustable via config. image_shape = Cpt(SetableSignal, name="image_shape", value=SHAPE, kind=Kind.config) image = Cpt( ComputedReadOnlySignal, @@ -205,8 +114,8 @@ class SimCamera(Device): def __init__( self, - *, name, + *, kind=None, parent=None, sim_init: dict = None, @@ -219,10 +128,17 @@ class SimCamera(Device): super().__init__(name=name, parent=parent, kind=kind, **kwargs) self._stopped = False - self.file_name = "" - self.metadata = {} + self.scaninfo = None + self._update_scaninfo() - def trigger(self): + def trigger(self) -> DeviceStatus: + """Trigger the camera to acquire images. + + This method can be called from BEC during a scan. It will acquire images and send them to BEC. + Whether the trigger is send from BEC is determined by the softwareTrigger argument in the device config. + + Here, we also run a callback on SUB_MONITOR to send the image data the device_monitor endpoint in BEC. + """ status = DeviceStatus(self) self.subscribe(status._finished, event_type=self.SUB_ACQ_DONE, run=False) @@ -230,7 +146,6 @@ class SimCamera(Device): def acquire(): try: for _ in range(self.burst.get()): - # Send data for each trigger self._run_subs(sub_type=self.SUB_MONITOR, value=self.image.get()) if self._stopped: raise DeviceStop @@ -243,28 +158,41 @@ class SimCamera(Device): threading.Thread(target=acquire, daemon=True).start() return status - def stage(self) -> list[object]: - """Stage the camera - - Receive scan message from REDIS first, extract relevant scan data, - and set all signals for the scan, e.g. scan_number, file_name, frames, etc. + def _update_scaninfo(self) -> None: + """Update scaninfo from BecScaninfoMixing + This depends on device manager and operation/sim_mode """ - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - scan_msg = messages.ScanStatusMessage.loads(msg) - self.metadata = { - "scanID": scan_msg.content["scanID"], - "RID": scan_msg.content["info"]["RID"], - "queueID": scan_msg.content["info"]["queueID"], - } - scan_number = scan_msg.content["info"]["scan_number"] - self.frames.set(scan_msg.content["info"]["num_points"]) - self.file_name = os.path.join( - self.file_path.get(), self.file_pattern.get().format(scan_number) + self.scaninfo = BecScaninfoMixin(self.device_manager) + + def stage(self) -> list[object]: + """Stage the camera for upcoming scan + + This method is called from BEC in preparation of a scan. + It receives metadata about the scan from BEC, + compiles it and prepares the camera for the scan. + + FYI: No data is written to disk in the simulation, but upon each trigger it + is published to the device_monitor endpoint in REDIS. + """ + if self._staged: + return super().stag e() + self.scaninfo.load_scan_metadata() + self.file_path.set( + os.path.join( + self.file_path.get(), self.file_pattern.get().format(self.scaninfo.scan_number) + ) ) + self.frames = self.scaninfo.num_points * self.scaninfo.frames_per_trigger + self.exp_time = self.scaninfo.exp_time self._stopped = False return super().stage() def _send_data_to_bec(self) -> None: + """Send data to BEC. + + Reads out all signals of type Kind.config, and send them to BEC. + Happens once for each scan. + """ config_readout = { signal.item.name: signal.item.get() for signal in self.walk_signals() @@ -272,7 +200,7 @@ class SimCamera(Device): } signals = {"config": config_readout, "data": self.file_name} - msg = messages.DeviceMessage(signals=signals, metadata=self.metadata) + msg = messages.DeviceMessage(signals=signals, metadata=self.scaninfo.metadata) self.device_manager.producer.set_and_publish( MessageEndpoints.device_read(self.name), msg.dumps() ) @@ -289,53 +217,11 @@ class SimCamera(Device): return super().unstage() def stop(self, *, success=False): + """Stop the device""" self._stopped = True super().stop(success=success) -class DummyController: - USER_ACCESS = [ - "some_var", - "controller_show_all", - "_func_with_args", - "_func_with_args_and_kwargs", - "_func_with_kwargs", - "_func_without_args_kwargs", - ] - some_var = 10 - another_var = 20 - - def on(self): - self._connected = True - - def off(self): - self._connected = False - - def _func_with_args(self, *args): - return args - - def _func_with_args_and_kwargs(self, *args, **kwargs): - return args, kwargs - - def _func_with_kwargs(self, **kwargs): - return kwargs - - def _func_without_args_kwargs(self): - return None - - def controller_show_all(self): - """dummy controller show all - - Raises: - in: _description_ - LimitError: _description_ - - Returns: - _type_: _description_ - """ - print(self.some_var) - - class DummyControllerDevice(Device): USER_ACCESS = ["controller"] @@ -439,14 +325,6 @@ class SynFlyer(Device, PositionerBase): flyer.start() -class SynController(OphydObject): - def on(self): - pass - - def off(self): - pass - - class SynFlyerLamNI(Device, PositionerBase): def __init__( self, From 6fb912bd9c4453d4474dd0dc5a94676988f356bc Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 9 Feb 2024 21:59:18 +0100 Subject: [PATCH 02/77] refactor: moved bec_scaninfo_mixin to ophyd_devices/utils --- ophyd_devices/epics/devices/psi_delay_generator_base.py | 2 +- ophyd_devices/epics/devices/psi_detector_base.py | 2 +- ophyd_devices/{epics/devices => utils}/bec_scaninfo_mixin.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) rename ophyd_devices/{epics/devices => utils}/bec_scaninfo_mixin.py (96%) diff --git a/ophyd_devices/epics/devices/psi_delay_generator_base.py b/ophyd_devices/epics/devices/psi_delay_generator_base.py index 193a591..76641c6 100644 --- a/ophyd_devices/epics/devices/psi_delay_generator_base.py +++ b/ophyd_devices/epics/devices/psi_delay_generator_base.py @@ -11,7 +11,7 @@ from ophyd.pseudopos import ( ) from ophyd.device import Staged -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.utils import bec_utils from bec_lib import bec_logger diff --git a/ophyd_devices/epics/devices/psi_detector_base.py b/ophyd_devices/epics/devices/psi_detector_base.py index 65f6518..176f82c 100644 --- a/ophyd_devices/epics/devices/psi_detector_base.py +++ b/ophyd_devices/epics/devices/psi_detector_base.py @@ -6,7 +6,7 @@ from bec_lib.device import DeviceStatus from bec_lib.file_utils import FileWriterMixin from ophyd import Device from ophyd.device import Staged -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.utils import bec_utils diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py similarity index 96% rename from ophyd_devices/epics/devices/bec_scaninfo_mixin.py rename to ophyd_devices/utils/bec_scaninfo_mixin.py index 1bd3985..3c20646 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -103,7 +103,10 @@ class BecScaninfoMixin: def get_username(self) -> str: """Get username""" if not self.sim_mode: - return self.device_manager.producer.get(MessageEndpoints.account()).decode() + msg = self.device_manager.producer.get(MessageEndpoints.account()) + if not isinstance(msg, str): + return os.getlogin() + return self.device_manager.producer.get(MessageEndpoints.account()) return os.getlogin() def load_scan_metadata(self) -> None: From 2225dafb7438f576d7033e220910b4cf8769fd33 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 9 Feb 2024 22:00:30 +0100 Subject: [PATCH 03/77] fix: separated core simulation classes from additional devices --- ophyd_devices/__init__.py | 10 +- ophyd_devices/sim/sim.py | 325 +++++++------------- ophyd_devices/sim/sim_additional_devices.py | 150 +++++++++ 3 files changed, 265 insertions(+), 220 deletions(-) create mode 100644 ophyd_devices/sim/sim_additional_devices.py diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index 67e6e1d..ee707a4 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -4,13 +4,9 @@ from .galil.galil_ophyd import GalilMotor from .galil.sgalil_ophyd import SGalilMotor from .npoint.npoint import NPointAxis from .rt_lamni import RtLamniMotor -from .sim.sim import SimPositioner, SimMonitor -from .sim.sim import SimPositioner as SynAxisOPAAS -from .sim.sim import SimMonitor as SynAxisMonitor -from .sim.sim import SimMonitor as SynGaussBEC -from .sim.sim import SimCamera as SynSLSDetector -from .sim.sim_signals import ReadOnlySignal as SynSignalRO -from .sim.sim import SynDeviceOPAAS, SynFlyer +from .sim.sim import SimPositioner, SimMonitor, SimCamera, SynDeviceOPAAS, SynFlyer +from .sim.sim_signals import ReadOnlySignal, SetableSignal, ComputedReadOnlySignal from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor from .utils.static_device_test import launch +from .utils.bec_device_base import BECDeviceBase diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index a0dd91f..5cb4478 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -6,12 +6,13 @@ import warnings import numpy as np from bec_lib import MessageEndpoints, bec_logger, messages from ophyd import Component as Cpt -from ophyd import Device, DeviceStatus, Kind from ophyd import DynamicDeviceComponent as Dcpt +from ophyd import Device, DeviceStatus, Kind from ophyd import PositionerBase, Signal from ophyd.sim import SynSignal + from ophyd.utils import LimitError -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor from ophyd_devices.sim.sim_additional_devices import DummyController @@ -102,7 +103,9 @@ class SimCamera(Device): exp_time = Cpt(SetableSignal, name="exp_time", value=1, kind=Kind.config) file_path = Cpt(SetableSignal, name="file_path", value="", kind=Kind.config) + file_pattern = Cpt(SetableSignal, name="file_pattern", value="", kind=Kind.config) frames = Cpt(SetableSignal, name="frames", value=1, kind=Kind.config) + burst = Cpt(SetableSignal, name="burst", value=1, kind=Kind.config) image_shape = Cpt(SetableSignal, name="image_shape", value=SHAPE, kind=Kind.config) image = Cpt( @@ -128,6 +131,7 @@ class SimCamera(Device): super().__init__(name=name, parent=parent, kind=kind, **kwargs) self._stopped = False + self._staged = False self.scaninfo = None self._update_scaninfo() @@ -175,15 +179,16 @@ class SimCamera(Device): is published to the device_monitor endpoint in REDIS. """ if self._staged: - return super().stag e() + return super().stage() self.scaninfo.load_scan_metadata() self.file_path.set( os.path.join( self.file_path.get(), self.file_pattern.get().format(self.scaninfo.scan_number) ) ) - self.frames = self.scaninfo.num_points * self.scaninfo.frames_per_trigger - self.exp_time = self.scaninfo.exp_time + self.frames.set(self.scaninfo.num_points * self.scaninfo.frames_per_trigger) + self.exp_time.set(self.scaninfo.exp_time) + self.burst.set(self.scaninfo.frames_per_trigger) self._stopped = False return super().stage() @@ -199,11 +204,9 @@ class SimCamera(Device): if signal.item._kind == Kind.config } - signals = {"config": config_readout, "data": self.file_name} + signals = {"config": config_readout, "data": self.file_path.get()} msg = messages.DeviceMessage(signals=signals, metadata=self.scaninfo.metadata) - self.device_manager.producer.set_and_publish( - MessageEndpoints.device_read(self.name), msg.dumps() - ) + self.device_manager.producer.set_and_publish(MessageEndpoints.device_read(self.name), msg) def unstage(self) -> list[object]: """Unstage the device @@ -222,205 +225,6 @@ class SimCamera(Device): super().stop(success=success) -class DummyControllerDevice(Device): - USER_ACCESS = ["controller"] - - -class SynFlyer(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) - - @property - def hints(self): - return {"fields": ["flyer_samx", "flyer_samy"]} - - 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 = messages.BundleMessage() - for ii in range(num_pos): - bundle.append( - messages.DeviceMessage( - signals={ - self.name: { - "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 = messages.BundleMessage() - device.device_manager.producer.set_and_publish( - MessageEndpoints.device_status(device.name), - messages.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), - messages.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 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 = messages.BundleMessage() - for ii in range(num_pos): - bundle.append( - messages.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 = messages.BundleMessage() - device.device_manager.producer.set_and_publish( - MessageEndpoints.device_status(device.name), - messages.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), - messages.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 SimPositioner(Device, PositionerBase): """ A simulated device mimicing any 1D Axis device (position, temperature, rotation). @@ -625,6 +429,105 @@ class SimPositioner(Device, PositionerBase): return "mm" +class SynFlyer(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) + + @property + def hints(self): + return {"fields": ["flyer_samx", "flyer_samy"]} + + 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 = messages.BundleMessage() + for ii in range(num_pos): + bundle.append( + messages.DeviceMessage( + signals={ + self.name: { + "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 = messages.BundleMessage() + device.device_manager.producer.set_and_publish( + MessageEndpoints.device_status(device.name), + messages.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), + messages.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 SynDynamicComponents(Device): messages = Dcpt({f"message{i}": (SynSignal, None, {"name": f"msg{i}"}) for i in range(1, 6)}) @@ -636,8 +539,4 @@ class SynDeviceSubOPAAS(Device): class SynDeviceOPAAS(Device): x = Cpt(SimPositioner, name="x") y = Cpt(SimPositioner, name="y") - z = Cpt(SynDeviceSubOPAAS, name="z") - - -if __name__ == "__main__": - gauss = SynGaussBEC(name="gauss") + z = Cpt(SimPositioner, name="z") diff --git a/ophyd_devices/sim/sim_additional_devices.py b/ophyd_devices/sim/sim_additional_devices.py new file mode 100644 index 0000000..2030b66 --- /dev/null +++ b/ophyd_devices/sim/sim_additional_devices.py @@ -0,0 +1,150 @@ +import time as ttime +import threading +import numpy as np + +from ophyd import OphydObject, Device, PositionerBase + +from bec_lib import messages, MessageEndpoints + + +class DummyControllerDevice(Device): + USER_ACCESS = ["controller"] + + +class DummyController: + USER_ACCESS = [ + "some_var", + "controller_show_all", + "_func_with_args", + "_func_with_args_and_kwargs", + "_func_with_kwargs", + "_func_without_args_kwargs", + ] + some_var = 10 + another_var = 20 + + def on(self): + self._connected = True + + def off(self): + self._connected = False + + def _func_with_args(self, *args): + return args + + def _func_with_args_and_kwargs(self, *args, **kwargs): + return args, kwargs + + def _func_with_kwargs(self, **kwargs): + return kwargs + + def _func_without_args_kwargs(self): + return None + + def controller_show_all(self): + """dummy controller show all + + Raises: + in: _description_ + LimitError: _description_ + + Returns: + _type_: _description_ + """ + print(self.some_var) + + +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 + + 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 = messages.BundleMessage() + for ii in range(num_pos): + bundle.append( + messages.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 = messages.BundleMessage() + device.device_manager.producer.set_and_publish( + MessageEndpoints.device_status(device.name), + messages.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), + messages.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() From 8ee502242457f3ac63c122f81e7600e300fdf73a Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 9 Feb 2024 22:01:25 +0100 Subject: [PATCH 04/77] feat: add BECDeviceBase to ophyd_devices.utils --- ophyd_devices/utils/bec_device_base.py | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 ophyd_devices/utils/bec_device_base.py diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py new file mode 100644 index 0000000..49443ba --- /dev/null +++ b/ophyd_devices/utils/bec_device_base.py @@ -0,0 +1,75 @@ +class BECDeviceBase: + """Base class for BEC devices with minimal functionality.""" + + def __init__(self, name: str, *args, **kwargs): + self.name = name + self._connected = True + self._destroyed = False + + @property + def hints(self): + """hints property""" + return {} + + @property + def connected(self) -> bool: + """connected property. + Check if signals are connected + + Returns: + bool: True if connected, False otherwise + """ + return self._connected + + @connected.setter + def connected(self, value: bool): + """connected setter""" + self._connected = value + + def describe(self) -> dict: + """describe method + + Includes all signals of type Kind.hinted and Kind.normal. + Override by child class with describe method + + Returns: + dict: Dictionary with dictionaries with signal descriptions ('source', 'dtype', 'shape') + """ + return {} + + def describe_configuration(self) -> dict: + """describe method + + Includes all signals of type Kind.config. + Override by child class with describe_configuration method + + Returns: + dict: Dictionary with dictionaries with signal descriptions ('source', 'dtype', 'shape') + """ + return {} + + def read_configuration(self) -> dict: + """read_configuration method + + Override by child class with read_configuration method + + Returns: + dict: Dictionary with nested dictionary of signals with kind.config: + {'signal_name' : {'value' : .., "timestamp" : ..}, ...} + """ + return {} + + def read(self) -> dict: + """read method + + Override by child class with read method + + Returns: + dict: Dictionary with nested dictionary of signals with kind.normal or kind.hinted: + {'signal_name' : {'value' : .., "timestamp" : ..}, ...} + """ + return {} + + def destroy(self): + """Destroy method""" + self._destroyed = True From 8cc7e408a52fb6ca98673fa2045f1658cfcc3925 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 13 Feb 2024 11:01:41 +0100 Subject: [PATCH 05/77] refactor: cleanup and renaming according to MR comments --- ophyd_devices/sim/sim.py | 4 +-- ...itional_devices.py => sim_test_devices.py} | 0 ophyd_devices/utils/bec_device_base.py | 2 +- ophyd_devices/utils/bec_scaninfo_mixin.py | 11 +++---- ophyd_devices/utils/bec_utils.py | 29 ------------------- 5 files changed, 9 insertions(+), 37 deletions(-) rename ophyd_devices/sim/{sim_additional_devices.py => sim_test_devices.py} (100%) diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 5cb4478..f217d2c 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -14,7 +14,7 @@ from ophyd.sim import SynSignal from ophyd.utils import LimitError from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor -from ophyd_devices.sim.sim_additional_devices import DummyController +from ophyd_devices.sim.sim_test_devices import DummyController from ophyd_devices.sim.sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal @@ -215,7 +215,7 @@ class SimCamera(Device): """ if self._stopped is True or not self._staged: return super().unstage() - self._send_data_to_bec() + # self._send_data_to_bec() return super().unstage() diff --git a/ophyd_devices/sim/sim_additional_devices.py b/ophyd_devices/sim/sim_test_devices.py similarity index 100% rename from ophyd_devices/sim/sim_additional_devices.py rename to ophyd_devices/sim/sim_test_devices.py diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py index 49443ba..9123353 100644 --- a/ophyd_devices/utils/bec_device_base.py +++ b/ophyd_devices/utils/bec_device_base.py @@ -1,5 +1,5 @@ class BECDeviceBase: - """Base class for BEC devices with minimal functionality.""" + """Base class for BEC devices with zero functionality.""" def __init__(self, name: str, *args, **kwargs): self.name = name diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index 3c20646..e2a47ad 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -102,11 +102,12 @@ class BecScaninfoMixin: def get_username(self) -> str: """Get username""" - if not self.sim_mode: - msg = self.device_manager.producer.get(MessageEndpoints.account()) - if not isinstance(msg, str): - return os.getlogin() - return self.device_manager.producer.get(MessageEndpoints.account()) + if self.sim_mode: + return os.getlogin() + + msg = self.device_manager.producer.get(MessageEndpoints.account()) + if msg: + return msg return os.getlogin() def load_scan_metadata(self) -> None: diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index c70cc10..94c9cf7 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -154,35 +154,6 @@ class DMMock: self.devices[name] = DeviceMock(name, value) -# class MockProducer: -# def set_and_publish(self, endpoint: str, msgdump: str): -# logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") - - -# class MockDeviceManager: -# def __init__(self) -> None: -# self.devices = devices() - - -# class OphydObject: -# def __init__(self) -> None: -# self.name = "mock_mokev" -# self.obj = mokev() - - -# class devices: -# def __init__(self): -# self.mokev = OphydObject() - - -# class mokev: -# def __init__(self): -# self.name = "mock_mokev" - -# def read(self): -# return {self.name: {"value": 16.0, "timestamp": time.time()}} - - class ConfigSignal(Signal): def __init__( self, From fa6e24f04894c8e9a6b5920d04468c5194bf45a4 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 13 Feb 2024 11:23:20 +0100 Subject: [PATCH 06/77] refactor: Remove send msg to BEC, seems to be not needed --- ophyd_devices/sim/sim.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index f217d2c..01e1508 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -192,22 +192,6 @@ class SimCamera(Device): self._stopped = False return super().stage() - def _send_data_to_bec(self) -> None: - """Send data to BEC. - - Reads out all signals of type Kind.config, and send them to BEC. - Happens once for each scan. - """ - config_readout = { - signal.item.name: signal.item.get() - for signal in self.walk_signals() - if signal.item._kind == Kind.config - } - - signals = {"config": config_readout, "data": self.file_path.get()} - msg = messages.DeviceMessage(signals=signals, metadata=self.scaninfo.metadata) - self.device_manager.producer.set_and_publish(MessageEndpoints.device_read(self.name), msg) - def unstage(self) -> list[object]: """Unstage the device @@ -215,7 +199,6 @@ class SimCamera(Device): """ if self._stopped is True or not self._staged: return super().unstage() - # self._send_data_to_bec() return super().unstage() From ad4096f3a2b6bb04ca4b356ba743da7322d6710c Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 13 Feb 2024 11:58:27 +0000 Subject: [PATCH 07/77] 0.20.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db9caad..6f4e29f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ +## v0.20.0 (2024-02-13) + +### Feature + +* Add BECDeviceBase to ophyd_devices.utils ([`8ee5022`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/8ee502242457f3ac63c122f81e7600e300fdf73a)) + +### Fix + +* Separated core simulation classes from additional devices ([`2225daf`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/2225dafb7438f576d7033e220910b4cf8769fd33)) + ## v0.19.3 (2024-02-10) ### Fix diff --git a/setup.py b/setup.py index 629e8fc..aa663de 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.19.3" +__version__ = "0.20.0" if __name__ == "__main__": setup( From bd42d9d56093316f4a9f90a3329b6b5a6d1c851e Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 13 Feb 2024 16:32:52 +0100 Subject: [PATCH 08/77] fix: Use getpass.getuser instead of os.getlogin to retrieve user name --- ophyd_devices/utils/bec_scaninfo_mixin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index e2a47ad..417259b 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -1,4 +1,4 @@ -import os +import getpass from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger @@ -103,12 +103,12 @@ class BecScaninfoMixin: def get_username(self) -> str: """Get username""" if self.sim_mode: - return os.getlogin() + return getpass.getuser() msg = self.device_manager.producer.get(MessageEndpoints.account()) if msg: return msg - return os.getlogin() + return getpass.getuser() def load_scan_metadata(self) -> None: """Load scan metadata From c5b220b1954eec47ebc0cc28ab818da5417c293f Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 13 Feb 2024 15:41:16 +0000 Subject: [PATCH 09/77] 0.20.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4e29f..62fb0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.20.1 (2024-02-13) + +### Fix + +* Use getpass.getuser instead of os.getlogin to retrieve user name ([`bd42d9d`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/bd42d9d56093316f4a9f90a3329b6b5a6d1c851e)) + ## v0.20.0 (2024-02-13) ### Feature diff --git a/setup.py b/setup.py index aa663de..31422eb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.20.0" +__version__ = "0.20.1" if __name__ == "__main__": setup( From b808659d4d8b1af262d6f62174b027b0736a005a Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Tue, 12 Dec 2023 09:13:58 +0100 Subject: [PATCH 10/77] feat: flomni stages --- ophyd_devices/__init__.py | 14 +- ophyd_devices/epics/__init__.py | 3 +- ophyd_devices/epics/devices/__init__.py | 36 +- .../epics/devices/flomni_sample_storage.py | 82 ++ ophyd_devices/galil/fgalil_ophyd.py | 81 +- ophyd_devices/galil/fupr_ophyd.py | 12 + ophyd_devices/galil/galil_ophyd.py | 10 +- ophyd_devices/rt_lamni/__init__.py | 3 +- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 749 ++++++++++++++++ ophyd_devices/rt_lamni/rt_ophyd.py | 831 ++++++++++++++++++ ophyd_devices/smaract/smaract_controller.py | 47 +- ophyd_devices/utils/controller.py | 10 +- ophyd_devices/utils/socket.py | 19 +- 13 files changed, 1845 insertions(+), 52 deletions(-) create mode 100644 ophyd_devices/epics/devices/flomni_sample_storage.py create mode 100644 ophyd_devices/rt_lamni/rt_flomni_ophyd.py create mode 100644 ophyd_devices/rt_lamni/rt_ophyd.py diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index b040504..1f88e9f 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -1,17 +1,21 @@ from .eiger1p5m_csaxs.eiger1p5m import Eiger1p5MDetector from .epics import * +from .galil.fgalil_ophyd import FlomniGalilMotor +from .galil.fupr_ophyd import FuprGalilMotor from .galil.galil_ophyd import GalilMotor from .galil.sgalil_ophyd import SGalilMotor from .npoint.npoint import NPointAxis -from .rt_lamni import RtLamniMotor -from .sim.sim import SimPositioner, SimMonitor, SimCamera -from .sim.sim import SimPositioner as SynAxisOPAAS +from .rt_lamni import RtFlomniMotor, RtLamniMotor +from .sim.sim import SimCamera +from .sim.sim import SimMonitor from .sim.sim import SimMonitor as SynAxisMonitor from .sim.sim import SimMonitor as SynGaussBEC +from .sim.sim import SimPositioner +from .sim.sim import SimPositioner as SynAxisOPAAS +from .sim.sim import SynDeviceOPAAS, SynFlyer from .sim.sim_signals import ReadOnlySignal from .sim.sim_signals import ReadOnlySignal as SynSignalRO -from .sim.sim import SynDeviceOPAAS, SynFlyer from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor -from .utils.static_device_test import launch from .utils.bec_device_base import BECDeviceBase +from .utils.static_device_test import launch diff --git a/ophyd_devices/epics/__init__.py b/ophyd_devices/epics/__init__.py index af09589..6856f55 100644 --- a/ophyd_devices/epics/__init__.py +++ b/ophyd_devices/epics/__init__.py @@ -4,6 +4,7 @@ from ophyd.quadem import QuadEM from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal from .devices.delay_generator_csaxs import DelayGeneratorcSAXS +from .devices.flomni_sample_storage import FlomniSampleStorage from .devices.InsertionDevice import InsertionDevice from .devices.slits import SlitH, SlitV from .devices.specMotors import ( @@ -20,7 +21,7 @@ from .devices.specMotors import ( PmMonoBender, ) from .devices.SpmBase import SpmBase -from .devices.XbpmBase import XbpmBase, XbpmCsaxsOp # X07MA specific devices from .devices.X07MADevices import * +from .devices.XbpmBase import XbpmBase, XbpmCsaxsOp diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 2b5c0c2..55677c8 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -1,7 +1,19 @@ -from .slits import SlitH, SlitV -from .XbpmBase import XbpmBase, XbpmCsaxsOp -from .SpmBase import SpmBase +# Standard ophyd classes +from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd.quadem import QuadEM +from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal + +from .DelayGeneratorDG645 import DelayGeneratorDG645 +from .eiger9m_csaxs import Eiger9McSAXS + +# cSAXS +from .epics_motor_ex import EpicsMotorEx +from .falcon_csaxs import FalconcSAXS +from .flomni_sample_storage import FlomniSampleStorage from .InsertionDevice import InsertionDevice +from .mcs_csaxs import McsCsaxs +from .pilatus_csaxs import PilatuscSAXS +from .slits import SlitH, SlitV from .specMotors import ( Bpm4i, EnergyKev, @@ -15,19 +27,5 @@ from .specMotors import ( PmDetectorRotation, PmMonoBender, ) - -# Standard ophyd classes -from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor -from ophyd.sim import SynAxis, SynSignal, SynPeriodicSignal -from ophyd.quadem import QuadEM - -# cSAXS -from .epics_motor_ex import EpicsMotorEx -from .mcs_csaxs import MCScSAXS -from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin -from .eiger9m_csaxs import Eiger9McSAXS -from .pilatus_csaxs import PilatuscSAXS -from .falcon_csaxs import FalconcSAXS -from .delay_generator_csaxs import DelayGeneratorcSAXS - -# from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin +from .SpmBase import SpmBase +from .XbpmBase import XbpmBase, XbpmCsaxsOp diff --git a/ophyd_devices/epics/devices/flomni_sample_storage.py b/ophyd_devices/epics/devices/flomni_sample_storage.py new file mode 100644 index 0000000..e9f1b7f --- /dev/null +++ b/ophyd_devices/epics/devices/flomni_sample_storage.py @@ -0,0 +1,82 @@ +import time + +from ophyd import Component as Cpt +from ophyd import Device +from ophyd import DynamicDeviceComponent as Dcpt +from ophyd import EpicsSignal + + +class FlomniSampleStorageError(Exception): + pass + + +class FlomniSampleStorage(Device): + USER_ACCESS = [ + "is_sample_slot_used", + "is_sample_in_gripper", + "set_sample_slot", + "unset_sample_slot", + "set_sample_in_gripper", + "unset_sample_in_gripper", + ] + SUB_VALUE = "value" + _default_sub = SUB_VALUE + sample_placed = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21) + } + sample_placed = Dcpt(sample_placed) + + sample_names = { + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True}) + for i in range(21) + } + sample_names = Dcpt(sample_names) + + sample_in_gripper = Cpt( + EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET" + ) + sample_in_gripper_name = Cpt( + EpicsSignal, + name="sample_in_gripper_name", + read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC", + string=True, + ) + + def __init__(self, prefix="", *, name, **kwargs): + super().__init__(prefix, name=name, **kwargs) + self.sample_placed.sample1.subscribe(self._emit_value) + + def _emit_value(self, **kwargs): + timestamp = kwargs.pop("timestamp", time.time()) + self.wait_for_connection() + self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self) + + def set_sample_slot(self, slot_nr: int, name: str) -> bool: + if slot_nr > 20: + raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.") + + getattr(self.sample_placed, f"sample{slot_nr}").set(1) + getattr(self.sample_names, f"sample{slot_nr}").set(name) + + def unset_sample_slot(self, slot_nr: int) -> bool: + if slot_nr > 20: + raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.") + + getattr(self.sample_placed, f"sample{slot_nr}").set(0) + getattr(self.sample_names, f"sample{slot_nr}").set("-") + + def set_sample_in_gripper(self, name: str) -> bool: + self.sample_in_gripper.set(1) + self.sample_in_gripper_name.set(name) + + def unset_sample_in_gripper(self) -> bool: + self.sample_in_gripper.set(0) + self.sample_in_gripper_name.set("-") + + def is_sample_slot_used(self, slot_nr: int) -> bool: + val = getattr(self.sample_placed, f"sample{slot_nr}").get() + return bool(val) + + def is_sample_in_gripper(self) -> bool: + val = self.sample_in_gripper.get() + return bool(val) diff --git a/ophyd_devices/galil/fgalil_ophyd.py b/ophyd_devices/galil/fgalil_ophyd.py index 84e0905..a351545 100644 --- a/ophyd_devices/galil/fgalil_ophyd.py +++ b/ophyd_devices/galil/fgalil_ophyd.py @@ -8,8 +8,6 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError -from prettytable import PrettyTable - from ophyd_devices.galil.galil_ophyd import ( BECConfigError, GalilAxesReferenced, @@ -20,15 +18,31 @@ from ophyd_devices.galil.galil_ophyd import ( GalilMotorResolution, GalilReadbackSignal, GalilSetpointSignal, + GalilSignalRO, retry_once, ) from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable logger = bec_logger.logger class FlomniGalilController(GalilController): + USER_ACCESS = [ + "describe", + "show_running_threads", + "galil_show_all", + "socket_put_and_receive", + "socket_put_confirmed", + "drive_axis_to_limit", + "find_reference", + "get_motor_limit_switch", + "fosaz_light_curtain_is_triggered", + "is_motor_on", + "all_axes_referenced", + ] + def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool: if axis_Id is None and axis_Id_numeric is not None: axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) @@ -40,13 +54,67 @@ class FlomniGalilController(GalilController): # TODO: check if all axes are referenced in all controllers return super().all_axes_referenced() + def fosaz_light_curtain_is_triggered(self) -> bool: + """ + Check the light curtain status for fosaz -class FlomniGalilReadbackSignal(GalilReadbackSignal): - pass + Returns: + bool: True if the light curtain is triggered + """ + + return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1 + + +class FlomniGalilReadbackSignal(GalilSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + + current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}")) + current_pos *= self.parent.sign + step_mm = self.parent.motor_resolution.get() + return current_pos / step_mm + + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + return val class FlomniGalilSetpointSignal(GalilSetpointSignal): - pass + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + GalilError: Raised if not all axes are referenced. + + """ + target_val = val * self.parent.sign + self.setpoint = target_val + axes_referenced = self.controller.all_axes_referenced() + if axes_referenced: + while self.controller.is_thread_active(0): + time.sleep(0.1) + + self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}") + self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}") + self.controller.socket_put_confirmed("movereq=1") + self.controller.socket_put_confirmed("XQ#NEWPAR") + while self.controller.is_thread_active(0): + time.sleep(0.005) + else: + raise GalilError("Not all axes are referenced.") class FlomniGalilMotorResolution(GalilMotorResolution): @@ -64,7 +132,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced): class FlomniGalilMotor(Device, PositionerBase): USER_ACCESS = ["controller"] readback = Cpt( - GalilReadbackSignal, + FlomniGalilReadbackSignal, signal_name="readback", kind="hinted", ) @@ -114,6 +182,7 @@ class FlomniGalilMotor(Device, PositionerBase): "device_mapping has been specified but the device_manager cannot be accessed." ) self.rt = self.device_mapping.get("rt") + self.pid_x_correction = 0 super().__init__( prefix, diff --git a/ophyd_devices/galil/fupr_ophyd.py b/ophyd_devices/galil/fupr_ophyd.py index e1f8492..31866a7 100644 --- a/ophyd_devices/galil/fupr_ophyd.py +++ b/ophyd_devices/galil/fupr_ophyd.py @@ -62,6 +62,18 @@ class FuprGalilReadbackSignal(GalilReadbackSignal): step_mm = self.parent.motor_resolution.get() return current_pos / step_mm + def read(self): + self._metadata["timestamp"] = time.time() + val = super().read() + if self.parent.axis_Id_numeric == 0: + try: + rt = self.parent.device_manager.devices[self.parent.rt] + if rt.enabled: + rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]) + except KeyError: + logger.warning("Failed to set RT value during readback.") + return val + class FuprGalilSetpointSignal(GalilSetpointSignal): @retry_once diff --git a/ophyd_devices/galil/galil_ophyd.py b/ophyd_devices/galil/galil_ophyd.py index 549bc34..a0292da 100644 --- a/ophyd_devices/galil/galil_ophyd.py +++ b/ophyd_devices/galil/galil_ophyd.py @@ -51,6 +51,11 @@ class GalilController(Controller): "socket_put_and_receive", "socket_put_confirmed", "lgalil_is_air_off_and_orchestra_enabled", + "drive_axis_to_limit", + "find_reference", + "get_motor_limit_switch", + "is_motor_on", + "all_axes_referenced", ] @threadlocked @@ -142,10 +147,11 @@ class GalilController(Controller): self.socket_put_confirmed(f"naxis={axis_Id_numeric}") self.socket_put_confirmed(f"ndir={direction_flag}") self.socket_put_confirmed("XQ#NEWPAR") + time.sleep(0.005) self.socket_put_confirmed("XQ#FES") - time.sleep(0.1) + time.sleep(0.01) while self.is_axis_moving(None, axis_Id_numeric): - time.sleep(0.1) + time.sleep(0.01) axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) # check if we actually hit the limit diff --git a/ophyd_devices/rt_lamni/__init__.py b/ophyd_devices/rt_lamni/__init__.py index edc7c01..5a60cfb 100644 --- a/ophyd_devices/rt_lamni/__init__.py +++ b/ophyd_devices/rt_lamni/__init__.py @@ -1 +1,2 @@ -from .rt_lamni_ophyd import RtLamniMotor, RtLamniController +from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor +from .rt_lamni_ophyd import RtLamniController, RtLamniMotor diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py new file mode 100644 index 0000000..9acce7d --- /dev/null +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -0,0 +1,749 @@ +import functools +import threading +import time +from typing import List + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.rt_lamni.rt_ophyd import ( + BECConfigError, + RtCommunicationError, + RtController, + RtError, + RtReadbackSignal, + RtSetpointSignal, + RtSignalRO, + retry_once, +) +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable + +logger = bec_logger.logger + + +class RtFlomniController(RtController): + USER_ACCESS = [ + "socket_put_and_receive", + "set_rotation_angle", + "feedback_disable", + "feedback_enable_without_reset", + "feedback_enable_with_reset", + "add_pos_to_scan", + "clear_trajectory_generator", + "show_cyclic_error_compensation", + "laser_tracker_on", + "laser_tracker_off", + "laser_tracker_show_all", + ] + + def __init__( + self, + *, + name=None, + socket_cls=None, + socket_host=None, + socket_port=None, + attr_name="", + parent=None, + labels=None, + kind=None, + ): + super().__init__( + name=name, + socket_cls=socket_cls, + socket_host=socket_host, + socket_port=socket_port, + attr_name=attr_name, + parent=parent, + labels=labels, + kind=kind, + ) + self.tracker_info = {} + self._min_scan_buffer_reached = False + self.rt_pid_voltage = None + + def add_pos_to_scan(self, positions) -> None: + def send_positions(parent, positions): + parent._min_scan_buffer_reached = False + start_time = time.time() + for pos_index, pos in enumerate(positions): + parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},{pos[2]}") + if pos_index > 100: + parent._min_scan_buffer_reached = True + parent._min_scan_buffer_reached = True + logger.info( + f"Sending {len(positions)} positions took {time.time()-start_time} seconds." + ) + + threading.Thread(target=send_positions, args=(self, positions), daemon=True).start() + + def move_to_zero(self): + self.socket_put("pa0,0") + self.get_axis_by_name("rtx").user_setpoint.setpoint = 0 + self.socket_put("pa1,0") + self.get_axis_by_name("rty").user_setpoint.setpoint = 0 + self.socket_put("pa2,0") + self.get_axis_by_name("rtz").user_setpoint.setpoint = 0 + time.sleep(0.05) + + def feedback_is_running(self) -> bool: + status = int(float(self.socket_put_and_receive("l2").strip())) + if status == 1: + return False + return True + + def feedback_enable_with_reset(self): + self.socket_put("l0") # disable feedback + + self.move_to_zero() + + if not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: + logger.info("Please wait, slew rate limiters not on target.") + while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: + time.sleep(0.05) + + self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0}) + self.clear_trajectory_generator() + + self.laser_tracker_on() + + # move to 0. FUPR will set the rotation angle during readout + self.get_device_manager().devices.fsamroy.obj.move(0, wait=True) + + fsamx = self.get_device_manager().devices.fsamx + + fsamx.obj.pid_x_correction = 0 + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") + fsamx_in = fsamx.user_parameter.get("in") + if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3): + raise RtError( + "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." + ) + + if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01): + fsamx.enabled_set = True + fsamx.obj.move(fsamx_in, wait=True) + fsamx.enabled_set = False + time.sleep(1) + + self.socket_put("l1") + time.sleep(0.4) + + if not self.feedback_is_running(): + raise RtError("Feedback is not running; likely an error in the interferometer.") + + time.sleep(1.5) + self.show_cyclic_error_compensation() + + self.rt_pid_voltage = self.get_pid_x() + rtx = self.get_device_manager().devices.rtx + rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage}) + + self.set_device_enabled("fsamx", False) + self.set_device_enabled("fsamy", False) + self.set_device_enabled("foptx", False) + self.set_device_enabled("fopty", False) + + def move_samx_to_scan_region(self, fovx: float, cenx: float): + if self.rt_pid_voltage is None: + rtx = self.get_device_manager().devices.rtx + self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage") + if self.rt_pid_voltage is None: + raise RtError( + "rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first." + ) + logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}") + expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100 + logger.info(f"Expected PID voltage: {expected_voltage}") + logger.info(f"Current PID voltage: {self.get_pid_x()}") + + wait_on_exit = False + while True: + if np.abs(self.get_pid_x() - expected_voltage) < 1: + break + wait_on_exit = True + self.socket_put("v0") + fsamx = self.get_device_manager().devices.fsamx + fsamx.enabled_set = True + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") + fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.01 + logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}") + fsamx_in = fsamx.user_parameter.get("in") + fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True) + fsamx.enabled_set = False + time.sleep(0.1) + self.laser_tracker_on() + time.sleep(0.01) + + if wait_on_exit: + time.sleep(1) + + self.socket_put("v1") + + @threadlocked + def clear_trajectory_generator(self): + self.socket_put("sc") + logger.info("flomni scan stopped and deleted, moving to start position") + + def feedback_enable_without_reset(self): + self.laser_tracker_on() + self.socket_put("l3") + time.sleep(0.01) + + if not self.feedback_is_running(): + raise RtError("Feedback is not running; likely an error in the interferometer.") + + self.set_device_enabled("fsamx", False) + self.set_device_enabled("fsamy", False) + self.set_device_enabled("foptx", False) + self.set_device_enabled("fopty", False) + + def feedback_disable(self): + self.clear_trajectory_generator() + self.move_to_zero() + self.socket_put("l0") + + self.set_device_enabled("fsamx", True) + self.set_device_enabled("fsamy", True) + self.set_device_enabled("foptx", True) + self.set_device_enabled("fopty", True) + + fsamx = self.get_device_manager().devices.fsamx + fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]") + + def get_pid_x(self) -> float: + voltage = float(self.socket_put_and_receive("g").strip()) + return voltage + + def show_cyclic_error_compensation(self): + cec0 = int(float(self.socket_put_and_receive("w0").strip())) + cec1 = int(float(self.socket_put_and_receive("w0").strip())) + + if cec0 == 32: + logger.info("Cyclic Error Compensation: y-axis is initialized") + else: + logger.info("Cyclic Error Compensation: y-axis is NOT initialized") + if cec1 == 32: + logger.info("Cyclic Error Compensation: x-axis is initialized") + else: + logger.info("Cyclic Error Compensation: y-axis is NOT initialized") + + def set_rotation_angle(self, val: float) -> None: + self.socket_put(f"a{(-val)/180*np.pi}") + + def laser_tracker_on(self): + self.laser_update_tracker_info() + + if not self.tracker_info["enabled_z"] or not self.tracker_info["enabled_y"]: + logger.info("Enabling the laser tracker. Please wait...") + + tracker_intensity = self.tracker_info["tracker_intensity"] + if ( + tracker_intensity < self.tracker_info["threshold_intensity_y"] + or tracker_intensity < self.tracker_info["threshold_intensity_z"] + ): + logger.info(self.tracker_info) + raise RtError("The tracker cannot be enabled because the beam intensity it low.") + + self.move_to_zero() + self.socket_put("T1") + time.sleep(0.5) + + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed( + "trackyct=0" + ) + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed( + "trackzct=0" + ) + + self.laser_tracker_wait_on_target() + logger.info("Laser tracker running!") + + def laser_tracker_off(self): + self.socket_put("T0") + + def laser_tracker_show_all(self): + self.laser_update_tracker_info() + t = PrettyTable() + t.title = f"Laser Tracker Info" + t.field_names = ["Name", "Value"] + for key, val in self.tracker_info.items(): + t.add_row([key, val]) + print(t) + + def laser_update_tracker_info(self): + ret = self.socket_put_and_receive("Ts") + + # remove trailing \n + ret = ret.split("\n")[0] + + tracker_values = [float(val) for val in ret.split(",")] + self.tracker_info = { + "tracker_intensity": tracker_values[2], + "threshold_intensity_y": tracker_values[8], + "enabled_y": bool(tracker_values[10]), + "beampos_y": tracker_values[5], + "target_y": tracker_values[6], + "piezo_voltage_y": tracker_values[9], + "threshold_intensity_z": tracker_values[3], + "enabled_z": bool(tracker_values[10]), + "beampos_z": tracker_values[0], + "target_z": tracker_values[1], + "piezo_voltage_z": tracker_values[4], + } + + def laser_tracker_galil_enable(self): + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("tracken=1") + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("trackyct=0") + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("trackzct=0") + self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("XQ#Tracker") + + def laser_tracker_on_target(self) -> bool: + self.laser_update_tracker_info() + if np.isclose( + self.tracker_info["beampos_y"], self.tracker_info["target_y"], atol=0.02 + ) and np.isclose(self.tracker_info["beampos_z"], self.tracker_info["target_z"], atol=0.02): + return True + return False + + def laser_tracker_wait_on_target(self): + max_repeat = 25 + count = 0 + while not self.laser_tracker_on_target(): + self.laser_tracker_galil_enable() + logger.info("Waiting for laser tracker to reach target.") + time.sleep(0.5) + count += 1 + if count > max_repeat: + raise RtError("Failed to reach laser target position.") + + def slew_rate_limiters_on_target(self) -> bool: + ret = int(float(self.socket_put_and_receive("y").strip())) + if ret == 3: + return True + return False + + def pid_y(self) -> float: + ret = float(self.socket_put_and_receive("G").strip()) + return ret + + def read_ssi_interferometer(self, axis_number): + val = float(self.socket_put_and_receive(f"j{axis_number}").strip()) + return val + + def show_signal_strength_interferometer(self): + t = PrettyTable() + t.title = f"Interferometer signal strength" + t.field_names = ["Axis", "Value"] + for i in range(3): + t.add_row([i, self.read_ssi_interferometer(i)]) + print(t) + + def _get_signals_from_table(self, return_table) -> dict: + self.average_stdeviations_x_st_fzp += float(return_table[4]) + self.average_stdeviations_y_st_fzp += float(return_table[7]) + signals = { + "target_x": {"value": float(return_table[2])}, + "average_x_st_fzp": {"value": float(return_table[3])}, + "stdev_x_st_fzp": {"value": float(return_table[4])}, + "target_y": {"value": float(return_table[5])}, + "average_y_st_fzp": {"value": float(return_table[6])}, + "stdev_y_st_fzp": {"value": float(return_table[7])}, + "average_rotz": {"value": float(return_table[8])}, + "stdev_rotz": {"value": float(return_table[9])}, + "average_stdeviations_x_st_fzp": { + "value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1) + }, + "average_stdeviations_y_st_fzp": { + "value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1) + }, + } + return signals + + @threadlocked + def start_scan(self): + if not self.feedback_is_running(): + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + # here exception + (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() + + if number_of_positions_planned == 0: + logger.error("Cannot start scan because no target positions are planned.") + raise RtError("Cannot start scan because no target positions are planned.") + # hier exception + # start a point-by-point scan (for cont scan in flomni it would be "sa") + self.socket_put_and_receive("sd") + + @retry_once + @threadlocked + def get_scan_status(self): + return_table = (self.socket_put_and_receive("sr")).split(",") + if len(return_table) != 3: + raise RtCommunicationError( + f"Expected to receive 3 return values. Instead received {return_table}" + ) + mode = int(float(return_table[0])) + # mode 0: direct positioning + # mode 1: running internal timer (not tested/used anymore) + # mode 2: rt point scan running + # mode 3: rt point scan starting + # mode 5/6: rt continuous scanning (not available in LamNI) + number_of_positions_planned = int(float(return_table[1])) + current_position_in_scan = int(float(return_table[2])) + return (mode, number_of_positions_planned, current_position_in_scan) + + def get_device_manager(self): + for axis in self._axis: + if hasattr(axis, "device_manager") and axis.device_manager: + return axis.device_manager + raise BECConfigError("Could not access the device_manager") + + def read_positions_from_sampler(self): + # this was for reading after the scan completed + number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + + read_counter = 0 + + self.average_stdeviations_x_st_fzp = 0 + self.average_stdeviations_y_st_fzp = 0 + self.average_lamni_angle = 0 + + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + + # if not (mode==2 or mode==3): + # error + self.get_device_manager().producer.set_and_publish( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=1, metadata=self.readout_metadata + ).dumps(), + ) + # while scan is running + while mode > 0: + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + time.sleep(0.01) + if current_position_in_scan > 5: + while current_position_in_scan > read_counter + 1: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + # logger.info(f"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + + self.publish_device_data(signals=signals, pointID=int(return_table[0])) + + time.sleep(0.05) + + # read the last samples even though scan is finished already + while number_of_positions_planned > read_counter: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + # logger.info(f"{return_table}") + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + self.publish_device_data(signals=signals, pointID=int(return_table[0])) + + self.get_device_manager().producer.set_and_publish( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=0, metadata=self.readout_metadata + ).dumps(), + ) + + logger.info( + f"Flomni statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}." + ) + + def publish_device_data(self, signals, pointID): + self.get_device_manager().producer.send( + MessageEndpoints.device_read("rt_flomni"), + messages.DeviceMessage( + signals=signals, + metadata={"pointID": pointID, **self.readout_metadata}, + ).dumps(), + ) + + def start_readout(self): + readout = threading.Thread(target=self.read_positions_from_sampler) + readout.start() + + def kickoff(self, metadata): + self.readout_metadata = metadata + while not self._min_scan_buffer_reached: + time.sleep(0.001) + self.start_scan() + time.sleep(0.1) + self.start_readout() + + +class RtFlomniReadbackSignal(RtReadbackSignal): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + time.sleep(0.1) + return_table = (self.controller.socket_put_and_receive(f"pr")).split(",") + + current_pos = float(return_table[self.parent.axis_Id_numeric]) + + current_pos *= self.parent.sign + self.parent.user_setpoint.setpoint = current_pos + return current_pos + + +class RtFlomniSetpointSignal(RtSetpointSignal): + setpoint = 0 + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + RtError: Raised if interferometer feedback is disabled. + + """ + if not self.parent.controller.feedback_is_running(): + raise RtError( + "The interferometer feedback is not running. Either it is turned off or and interferometer error occured." + ) + self.set_with_feedback_disabled(val) + + def set_with_feedback_disabled(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}") + + +class RtFlomniFeedbackRunning(RtSignalRO): + @threadlocked + def _socket_get(self): + return int(self.parent.controller.feedback_is_running()) + + +class RtFlomniMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt( + RtFlomniReadbackSignal, + signal_name="readback", + kind="hinted", + ) + user_setpoint = Cpt(RtFlomniSetpointSignal, signal_name="setpoint") + + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2844.psi.ch", + port=2222, + sign=1, + socket_cls=SocketIO, + device_manager=None, + limits=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = RtFlomniController( + socket_cls=socket_cls, socket_host=host, socket_port=port + ) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.device_manager = device_manager + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while not self.controller.slew_rate_limiters_on_target(): + print("motor is moving") + val = self.readback.read() + self._run_subs( + sub_type=self.SUB_READBACK, + value=val, + timestamp=time.time(), + ) + time.sleep(0.01) + print("Move finished") + self._done_moving() + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + def kickoff(self, metadata, **kwargs) -> None: + self.controller.kickoff(metadata) + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "um" + + # how is this used later? + + def stage(self) -> List[object]: + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + rtcontroller = RtFlomniController( + socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222 + ) + rtcontroller.on() + rtcontroller.laser_tracker_on() diff --git a/ophyd_devices/rt_lamni/rt_ophyd.py b/ophyd_devices/rt_lamni/rt_ophyd.py new file mode 100644 index 0000000..202c7ef --- /dev/null +++ b/ophyd_devices/rt_lamni/rt_ophyd.py @@ -0,0 +1,831 @@ +import functools +import threading +import time +from typing import List + +import numpy as np +from bec_lib import MessageEndpoints, bec_logger, messages +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError + +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected + +logger = bec_logger.logger + + +class RtCommunicationError(Exception): + pass + + +class RtError(Exception): + pass + + +class BECConfigError(Exception): + pass + + +def retry_once(fcn): + """Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty.""" + + @functools.wraps(fcn) + def wrapper(self, *args, **kwargs): + try: + val = fcn(self, *args, **kwargs) + except (RtCommunicationError, RtError): + val = fcn(self, *args, **kwargs) + return val + + return wrapper + + +class RtController(Controller): + _axes_per_controller = 3 + USER_ACCESS = [ + "socket_put_and_receive", + "set_rotation_angle", + "feedback_disable", + "feedback_enable_without_reset", + "feedback_disable_and_even_reset_lamni_angle_interferometer", + "feedback_enable_with_reset", + "add_pos_to_scan", + "clear_trajectory_generator", + "_set_axis_velocity", + "_set_axis_velocity_maximum_speed", + "_position_sampling_single_read", + "_position_sampling_single_reset_and_start_sampling", + ] + + def on(self, controller_num=0) -> None: + """Open a new socket connection to the controller""" + # if not self.connected: + # try: + # self.sock.open() + # # discuss - after disconnect takes a while for the server to be ready again + # max_retries = 10 + # tries = 0 + # while not self.connected: + # try: + # welcome_message = self.sock.receive() + # self.connected = True + # except ConnectionResetError as conn_reset: + # if tries > max_retries: + # raise conn_reset + # tries += 1 + # time.sleep(2) + # except ConnectionRefusedError as conn_error: + # logger.error("Failed to open a connection to RTLamNI.") + # raise RtCommunicationError from conn_error + + # else: + # logger.info("The connection has already been established.") + # # warnings.warn(f"The connection has already been established.", stacklevel=2) + super().on() + # self._update_flyer_device_info() + + def set_axis(self, axis: Device, axis_nr: int) -> None: + """Assign an axis to a device instance. + + Args: + axis (Device): Device instance (e.g. GalilMotor) + axis_nr (int): Controller axis number + + """ + self._axis[axis_nr] = axis + + @threadlocked + def socket_put(self, val: str) -> None: + self.sock.put(f"{val}\n".encode()) + + @threadlocked + def socket_get(self) -> str: + return self.sock.receive().decode() + + @retry_once + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + if remove_trailing_chars: + return self._remove_trailing_characters(self.sock.receive().decode()) + return self.socket_get() + + def is_axis_moving(self, axis_Id) -> bool: + # this checks that axis is on target + axis_is_on_target = bool(float(self.socket_put_and_receive(f"o"))) + return not axis_is_on_target + + # def is_thread_active(self, thread_id: int) -> bool: + # val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}")) + # if val == -1: + # return False + # return True + + def _remove_trailing_characters(self, var) -> str: + if len(var) > 1: + return var.split("\r\n")[0] + return var + + @threadlocked + def set_rotation_angle(self, val: float): + self.socket_put(f"a{(val-300+30.538)/180*np.pi}") + + @threadlocked + def stop_all_axes(self): + self.socket_put("sc") + + @threadlocked + def feedback_disable(self): + self.socket_put("J0") + logger.info("LamNI Feedback disabled.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + @threadlocked + def _set_axis_velocity(self, um_per_s): + self.socket_put(f"V{um_per_s}") + + @threadlocked + def _set_axis_velocity_maximum_speed(self): + self.socket_put(f"V0") + + # for developement of soft continuous scanning + @threadlocked + def _position_sampling_single_reset_and_start_sampling(self): + self.socket_put(f"Ss") + + @threadlocked + def _position_sampling_single_read(self): + (number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive( + f"Sr" + ).split(",") + avg_x = float(sum1) / int(number_of_samples) + avg_y = float(sum0) / int(number_of_samples) + stdev_x = np.sqrt( + float(sum1_2) / int(number_of_samples) + - np.power(float(sum1) / int(number_of_samples), 2) + ) + stdev_y = np.sqrt( + float(sum0_2) / int(number_of_samples) + - np.power(float(sum0) / int(number_of_samples), 2) + ) + return (avg_x, avg_y, stdev_x, stdev_y) + + @threadlocked + def feedback_enable_without_reset(self): + # read current interferometer position + return_table = (self.socket_put_and_receive(f"J4")).split(",") + x_curr = float(return_table[2]) + y_curr = float(return_table[1]) + # set these as closed loop target position + self.socket_put(f"pa0,{x_curr:.4f}") + self.socket_put(f"pa1,{y_curr:.4f}") + self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr) + self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr) + self.socket_put("J5") + logger.info("LamNI Feedback enabled (without reset).") + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + @threadlocked + def feedback_disable_and_even_reset_lamni_angle_interferometer(self): + self.socket_put("J6") + logger.info("LamNI Feedback disabled including the angular interferometer.") + self.set_device_enabled("lsamx", True) + self.set_device_enabled("lsamy", True) + self.set_device_enabled("loptx", True) + self.set_device_enabled("lopty", True) + self.set_device_enabled("loptz", True) + + def get_device_manager(self): + for axis in self._axis: + if hasattr(axis, "device_manager") and axis.device_manager: + return axis.device_manager + raise BECConfigError("Could not access the device_manager") + + def get_axis_by_name(self, name): + for axis in self._axis: + if axis: + if axis.name == name: + return axis + raise RuntimeError(f"Could not find an axis with name {name}") + + @threadlocked + def clear_trajectory_generator(self): + self.socket_put("sc") + logger.info("LamNI scan stopped and deleted, moving to start position") + + def add_pos_to_scan(self, positions) -> None: + def send_positions(parent, positions): + parent._min_scan_buffer_reached = False + for pos_index, pos in enumerate(positions): + parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},0") + if pos_index > 100: + parent._min_scan_buffer_reached = True + parent._min_scan_buffer_reached = True + + threading.Thread(target=send_positions, args=(self, positions), daemon=True).start() + + @retry_once + @threadlocked + def get_scan_status(self): + return_table = (self.socket_put_and_receive(f"sr")).split(",") + if len(return_table) != 3: + raise RtCommunicationError( + f"Expected to receive 3 return values. Instead received {return_table}" + ) + mode = int(return_table[0]) + # mode 0: direct positioning + # mode 1: running internal timer (not tested/used anymore) + # mode 2: rt point scan running + # mode 3: rt point scan starting + # mode 5/6: rt continuous scanning (not available in LamNI) + number_of_positions_planned = int(return_table[1]) + current_position_in_scan = int(return_table[2]) + return (mode, number_of_positions_planned, current_position_in_scan) + + @threadlocked + def start_scan(self): + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + # here exception + (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() + + if number_of_positions_planned == 0: + logger.error("Cannot start scan because no target positions are planned.") + raise RtError("Cannot start scan because no target positions are planned.") + # hier exception + # start a point-by-point scan (for cont scan in flomni it would be "sa") + self.socket_put_and_receive("sd") + + def start_readout(self): + readout = threading.Thread(target=self.read_positions_from_sampler) + readout.start() + + def _update_flyer_device_info(self): + flyer_info = self._get_flyer_device_info() + self.get_device_manager().producer.set( + MessageEndpoints.device_info("rt_scan"), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), + ) + + def _get_flyer_device_info(self) -> dict: + return { + "device_name": self.name, + "device_attr_name": getattr(self, "attr_name", ""), + "device_dotted_name": getattr(self, "dotted_name", ""), + "device_info": { + "device_base_class": "ophydobject", + "signals": [], + "hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]}, + "describe": {}, + "describe_configuration": {}, + "sub_devices": [], + "custom_user_access": [], + }, + } + + def kickoff(self, metadata): + self.readout_metadata = metadata + while not self._min_scan_buffer_reached: + time.sleep(0.001) + self.start_scan() + time.sleep(0.1) + self.start_readout() + + def _get_signals_from_table(self, return_table) -> dict: + self.average_stdeviations_x_st_fzp += float(return_table[5]) + self.average_stdeviations_y_st_fzp += float(return_table[8]) + self.average_lamni_angle += float(return_table[19]) + signals = { + "target_x": {"value": float(return_table[3])}, + "average_x_st_fzp": {"value": float(return_table[4])}, + "stdev_x_st_fzp": {"value": float(return_table[5])}, + "target_y": {"value": float(return_table[6])}, + "average_y_st_fzp": {"value": float(return_table[7])}, + "stdev_y_st_fzp": {"value": float(return_table[8])}, + "average_cap1": {"value": float(return_table[9])}, + "stdev_cap1": {"value": float(return_table[10])}, + "average_cap2": {"value": float(return_table[11])}, + "stdev_cap2": {"value": float(return_table[12])}, + "average_cap3": {"value": float(return_table[13])}, + "stdev_cap3": {"value": float(return_table[14])}, + "average_cap4": {"value": float(return_table[15])}, + "stdev_cap4": {"value": float(return_table[16])}, + "average_cap5": {"value": float(return_table[17])}, + "stdev_cap5": {"value": float(return_table[18])}, + "average_angle_interf_ST": {"value": float(return_table[19])}, + "stdev_angle_interf_ST": {"value": float(return_table[20])}, + "average_stdeviations_x_st_fzp": { + "value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1) + }, + "average_stdeviations_y_st_fzp": { + "value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1) + }, + "average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)}, + } + return signals + + def read_positions_from_sampler(self): + # this was for reading after the scan completed + number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + + read_counter = 0 + previous_point_in_scan = 0 + + self.average_stdeviations_x_st_fzp = 0 + self.average_stdeviations_y_st_fzp = 0 + self.average_lamni_angle = 0 + + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + + # if not (mode==2 or mode==3): + # error + self.get_device_manager().producer.set_and_publish( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=1, metadata=self.readout_metadata + ).dumps(), + ) + # while scan is running + while mode > 0: + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") + mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() + time.sleep(0.01) + if current_position_in_scan > 5: + while current_position_in_scan > read_counter + 1: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + # logger.info(f"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + + self.publish_device_data(signals=signals, pointID=int(return_table[0])) + + time.sleep(0.05) + + # read the last samples even though scan is finished already + while number_of_positions_planned > read_counter: + return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + # logger.info(f"{return_table}") + read_counter = read_counter + 1 + + signals = self._get_signals_from_table(return_table) + self.publish_device_data(signals=signals, pointID=int(return_table[0])) + + self.get_device_manager().producer.set_and_publish( + MessageEndpoints.device_status("rt_scan"), + messages.DeviceStatusMessage( + device="rt_scan", status=0, metadata=self.readout_metadata + ).dumps(), + ) + + logger.info( + f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}." + ) + + def publish_device_data(self, signals, pointID): + self.get_device_manager().producer.send( + MessageEndpoints.device_read("rt_lamni"), + messages.DeviceMessage( + signals=signals, + metadata={"pointID": pointID, **self.readout_metadata}, + ).dumps(), + ) + + def feedback_status_angle_lamni(self) -> bool: + return_table = (self.socket_put_and_receive(f"J7")).split(",") + logger.debug( + f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}" + ) + return bool(return_table[0]) + + def feedback_enable_with_reset(self): + if not self.feedback_status_angle_lamni(): + self.feedback_disable_and_even_reset_lamni_angle_interferometer() + logger.info(f"LamNI resetting interferometer inclusive angular interferomter.") + else: + self.feedback_disable() + logger.info( + f"LamNI resetting interferomter except angular interferometer which is already running." + ) + + # set these as closed loop target position + + self.socket_put(f"pa0,0") + self.get_axis_by_name("rtx").user_setpoint.setpoint = 0 + self.socket_put(f"pa1,0") + self.get_axis_by_name("rty").user_setpoint.setpoint = 0 + self.socket_put( + f"pa2,0" + ) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used + self.clear_trajectory_generator() + + self.get_device_manager().devices.lsamrot.obj.move(0, wait=True) + + galil_controller_rt_status = ( + self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled() + ) + + if galil_controller_rt_status == 0: + logger.error( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + raise RtError( + "Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller." + ) + + time.sleep(0.03) + + lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter + if lsamx_user_params is None or lsamx_user_params.get("center") is None: + raise RuntimeError("lsamx center is not defined") + lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter + if lsamy_user_params is None or lsamy_user_params.get("center") is None: + raise RuntimeError("lsamy center is not defined") + lsamx_center = lsamx_user_params.get("center") + lsamy_center = lsamy_user_params.get("center") + self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True) + self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True) + self.socket_put("J1") + + _waitforfeedbackctr = 0 + + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) + + while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100: + time.sleep(0.01) + _waitforfeedbackctr = _waitforfeedbackctr + 1 + interferometer_feedback_not_running = int( + (self.socket_put_and_receive("J2")).split(",")[0] + ) + + self.set_device_enabled("lsamx", False) + self.set_device_enabled("lsamy", False) + self.set_device_enabled("loptx", False) + self.set_device_enabled("lopty", False) + self.set_device_enabled("loptz", False) + + if interferometer_feedback_not_running == 1: + logger.error( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + raise RtError( + "Cannot start scan because feedback loop is not running or there is an interferometer error." + ) + + time.sleep(0.01) + + # ptychography_alignment_done = 0 + + def set_device_enabled(self, device_name: str, enabled: bool) -> None: + """enable / disable a device""" + if device_name not in self.get_device_manager().devices: + logger.warning( + f"Device {device_name} is not configured and cannot be enabled/disabled." + ) + return + self.get_device_manager().devices[device_name].enabled_set = enabled + + +class RtSignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.parent.controller + self.sock = self.parent.controller.sock + + +class RtSignalRO(RtSignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class RtReadbackSignal(RtSignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + """Get command for the readback signal + + Returns: + float: Readback value after adjusting for sign and motor resolution. + """ + return_table = (self.controller.socket_put_and_receive(f"J4")).split(",") + print(return_table) + if self.parent.axis_Id_numeric == 0: + readback_index = 2 + elif self.parent.axis_Id_numeric == 1: + readback_index = 1 + else: + raise RtError("Currently, only two axes are supported.") + + current_pos = float(return_table[readback_index]) + + current_pos *= self.parent.sign + return current_pos + + +class RtSetpointSignal(RtSignalBase): + setpoint = 0 + + def _socket_get(self) -> float: + """Get command for receiving the setpoint / target value. + The value is not pulled from the controller but instead just the last setpoint used. + + Returns: + float: setpoint / target value + """ + return self.setpoint + + @retry_once + @threadlocked + def _socket_set(self, val: float) -> None: + """Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign. + Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted. + + Args: + val (float): Target value / setpoint value + + Raises: + RtError: Raised if interferometer feedback is disabled. + + """ + interferometer_feedback_not_running = int( + (self.controller.socket_put_and_receive("J2")).split(",")[0] + ) + if interferometer_feedback_not_running != 0: + raise RtError( + "The interferometer feedback is not running. Either it is turned off or and interferometer error occured." + ) + self.set_with_feedback_disabled(val) + + def set_with_feedback_disabled(self, val): + target_val = val * self.parent.sign + self.setpoint = target_val + self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}") + + +class RtMotorIsMoving(RtSignalRO): + def _socket_get(self): + return self.controller.is_axis_moving(self.parent.axis_Id_numeric) + + def get(self): + val = super().get() + if val is not None: + self._run_subs( + sub_type=self.SUB_VALUE, + value=val, + timestamp=time.time(), + ) + return val + + +class RtFeedbackRunning(RtSignalRO): + @threadlocked + def _socket_get(self): + if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0: + return 1 + else: + return 0 + + +class RtMotor(Device, PositionerBase): + USER_ACCESS = ["controller"] + readback = Cpt( + RtReadbackSignal, + signal_name="readback", + kind="hinted", + ) + user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint") + + motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="mpc2680.psi.ch", + port=3333, + sign=1, + socket_cls=SocketIO, + device_manager=None, + limits=None, + **kwargs, + ): + self.axis_Id = axis_Id + self.sign = sign + self.controller = RtController(socket=socket_cls(host=host, port=port)) + self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric) + self.device_manager = device_manager + self.tolerance = kwargs.pop("tolerance", 0.5) + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + + # self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE) + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + def _forward_readback(self, **kwargs): + kwargs.pop("sub_type") + self._run_subs(sub_type="readback", **kwargs) + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a specified position, optionally waiting for motion to + complete. + + Parameters + ---------- + position + Position to move to + moved_cb : callable + Call this callback when movement has finished. This callback must + accept one keyword argument: 'obj' which will be set to this + positioner instance. + timeout : float, optional + Maximum time to wait for the motion. If None, the default timeout + for this positioner is used. + + Returns + ------- + status : MoveStatus + + Raises + ------ + TimeoutError + When motion takes longer than `timeout` + ValueError + On invalid positions + RuntimeError + If motion fails other than timing out + """ + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + self.user_setpoint.put(position, wait=False) + + def move_and_finish(): + while self.motor_is_moving.get(): + print("motor is moving") + val = self.readback.read() + self._run_subs( + sub_type=self.SUB_READBACK, + value=val, + timestamp=time.time(), + ) + time.sleep(0.01) + print("Move finished") + self._done_moving() + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + @property + def axis_Id(self): + return self._axis_Id_alpha + + @axis_Id.setter + def axis_Id(self, val): + if isinstance(val, str): + if len(val) != 1: + raise ValueError(f"Only single-character axis_Ids are supported.") + self._axis_Id_alpha = val + self._axis_Id_numeric = ord(val.lower()) - 97 + else: + raise TypeError(f"Expected value of type str but received {type(val)}") + + @property + def axis_Id_numeric(self): + return self._axis_Id_numeric + + @axis_Id_numeric.setter + def axis_Id_numeric(self, val): + if isinstance(val, int): + if val > 26: + raise ValueError(f"Numeric value exceeds supported range.") + self._axis_Id_alpha = val + self._axis_Id_numeric = (chr(val + 97)).capitalize() + else: + raise TypeError(f"Expected value of type int but received {type(val)}") + + def kickoff(self, metadata, **kwargs) -> None: + self.controller.kickoff(metadata) + + @property + def egu(self): + """The engineering units (EGU) for positions""" + return "um" + + # how is this used later? + + def stage(self) -> List[object]: + return super().stage() + + def unstage(self) -> List[object]: + return super().unstage() + + def stop(self, *, success=False): + self.controller.stop_all_axes() + return super().stop(success=success) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + mock = False + if not mock: + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1) + rty.stage() + status = rty.move(0, wait=True) + status = rty.move(10, wait=True) + rty.read() + + rty.get() + rty.describe() + + rty.unstage() + else: + from ophyd_devices.utils.socket import SocketMock + + rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock) + rtx.stage() + # rty.stage() diff --git a/ophyd_devices/smaract/smaract_controller.py b/ophyd_devices/smaract/smaract_controller.py index 410b1fd..30f9314 100644 --- a/ophyd_devices/smaract/smaract_controller.py +++ b/ophyd_devices/smaract/smaract_controller.py @@ -3,8 +3,10 @@ import functools import json import logging import os +import time import numpy as np +from prettytable import PrettyTable from typeguard import typechecked from ophyd_devices.smaract.smaract_errors import ( @@ -73,7 +75,13 @@ class SmaractSensors: class SmaractController(Controller): _axes_per_controller = 6 _initialized = False - USER_ACCESS = ["socket_put_and_receive", "smaract_show_all", "move_open_loop_steps"] + USER_ACCESS = [ + "socket_put_and_receive", + "smaract_show_all", + "move_open_loop_steps", + "find_reference_mark", + "describe", + ] def __init__( self, @@ -117,7 +125,19 @@ class SmaractController(Controller): raise_if_not_status=False, ) -> str: self.socket_put(val) - return_val = self.socket_get() + return_val = "" + max_wait_time = 1 + elapsed_time = 0 + sleep_time = 0.01 + while True: + ret = self.socket_get() + return_val += ret + if ret.endswith("\n"): + break + time.sleep(sleep_time) + elapsed_time += sleep_time + if elapsed_time > max_wait_time: + break if remove_trailing_chars: return_val = self._remove_trailing_characters(return_val) logger.debug(f"Sending {val}; Returned {return_val}") @@ -234,15 +254,15 @@ class SmaractController(Controller): @axis_checked @typechecked def move_open_loop_steps( - self, axis_Id_numeric: int, steps: int, amplitude: int = 2000, frequency: int = 500 + self, axis_Id_numeric: int, steps: int, amplitude: int = 4000, frequency: int = 2000 ) -> None: - """Move open loop steps + """Move open loop steps. It performs a burst of steps with the given parameters. Args: axis_Id_numeric (int): Axis number. - steps (float): Relative position to move to in mm. - hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000. - + steps (int): Number and direction of steps to perform. The valid range is -30,000..30,000. A value of 0 stops the positioner, but see S command. A value of 30,000 or -30,000 performs an unbounded move. This should be used with caution since the positioner will only stop on an S command. + amplitude (int): Amplitude that the steps are performed with. Lower amplitude values result in a smaller step width. The parameter must be given as a 12bit value (range 0..4,095). 0 corresponds to 0V, 4,095 to 100V. Default: 4000 + frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000. """ self.socket_put_and_receive( f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", @@ -374,6 +394,15 @@ class SmaractController(Controller): if self._message_starts_with(return_val, f":ST{axis_Id_numeric}"): return self._sensors.avail_sensors.get(int(return_val.strip(f":ST{axis_Id_numeric},"))) + @retry_once + @axis_checked + def find_reference_mark( + self, axis_Id_numeric: int, direction: int, holdTime: int, autoZero: int + ) -> None: + return_val = self.socket_put_and_receive( + f"FRM{axis_Id_numeric},{direction},{holdTime},{autoZero}" + ) + @retry_once @axis_checked def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None: @@ -419,7 +448,7 @@ class SmaractController(Controller): "Closed Loop Speed", "Position", ] - for ax in range(self._Smaract_axis_per_controller): + for ax in range(self._axes_per_controller): axis = self._axis[ax] if axis is not None: t.add_row( @@ -428,7 +457,7 @@ class SmaractController(Controller): axis.name, axis.connected, self.axis_is_referenced(axis.axis_Id_numeric), - self.get_closed_loop_move_speed(axis.axis_Id), + self.get_closed_loop_move_speed(axis.axis_Id_numeric), axis.readback.read().get(axis.name).get("value"), ] ) diff --git a/ophyd_devices/utils/controller.py b/ophyd_devices/utils/controller.py index 98793b0..7180f4b 100644 --- a/ophyd_devices/utils/controller.py +++ b/ophyd_devices/utils/controller.py @@ -57,10 +57,6 @@ class Controller(OphydObject): labels=None, kind=None, ): - self.sock = None - self._socket_cls = socket_cls - self._socket_host = socket_host - self._socket_port = socket_port if not self._initialized: super().__init__( name=name, attr_name=attr_name, parent=parent, labels=labels, kind=kind @@ -69,6 +65,10 @@ class Controller(OphydObject): self._axis = [] self._initialize() self._initialized = True + self.sock = None + self._socket_cls = socket_cls + self._socket_host = socket_host + self._socket_port = socket_port def _initialize(self): self._connected = False @@ -133,7 +133,7 @@ class Controller(OphydObject): def off(self) -> None: """Close the socket connection to the controller""" - if self.connected or self.sock is not None: + if self.connected and self.sock is not None: self.sock.close() self.connected = False self.sock = None diff --git a/ophyd_devices/utils/socket.py b/ophyd_devices/utils/socket.py index 5afef82..ace2cf5 100644 --- a/ophyd_devices/utils/socket.py +++ b/ophyd_devices/utils/socket.py @@ -177,18 +177,29 @@ class SocketSignal(abc.ABC, Signal): class SocketIO: """SocketIO helper class for TCP IP connections""" - def __init__(self, host, port): + def __init__(self, host, port, max_retry=10): self.host = host self.port = port self.is_open = False + self.max_retry = max_retry self._initialize_socket() def connect(self): print(f"connecting to {self.host} port {self.port}") # self.sock.create_connection((host, port)) - if self.sock is None: - self._initialize_socket() - self.sock.connect((self.host, self.port)) + retry_count = 0 + while True: + try: + if self.sock is None: + self._initialize_socket() + self.sock.connect((self.host, self.port)) + break + except Exception as exc: + self.sock = None + time.sleep(2) + retry_count += 1 + if retry_count > self.max_retry: + raise exc def _put(self, msg_bytes): logger.debug(f"put message: {msg_bytes}") From 4760456e6318b66fa26f35205f669dbbf7d0e777 Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Tue, 12 Dec 2023 14:36:38 +0100 Subject: [PATCH 11/77] fix: online changes during flomni comm --- ophyd_devices/galil/fgalil_ophyd.py | 18 ++--------- ophyd_devices/galil/fupr_ophyd.py | 23 +++----------- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 38 +++++++++++------------ 3 files changed, 26 insertions(+), 53 deletions(-) diff --git a/ophyd_devices/galil/fgalil_ophyd.py b/ophyd_devices/galil/fgalil_ophyd.py index a351545..30f7049 100644 --- a/ophyd_devices/galil/fgalil_ophyd.py +++ b/ophyd_devices/galil/fgalil_ophyd.py @@ -131,11 +131,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced): class FlomniGalilMotor(Device, PositionerBase): USER_ACCESS = ["controller"] - readback = Cpt( - FlomniGalilReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint") motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config") motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") @@ -271,18 +267,10 @@ class FlomniGalilMotor(Device, PositionerBase): while self.motor_is_moving.get(): logger.info("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.1) val = self.readback.read() - success = np.isclose( - val[self.name]["value"], - position, - atol=self.tolerance, - ) + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) if not success: print(" stop") diff --git a/ophyd_devices/galil/fupr_ophyd.py b/ophyd_devices/galil/fupr_ophyd.py index 31866a7..f7aa2cf 100644 --- a/ophyd_devices/galil/fupr_ophyd.py +++ b/ophyd_devices/galil/fupr_ophyd.py @@ -8,8 +8,6 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError -from prettytable import PrettyTable - from ophyd_devices.galil.galil_ophyd import ( BECConfigError, GalilAxesReferenced, @@ -24,6 +22,7 @@ from ophyd_devices.galil.galil_ophyd import ( ) from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable logger = bec_logger.logger @@ -119,11 +118,7 @@ class FuprGalilAxesReferenced(GalilAxesReferenced): class FuprGalilMotor(Device, PositionerBase): USER_ACCESS = ["controller"] MOTOR_RESOLUTION = 25600 - readback = Cpt( - GalilReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint") motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config") motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") @@ -169,7 +164,7 @@ class FuprGalilMotor(Device, PositionerBase): raise BECConfigError( "device_mapping has been specified but the device_manager cannot be accessed." ) - self.rt = self.device_mapping.get("rt") + self.rt = self.device_mapping.get("rt", "rtx") super().__init__( prefix, @@ -258,18 +253,10 @@ class FuprGalilMotor(Device, PositionerBase): while self.motor_is_moving.get(): logger.info("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.1) val = self.readback.read() - success = np.isclose( - val[self.name]["value"], - position, - atol=self.tolerance, - ) + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) if not success: print(" stop") diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index 9acce7d..ce4cc43 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -122,7 +122,8 @@ class RtFlomniController(RtController): fsamx_in = fsamx.user_parameter.get("in") if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3): raise RtError( - "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." + "Something is wrong. fsamx is very far from the samx_in position. Don't dare" + " correct automatically." ) if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01): @@ -155,7 +156,8 @@ class RtFlomniController(RtController): self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage") if self.rt_pid_voltage is None: raise RtError( - "rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first." + "rt_pid_voltage not set in rtx user parameters. Please run" + " feedback_enable_with_reset first." ) logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}") expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100 @@ -171,7 +173,7 @@ class RtFlomniController(RtController): fsamx = self.get_device_manager().devices.fsamx fsamx.enabled_set = True fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") - fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.01 + fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007 logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}") fsamx_in = fsamx.user_parameter.get("in") fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True) @@ -234,7 +236,7 @@ class RtFlomniController(RtController): logger.info("Cyclic Error Compensation: y-axis is NOT initialized") def set_rotation_angle(self, val: float) -> None: - self.socket_put(f"a{(-val)/180*np.pi}") + self.socket_put(f"a{val/180*np.pi}") def laser_tracker_on(self): self.laser_update_tracker_info() @@ -369,10 +371,12 @@ class RtFlomniController(RtController): def start_scan(self): if not self.feedback_is_running(): logger.error( - "Cannot start scan because feedback loop is not running or there is an interferometer error." + "Cannot start scan because feedback loop is not running or there is an" + " interferometer error." ) raise RtError( - "Cannot start scan because feedback loop is not running or there is an interferometer error." + "Cannot start scan because feedback loop is not running or there is an" + " interferometer error." ) # here exception (mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status() @@ -465,15 +469,16 @@ class RtFlomniController(RtController): ) logger.info( - f"Flomni statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}." + "Flomni statistics: Average of all standard deviations: x" + f" {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y" + f" {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}." ) def publish_device_data(self, signals, pointID): self.get_device_manager().producer.send( MessageEndpoints.device_read("rt_flomni"), messages.DeviceMessage( - signals=signals, - metadata={"pointID": pointID, **self.readout_metadata}, + signals=signals, metadata={"pointID": pointID, **self.readout_metadata} ).dumps(), ) @@ -527,7 +532,8 @@ class RtFlomniSetpointSignal(RtSetpointSignal): """ if not self.parent.controller.feedback_is_running(): raise RtError( - "The interferometer feedback is not running. Either it is turned off or and interferometer error occured." + "The interferometer feedback is not running. Either it is turned off or and" + " interferometer error occured." ) self.set_with_feedback_disabled(val) @@ -545,11 +551,7 @@ class RtFlomniFeedbackRunning(RtSignalRO): class RtFlomniMotor(Device, PositionerBase): USER_ACCESS = ["controller"] - readback = Cpt( - RtFlomniReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(RtFlomniReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(RtFlomniSetpointSignal, signal_name="setpoint") high_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -673,11 +675,7 @@ class RtFlomniMotor(Device, PositionerBase): while not self.controller.slew_rate_limiters_on_target(): print("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.01) print("Move finished") self._done_moving() From 747aa36837fa823cd2f05e294e2ee9ee83074f43 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 12 Dec 2023 15:10:27 +0100 Subject: [PATCH 12/77] fix: fixed import after rebase conflict --- ophyd_devices/epics/devices/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 55677c8..82f6a51 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -3,7 +3,7 @@ from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO from ophyd.quadem import QuadEM from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal -from .DelayGeneratorDG645 import DelayGeneratorDG645 +from .delay_generator_csaxs import DelayGeneratorcSAXS from .eiger9m_csaxs import Eiger9McSAXS # cSAXS @@ -11,8 +11,9 @@ from .epics_motor_ex import EpicsMotorEx from .falcon_csaxs import FalconcSAXS from .flomni_sample_storage import FlomniSampleStorage from .InsertionDevice import InsertionDevice -from .mcs_csaxs import McsCsaxs +from .mcs_csaxs import MCScSAXS from .pilatus_csaxs import PilatuscSAXS +from .psi_detector_base import CustomDetectorMixin, PSIDetectorBase from .slits import SlitH, SlitV from .specMotors import ( Bpm4i, @@ -29,3 +30,5 @@ from .specMotors import ( ) from .SpmBase import SpmBase from .XbpmBase import XbpmBase, XbpmCsaxsOp + +# from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin From bf38e89f797bd72b45f2457df19c5c468af7cc9c Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 12 Dec 2023 15:38:18 +0100 Subject: [PATCH 13/77] refactor: formatting; fixed tests for expected return --- ophyd_devices/smaract/smaract_controller.py | 33 +++----- tests/test_smaract.py | 90 ++++++++------------- 2 files changed, 41 insertions(+), 82 deletions(-) diff --git a/ophyd_devices/smaract/smaract_controller.py b/ophyd_devices/smaract/smaract_controller.py index 30f9314..3a65e79 100644 --- a/ophyd_devices/smaract/smaract_controller.py +++ b/ophyd_devices/smaract/smaract_controller.py @@ -9,10 +9,7 @@ import numpy as np from prettytable import PrettyTable from typeguard import typechecked -from ophyd_devices.smaract.smaract_errors import ( - SmaractCommunicationError, - SmaractErrorCode, -) +from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked logger = logging.getLogger("smaract_controller") @@ -118,11 +115,7 @@ class SmaractController(Controller): @threadlocked def socket_put_and_receive( - self, - val: str, - remove_trailing_chars=True, - check_for_errors=True, - raise_if_not_status=False, + self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False ) -> str: self.socket_put(val) return_val = "" @@ -195,7 +188,7 @@ class SmaractController(Controller): return bool(int(return_val.split(",")[1])) def all_axes_referenced(self) -> bool: - return all([self.is_axis_referenced(ax) for ax in self._axis if ax is not None]) + return all(self.axis_is_referenced(ax) for ax in self._axis if ax is not None) @retry_once @axis_checked @@ -265,8 +258,7 @@ class SmaractController(Controller): frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000. """ self.socket_put_and_receive( - f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", - raise_if_not_status=True, + f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True ) @retry_once @@ -440,14 +432,7 @@ class SmaractController(Controller): def describe(self) -> None: t = PrettyTable() t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" - t.field_names = [ - "Axis", - "Name", - "Connected", - "Referenced", - "Closed Loop Speed", - "Position", - ] + t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"] for ax in range(self._axes_per_controller): axis = self._axis[ax] if axis is not None: @@ -512,7 +497,7 @@ class SmaractController(Controller): def _message_starts_with(self, msg: str, leading_chars: str) -> bool: if msg.startswith(leading_chars): return True - else: - raise SmaractCommunicationError( - f"Expected to receive a return message starting with {leading_chars} but instead received '{msg}'" - ) + raise SmaractCommunicationError( + f"Expected to receive a return message starting with {leading_chars} but instead" + f" received '{msg}'" + ) diff --git a/tests/test_smaract.py b/tests/test_smaract.py index 6c2d733..776b5e4 100644 --- a/tests/test_smaract.py +++ b/tests/test_smaract.py @@ -3,20 +3,17 @@ from utils import SocketMock from ophyd_devices.smaract import SmaractController from ophyd_devices.smaract.smaract_controller import SmaractCommunicationMode -from ophyd_devices.smaract.smaract_errors import ( - SmaractCommunicationError, - SmaractErrorCode, -) +from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode from ophyd_devices.smaract.smaract_ophyd import SmaractMotor @pytest.mark.parametrize( "axis,position,get_message,return_msg", [ - (0, 50, b":GP0\n", b":P0,50000000"), - (1, 0, b":GP1\n", b":P1,0"), - (0, -50, b":GP0\n", b":P0,-50000000"), - (0, -50.23, b":GP0\n", b":P0,-50230000"), + (0, 50, b":GP0\n", b":P0,50000000\n"), + (1, 0, b":GP1\n", b":P1,0\n"), + (0, -50, b":GP0\n", b":P0,-50000000\n"), + (0, -50.23, b":GP0\n", b":P0,-50230000\n"), ], ) def test_get_position(axis, position, get_message, return_msg): @@ -33,10 +30,10 @@ def test_get_position(axis, position, get_message, return_msg): @pytest.mark.parametrize( "axis,is_referenced,get_message,return_msg,exception", [ - (0, True, b":GPPK0\n", b":PPK0,1", None), - (1, True, b":GPPK1\n", b":PPK1,1", None), - (0, False, b":GPPK0\n", b":PPK0,0", None), - (200, False, b":GPPK0\n", b":PPK0,0", ValueError), + (0, True, b":GPPK0\n", b":PPK0,1\n", None), + (1, True, b":GPPK1\n", b":PPK1,1\n", None), + (0, False, b":GPPK0\n", b":PPK0,0\n", None), + (200, False, b":GPPK0\n", b":PPK0,0\n", ValueError), ], ) def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, exception): @@ -57,7 +54,7 @@ def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, except @pytest.mark.parametrize( "return_msg,exception,raised", [ - (b"false", SmaractCommunicationError, False), + (b"false\n", SmaractCommunicationError, False), (b":E0,1", SmaractErrorCode, True), (b":E,1", SmaractCommunicationError, True), (b":E,-1", SmaractCommunicationError, True), @@ -79,15 +76,11 @@ def test_socket_put_and_receive_raises_exception(return_msg, exception, raised): with pytest.raises(exception): controller.socket_put_and_receive(b"test") else: - assert controller.socket_put_and_receive(b"test") == return_msg.decode() + assert controller.socket_put_and_receive(b"test") == return_msg.split(b"\n")[0].decode() @pytest.mark.parametrize( - "mode,get_message,return_msg", - [ - (0, b":GCM\n", b":CM0"), - (1, b":GCM\n", b":CM1"), - ], + "mode,get_message,return_msg", [(0, b":GCM\n", b":CM0\n"), (1, b":GCM\n", b":CM1\n")] ) def test_communication_mode(mode, get_message, return_msg): SmaractController._reset_controller() @@ -103,16 +96,16 @@ def test_communication_mode(mode, get_message, return_msg): @pytest.mark.parametrize( "is_moving,get_message,return_msg", [ - (0, b":GS0\n", b":S0,0"), - (1, b":GS0\n", b":S0,1"), - (1, b":GS0\n", b":S0,2"), - (0, b":GS0\n", b":S0,3"), - (1, b":GS0\n", b":S0,4"), - (0, b":GS0\n", b":S0,5"), - (0, b":GS0\n", b":S0,6"), - (1, b":GS0\n", b":S0,7"), - (0, b":GS0\n", b":S0,9"), - (0, [b":GS0\n", b":GS0\n"], [b":E0,0", b":S0,9"]), + (0, b":GS0\n", b":S0,0\n"), + (1, b":GS0\n", b":S0,1\n"), + (1, b":GS0\n", b":S0,2\n"), + (0, b":GS0\n", b":S0,3\n"), + (1, b":GS0\n", b":S0,4\n"), + (0, b":GS0\n", b":S0,5\n"), + (0, b":GS0\n", b":S0,6\n"), + (1, b":GS0\n", b":S0,7\n"), + (0, b":GS0\n", b":S0,9\n"), + (0, [b":GS0\n", b":GS0\n"], [b":E0,0\n", b":S0,9"]), ], ) def test_axis_is_moving(is_moving, get_message, return_msg): @@ -131,9 +124,9 @@ def test_axis_is_moving(is_moving, get_message, return_msg): @pytest.mark.parametrize( "sensor_id,axis,get_msg,return_msg", [ - (1, 0, b":GST0\n", b":ST0,1"), - (6, 0, b":GST0\n", b":ST0,6"), - (6, 1, b":GST1\n", b":ST1,6"), + (1, 0, b":GST0\n", b":ST0,1\n"), + (6, 0, b":GST0\n", b":ST0,6\n"), + (6, 1, b":GST1\n", b":ST1,6\n"), ], ) def test_get_sensor_definition(sensor_id, axis, get_msg, return_msg): @@ -191,34 +184,29 @@ def test_move_axis_to_absolute_position(pos, axis, hold_time, get_msg, return_ms ( 50, [b":GPPK0\n", b":MPA0,50000000,1000\n", b":GS0\n", b":GP0\n"], - [b":PPK0,1", b":E0,0", b":S0,0", b":P0,50000000\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,50000000\n"], ), ( 0, [b":GPPK0\n", b":MPA0,0,1000\n", b":GS0\n", b":GP0\n"], - [b":PPK0,1", b":E0,0", b":S0,0", b":P0,0000000\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,0000000\n"], ), ( 20.23, [b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"], - [b":PPK0,1", b":E0,0", b":S0,0", b":P0,20230000\n"], + [b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"], ), ( 20.23, [b":GPPK0\n", b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"], - [b":S0,0", b":PPK0,1", b":E0,0", b":S0,0", b":P0,20230000\n"], + [b":S0,0\n", b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"], ), ], ) def test_move_axis(pos, get_msg, return_msg): SmaractController._reset_controller() lsmarA = SmaractMotor( - "A", - name="lsmarA", - host="mpc2680.psi.ch", - port=8085, - sign=1, - socket_cls=SocketMock, + "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock ) lsmarA.controller.on() controller = lsmarA.controller @@ -228,25 +216,11 @@ def test_move_axis(pos, get_msg, return_msg): assert controller.sock.buffer_put == get_msg -@pytest.mark.parametrize( - "num_axes,get_msg,return_msg", - [ - ( - 1, - [b":S0\n"], - [b":E0,0"], - ) - ], -) +@pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])]) def test_stop_axis(num_axes, get_msg, return_msg): SmaractController._reset_controller() lsmarA = SmaractMotor( - "A", - name="lsmarA", - host="mpc2680.psi.ch", - port=8085, - sign=1, - socket_cls=SocketMock, + "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock ) lsmarA.stage() controller = lsmarA.controller From 1c95220234b65f95c95d5033551e17a1689f1249 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 12 Dec 2023 19:27:52 +0100 Subject: [PATCH 14/77] test: added tests for fupr and flomni galil --- ophyd_devices/galil/fupr_ophyd.py | 31 +----- ophyd_devices/galil/galil_ophyd.py | 25 +---- tests/test_controller.py | 2 + tests/test_fupr_ophyd.py | 60 ++++++++++ tests/test_galil.py | 130 ++++++++-------------- tests/test_galil_flomni.py | 172 +++++++++++++++++++++++++++++ 6 files changed, 285 insertions(+), 135 deletions(-) create mode 100644 tests/test_fupr_ophyd.py create mode 100644 tests/test_galil_flomni.py diff --git a/ophyd_devices/galil/fupr_ophyd.py b/ophyd_devices/galil/fupr_ophyd.py index f7aa2cf..d0c26ed 100644 --- a/ophyd_devices/galil/fupr_ophyd.py +++ b/ophyd_devices/galil/fupr_ophyd.py @@ -8,6 +8,8 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError +from prettytable import PrettyTable + from ophyd_devices.galil.galil_ophyd import ( BECConfigError, GalilAxesReferenced, @@ -22,7 +24,6 @@ from ophyd_devices.galil.galil_ophyd import ( ) from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected -from prettytable import PrettyTable logger = bec_logger.logger @@ -315,31 +316,3 @@ class FuprGalilMotor(Device, PositionerBase): def stop(self, *, success=False): self.controller.stop_all_axes() return super().stop(success=success) - - -# if __name__ == "__main__": -# mock = False -# if not mock: -# leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1) -# leyey.stage() -# status = leyey.move(0, wait=True) -# status = leyey.move(10, wait=True) -# leyey.read() - -# leyey.get() -# leyey.describe() - -# leyey.unstage() -# else: -# from ophyd_devices.utils.socket import SocketMock - -# leyex = GalilMotor( -# "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock -# ) -# leyey = GalilMotor( -# "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock -# ) -# leyex.stage() -# # leyey.stage() - -# leyex.controller.galil_show_all() diff --git a/ophyd_devices/galil/galil_ophyd.py b/ophyd_devices/galil/galil_ophyd.py index a0292da..4498800 100644 --- a/ophyd_devices/galil/galil_ophyd.py +++ b/ophyd_devices/galil/galil_ophyd.py @@ -369,11 +369,7 @@ class GalilMotorIsMoving(GalilSignalRO): def get(self): val = super().get() if val is not None: - self._run_subs( - sub_type=self.SUB_VALUE, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) return val @@ -385,11 +381,7 @@ class GalilAxesReferenced(GalilSignalRO): class GalilMotor(Device, PositionerBase): USER_ACCESS = ["controller"] - readback = Cpt( - GalilReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint") motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config") motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal") @@ -520,18 +512,10 @@ class GalilMotor(Device, PositionerBase): while self.motor_is_moving.get(): logger.info("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.1) val = self.readback.read() - success = np.isclose( - val[self.name]["value"], - position, - atol=self.tolerance, - ) + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) if not success: print(" stop") @@ -593,6 +577,7 @@ class GalilMotor(Device, PositionerBase): if __name__ == "__main__": + # pytest: skip-file mock = False if not mock: leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1) diff --git a/tests/test_controller.py b/tests/test_controller.py index 6d69c13..6543587 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -14,6 +14,7 @@ def test_controller_off(): # make sure it is indempotent controller.off() + controller._reset_controller() def test_controller_on(): @@ -28,3 +29,4 @@ def test_controller_on(): # make sure it is indempotent controller.on() socket_cls().open.assert_called_once() + controller._reset_controller() diff --git a/tests/test_fupr_ophyd.py b/tests/test_fupr_ophyd.py new file mode 100644 index 0000000..62589b8 --- /dev/null +++ b/tests/test_fupr_ophyd.py @@ -0,0 +1,60 @@ +from unittest import mock + +import pytest +from utils import SocketMock + +from ophyd_devices.galil.fupr_ophyd import FuprGalilController, FuprGalilMotor + + +@pytest.fixture +def fsamroy(): + FuprGalilController._reset_controller() + fsamroy_motor = FuprGalilMotor( + "A", name="fsamroy", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + fsamroy_motor.controller.on() + assert isinstance(fsamroy_motor.controller, FuprGalilController) + yield fsamroy_motor + fsamroy_motor.controller.off() + fsamroy_motor.controller._reset_controller() + + +@pytest.mark.parametrize( + "pos,msg_received,msg_put,sign", + [ + (-0.5, b" -12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], 1), + (-0.5, b" 12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], -1), + ], +) +def test_axis_get(fsamroy, pos, msg_received, msg_put, sign): + fsamroy.sign = sign + fsamroy.device_manager = mock.MagicMock() + fsamroy.controller.sock.flush_buffer() + fsamroy.controller.sock.buffer_recv = msg_received + val = fsamroy.read() + assert val["fsamroy"]["value"] == pos + assert fsamroy.readback.get() == pos + assert fsamroy.controller.sock.buffer_put == msg_put + + +@pytest.mark.parametrize( + "target_pos,socket_put_messages,socket_get_messages", + [ + ( + 0, + [b"MG axisref\r", b"PAA=0\r", b"PAA=0\r", b"BGA\r"], + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) + ], +) +def test_axis_put(fsamroy, target_pos, socket_put_messages, socket_get_messages): + fsamroy.controller.sock.flush_buffer() + fsamroy.controller.sock.buffer_recv = socket_get_messages + fsamroy.user_setpoint.put(target_pos) + assert fsamroy.controller.sock.buffer_put == socket_put_messages + + +def test_drive_axis_to_limit(fsamroy): + fsamroy.controller.sock.flush_buffer() + with pytest.raises(NotImplementedError): + fsamroy.controller.drive_axis_to_limit(0, "forward") diff --git a/tests/test_galil.py b/tests/test_galil.py index 5628ef8..2a2991f 100644 --- a/tests/test_galil.py +++ b/tests/test_galil.py @@ -3,26 +3,36 @@ from unittest import mock import pytest from utils import SocketMock -from ophyd_devices.galil.galil_ophyd import GalilMotor +from ophyd_devices.galil.galil_ophyd import GalilController, GalilMotor -@pytest.mark.parametrize( - "pos,msg,sign", - [ - (1, b" -12800\n\r", 1), - (-1, b" -12800\n\r", -1), - ], -) -def test_axis_get(pos, msg, sign): - leyey = GalilMotor( - "H", - name="leyey", - host="mpc2680.psi.ch", - port=8081, - sign=sign, - socket_cls=SocketMock, +@pytest.fixture(scope="function") +def leyey(): + GalilController._reset_controller() + leyey_motor = GalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock ) - leyey.controller.on() + leyey_motor.controller.on() + yield leyey_motor + leyey_motor.controller.off() + leyey_motor.controller._reset_controller() + + +@pytest.fixture(scope="function") +def leyex(): + GalilController._reset_controller() + leyex_motor = GalilMotor( + "A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyex_motor.controller.on() + yield leyex_motor + leyex_motor.controller.off() + leyex_motor.controller._reset_controller() + + +@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)]) +def test_axis_get(leyey, pos, msg, sign): + leyey.sign = sign leyey.controller.sock.flush_buffer() leyey.controller.sock.buffer_recv = msg val = leyey.read() @@ -44,21 +54,11 @@ def test_axis_get(pos, msg, sign): b"XQ#NEWPAR\r", b"MG_XQ0\r", ], - [ - b"1.00", - b"-1", - b":", - b":", - b":", - b":", - b"-1", - ], - ), + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) ], ) -def test_axis_put(target_pos, socket_put_messages, socket_get_messages): - leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock) - leyey.controller.on() +def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): leyey.controller.sock.flush_buffer() leyey.controller.sock.buffer_recv = socket_get_messages leyey.user_setpoint.put(target_pos) @@ -82,17 +82,7 @@ def test_axis_put(target_pos, socket_put_messages, socket_get_messages): b"MG_XQ2\r", b"MG _LRA, _LFA\r", ], - [ - b":", - b":", - b":", - b":", - b"0", - b"0", - b"-1", - b"-1", - b"1.000 0.000", - ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], ), ( 1, @@ -108,27 +98,15 @@ def test_axis_put(target_pos, socket_put_messages, socket_get_messages): b"MG_XQ2\r", b"MG _LRB, _LFB\r", ], - [ - b":", - b":", - b":", - b":", - b"0", - b"0", - b"-1", - b"-1", - b"0.000 1.000", - ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], ), ], ) -def test_drive_axis_to_limit(axis_nr, direction, socket_put_messages, socket_get_messages): - leyey = GalilMotor("A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock) - leyey.controller.on() - leyey.controller.sock.flush_buffer() - leyey.controller.sock.buffer_recv = socket_get_messages - leyey.controller.drive_axis_to_limit(axis_nr, direction) - assert leyey.controller.sock.buffer_put == socket_put_messages +def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.drive_axis_to_limit(axis_nr, direction) + assert leyex.controller.sock.buffer_put == socket_put_messages @pytest.mark.parametrize( @@ -146,16 +124,7 @@ def test_drive_axis_to_limit(axis_nr, direction, socket_put_messages, socket_get b"MG_XQ2\r", b"MG axisref[0]\r", ], - [ - b":", - b":", - b":", - b"0", - b"0", - b"-1", - b"-1", - b"1.00", - ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ( 1, @@ -169,23 +138,12 @@ def test_drive_axis_to_limit(axis_nr, direction, socket_put_messages, socket_get b"MG_XQ2\r", b"MG axisref[1]\r", ], - [ - b":", - b":", - b":", - b"0", - b"0", - b"-1", - b"-1", - b"1.00", - ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ], ) -def test_find_reference(axis_nr, socket_put_messages, socket_get_messages): - leyey = GalilMotor("A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock) - leyey.controller.on() - leyey.controller.sock.flush_buffer() - leyey.controller.sock.buffer_recv = socket_get_messages - leyey.controller.find_reference(axis_nr) - assert leyey.controller.sock.buffer_put == socket_put_messages +def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.find_reference(axis_nr) + assert leyex.controller.sock.buffer_put == socket_put_messages diff --git a/tests/test_galil_flomni.py b/tests/test_galil_flomni.py new file mode 100644 index 0000000..cb7255a --- /dev/null +++ b/tests/test_galil_flomni.py @@ -0,0 +1,172 @@ +from unittest import mock + +import pytest +from utils import SocketMock + +from ophyd_devices.galil.fgalil_ophyd import FlomniGalilController, FlomniGalilMotor + + +@pytest.fixture(scope="function") +def leyey(): + FlomniGalilController._reset_controller() + leyey_motor = FlomniGalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyey_motor.controller.on() + yield leyey_motor + leyey_motor.controller.off() + leyey_motor.controller._reset_controller() + + +@pytest.fixture(scope="function") +def leyex(): + FlomniGalilController._reset_controller() + leyex_motor = FlomniGalilMotor( + "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + leyex_motor.controller.on() + yield leyex_motor + leyex_motor.controller.off() + leyex_motor.controller._reset_controller() + + +@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)]) +def test_axis_get(leyey, pos, msg, sign): + leyey.sign = sign + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = msg + val = leyey.read() + assert val["leyey"]["value"] == pos + assert leyey.readback.get() == pos + + +@pytest.mark.parametrize( + "target_pos,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"MG allaxref\r", + b"MG_XQ0\r", + b"naxis=7\r", + b"ntarget=0.000\r", + b"movereq=1\r", + b"XQ#NEWPAR\r", + b"MG_XQ0\r", + ], + [b"1.00", b"-1", b":", b":", b":", b":", b"-1"], + ) + ], +) +def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): + leyey.controller.sock.flush_buffer() + leyey.controller.sock.buffer_recv = socket_get_messages + leyey.user_setpoint.put(target_pos) + assert leyey.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,direction,socket_put_messages,socket_get_messages", + [ + ( + 0, + "forward", + [ + b"naxis=0\r", + b"ndir=1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG _LRA, _LFA\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], + ), + ( + 1, + "reverse", + [ + b"naxis=1\r", + b"ndir=-1\r", + b"XQ#NEWPAR\r", + b"XQ#FES\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG _LRB, _LFB\r", + ], + [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], + ), + ], +) +def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.drive_axis_to_limit(axis_nr, direction) + assert leyex.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_nr,socket_put_messages,socket_get_messages", + [ + ( + 0, + [ + b"naxis=0\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG_XQ0\r", + b"MG _MOA\r", + b"MG axisref[0]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ( + 1, + [ + b"naxis=1\r", + b"XQ#NEWPAR\r", + b"XQ#FRM\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG_XQ0\r", + b"MG _MOB\r", + b"MG axisref[1]\r", + ], + [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + ), + ], +) +def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages): + leyex.controller.sock.flush_buffer() + leyex.controller.sock.buffer_recv = socket_get_messages + leyex.controller.find_reference(axis_nr) + assert leyex.controller.sock.buffer_put == socket_put_messages + + +@pytest.mark.parametrize( + "axis_Id,socket_put_messages,socket_get_messages,triggered", + [ + ("A", [b"MG @IN[14]\r"], [b" 1.0000\n"], True), + ("B", [b"MG @IN[14]\r"], [b" 0.0000\n"], False), + ], +) +def test_fosaz_light_curtain_is_triggered( + axis_Id, socket_put_messages, socket_get_messages, triggered +): + """test that the light curtain is triggered""" + fosaz = FlomniGalilMotor( + axis_Id, name="fosaz", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock + ) + fosaz.controller.on() + fosaz.controller.sock.flush_buffer() + fosaz.controller.sock.buffer_recv = socket_get_messages + assert fosaz.controller.fosaz_light_curtain_is_triggered() == triggered + assert fosaz.controller.sock.buffer_put == socket_put_messages + fosaz.controller.off() + fosaz.controller._reset_controller() From 5e9d3aed17ce02142f12ba69ea562d6c30b41ae3 Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Tue, 12 Dec 2023 09:13:58 +0100 Subject: [PATCH 15/77] feat: flomni stages --- ophyd_devices/epics/devices/__init__.py | 2 -- ophyd_devices/galil/fgalil_ophyd.py | 3 ++- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 9 ++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 82f6a51..86ceba7 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -30,5 +30,3 @@ from .specMotors import ( ) from .SpmBase import SpmBase from .XbpmBase import XbpmBase, XbpmCsaxsOp - -# from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin diff --git a/ophyd_devices/galil/fgalil_ophyd.py b/ophyd_devices/galil/fgalil_ophyd.py index 30f7049..7cebee9 100644 --- a/ophyd_devices/galil/fgalil_ophyd.py +++ b/ophyd_devices/galil/fgalil_ophyd.py @@ -8,6 +8,8 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError +from prettytable import PrettyTable + from ophyd_devices.galil.galil_ophyd import ( BECConfigError, GalilAxesReferenced, @@ -23,7 +25,6 @@ from ophyd_devices.galil.galil_ophyd import ( ) from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected -from prettytable import PrettyTable logger = bec_logger.logger diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index ce4cc43..bf7bd88 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -9,6 +9,8 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError +from prettytable import PrettyTable + from ophyd_devices.rt_lamni.rt_ophyd import ( BECConfigError, RtCommunicationError, @@ -21,7 +23,6 @@ from ophyd_devices.rt_lamni.rt_ophyd import ( ) from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected -from prettytable import PrettyTable logger = bec_logger.logger @@ -122,8 +123,7 @@ class RtFlomniController(RtController): fsamx_in = fsamx.user_parameter.get("in") if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3): raise RtError( - "Something is wrong. fsamx is very far from the samx_in position. Don't dare" - " correct automatically." + "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." ) if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01): @@ -156,8 +156,7 @@ class RtFlomniController(RtController): self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage") if self.rt_pid_voltage is None: raise RtError( - "rt_pid_voltage not set in rtx user parameters. Please run" - " feedback_enable_with_reset first." + "rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first." ) logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}") expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100 From 366f592e08a4cb50ddae7b3f8ba3aa214574f61f Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Fri, 16 Feb 2024 08:27:06 +0100 Subject: [PATCH 16/77] feat(galil): added lights on/off --- ophyd_devices/galil/fgalil_ophyd.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ophyd_devices/galil/fgalil_ophyd.py b/ophyd_devices/galil/fgalil_ophyd.py index 7cebee9..b30e30b 100644 --- a/ophyd_devices/galil/fgalil_ophyd.py +++ b/ophyd_devices/galil/fgalil_ophyd.py @@ -42,6 +42,8 @@ class FlomniGalilController(GalilController): "fosaz_light_curtain_is_triggered", "is_motor_on", "all_axes_referenced", + "lights_off", + "lights_on", ] def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool: @@ -65,6 +67,18 @@ class FlomniGalilController(GalilController): return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1 + def lights_off(self) -> None: + """ + Turn off the lights + """ + self.socket_put_confirmed("CB15") + + def lights_on(self) -> None: + """ + Turn on the lights + """ + self.socket_put_confirmed("SB15") + class FlomniGalilReadbackSignal(GalilSignalRO): @retry_once From b9e777c14e1d4e8804c89e2c401ba5956316bb22 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 16 Feb 2024 08:51:22 +0100 Subject: [PATCH 17/77] refactor(fgalil): cleanup --- ophyd_devices/galil/fgalil_ophyd.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ophyd_devices/galil/fgalil_ophyd.py b/ophyd_devices/galil/fgalil_ophyd.py index b30e30b..89707b9 100644 --- a/ophyd_devices/galil/fgalil_ophyd.py +++ b/ophyd_devices/galil/fgalil_ophyd.py @@ -1,4 +1,3 @@ -import functools import threading import time @@ -7,24 +6,21 @@ from bec_lib import bec_logger from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait -from ophyd.utils import LimitError, ReadOnlyError -from prettytable import PrettyTable +from ophyd.utils import LimitError from ophyd_devices.galil.galil_ophyd import ( BECConfigError, GalilAxesReferenced, - GalilCommunicationError, GalilController, GalilError, GalilMotorIsMoving, GalilMotorResolution, - GalilReadbackSignal, GalilSetpointSignal, GalilSignalRO, retry_once, ) -from ophyd_devices.utils.controller import Controller, threadlocked -from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from ophyd_devices.utils.controller import threadlocked +from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected logger = bec_logger.logger @@ -310,7 +306,7 @@ class FlomniGalilMotor(Device, PositionerBase): def axis_Id(self, val): if isinstance(val, str): if len(val) != 1: - raise ValueError(f"Only single-character axis_Ids are supported.") + raise ValueError("Only single-character axis_Ids are supported.") self._axis_Id_alpha = val self._axis_Id_numeric = ord(val.lower()) - 97 else: @@ -324,7 +320,7 @@ class FlomniGalilMotor(Device, PositionerBase): def axis_Id_numeric(self, val): if isinstance(val, int): if val > 26: - raise ValueError(f"Numeric value exceeds supported range.") + raise ValueError("Numeric value exceeds supported range.") self._axis_Id_alpha = val self._axis_Id_numeric = (chr(val + 97)).capitalize() else: From 6d7fd5fd9f8c293c834a0e321348f31855658744 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 16 Feb 2024 09:28:20 +0100 Subject: [PATCH 18/77] test(rt_flomni): added tests --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 7 ++- tests/test_rt_flomni.py | 58 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 tests/test_rt_flomni.py diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index bf7bd88..1627aaf 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -1,4 +1,3 @@ -import functools import threading import time from typing import List @@ -8,7 +7,7 @@ from bec_lib import MessageEndpoints, bec_logger, messages from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait -from ophyd.utils import LimitError, ReadOnlyError +from ophyd.utils import LimitError from prettytable import PrettyTable from ophyd_devices.rt_lamni.rt_ophyd import ( @@ -21,8 +20,8 @@ from ophyd_devices.rt_lamni.rt_ophyd import ( RtSignalRO, retry_once, ) -from ophyd_devices.utils.controller import Controller, threadlocked -from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from ophyd_devices.utils.controller import threadlocked +from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected logger = bec_logger.logger diff --git a/tests/test_rt_flomni.py b/tests/test_rt_flomni.py new file mode 100644 index 0000000..fa754a5 --- /dev/null +++ b/tests/test_rt_flomni.py @@ -0,0 +1,58 @@ +from unittest import mock + +import pytest +from utils import SocketMock + +from ophyd_devices.rt_lamni import RtFlomniController, RtFlomniMotor + + +@pytest.fixture() +def rt_flomni(): + rt_flomni = RtFlomniController( + name="rt_flomni", socket_cls=SocketMock, socket_host="localhost", socket_port=8081 + ) + with mock.patch.object(rt_flomni, "get_device_manager"): + with mock.patch.object(rt_flomni, "sock"): + rtx = mock.MagicMock(spec=RtFlomniMotor) + rtx.name = "rtx" + rty = mock.MagicMock(spec=RtFlomniMotor) + rty.name = "rty" + rtz = mock.MagicMock(spec=RtFlomniMotor) + rtz.name = "rtz" + rt_flomni.set_axis(rtx, 0) + rt_flomni.set_axis(rty, 1) + rt_flomni.set_axis(rtz, 2) + yield rt_flomni + + +def test_rt_flomni_move_to_zero(rt_flomni): + rt_flomni.move_to_zero() + assert rt_flomni.sock.mock_calls == [ + mock.call.put(b"pa0,0\n"), + mock.call.put(b"pa1,0\n"), + mock.call.put(b"pa2,0\n"), + ] + + +@pytest.mark.parametrize("return_value,is_running", [(b"1.00\n", False), (b"0.00\n", True)]) +def test_rt_flomni_feedback_is_running(rt_flomni, return_value, is_running): + rt_flomni.sock.receive.return_value = return_value + assert rt_flomni.feedback_is_running() == is_running + assert mock.call.put(b"l2\n") in rt_flomni.sock.mock_calls + + +def test_feedback_enable_with_reset(rt_flomni): + + device_manager = rt_flomni.get_device_manager() + device_manager.devices.fsamx.user_parameter.get.return_value = 0.05 + device_manager.devices.fsamx.obj.readback.get.return_value = 0.05 + + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + with mock.patch.object(rt_flomni, "pid_y", return_value=0.05): + with mock.patch.object( + rt_flomni, "slew_rate_limiters_on_target", return_value=True + ) as slew_rate_limiters_on_target: + + rt_flomni.feedback_enable_with_reset() + laser_tracker_on.assert_called_once() From a48bc73cc581edf660a1132917de3b96288b11d7 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 16 Feb 2024 09:52:46 +0000 Subject: [PATCH 19/77] 0.21.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 13 +++++++++++++ setup.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fb0ec..3321e4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ +## v0.21.0 (2024-02-16) + +### Feature + +* **galil:** Added lights on/off ([`366f592`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/366f592e08a4cb50ddae7b3f8ba3aa214574f61f)) +* Flomni stages ([`5e9d3ae`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/5e9d3aed17ce02142f12ba69ea562d6c30b41ae3)) +* Flomni stages ([`b808659`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/b808659d4d8b1af262d6f62174b027b0736a005a)) + +### Fix + +* Fixed import after rebase conflict ([`747aa36`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/747aa36837fa823cd2f05e294e2ee9ee83074f43)) +* Online changes during flomni comm ([`4760456`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/4760456e6318b66fa26f35205f669dbbf7d0e777)) + ## v0.20.1 (2024-02-13) ### Fix diff --git a/setup.py b/setup.py index 31422eb..6e955a0 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.20.1" +__version__ = "0.21.0" if __name__ == "__main__": setup( From 4159f3e3ec20727b395808118f3c0c166d9d1c0c Mon Sep 17 00:00:00 2001 From: Mathias Guijarro Date: Tue, 13 Feb 2024 15:43:51 +0100 Subject: [PATCH 20/77] fix(deprecation): remove all remaining .dumps(), .loads() and producer->connector --- ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py | 7 ++----- ophyd_devices/epics/devices/eiger9m_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/falcon_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/mcs_csaxs.py | 4 ++-- ophyd_devices/epics/devices/pilatus_csaxs.py | 10 +++++----- .../epics/devices/psi_delay_generator_base.py | 2 +- .../epics/devices/psi_detector_base.py | 2 +- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 16 ++++++++-------- ophyd_devices/sim/sim.py | 18 ++++++++---------- ophyd_devices/sim/sim_test_devices.py | 18 ++++++++---------- ophyd_devices/sim/sim_xtreme.py | 4 ++-- ophyd_devices/utils/bec_scaninfo_mixin.py | 4 ++-- ophyd_devices/utils/bec_utils.py | 14 +++++++------- tests/test_eiger9m_csaxs.py | 14 +++++++------- tests/test_falcon_csaxs.py | 12 ++++++------ tests/test_mcs_card.py | 8 ++++---- tests/test_pilatus_csaxs.py | 12 ++++++------ tests/utils.py | 4 ++-- 18 files changed, 81 insertions(+), 88 deletions(-) diff --git a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py index 256b124..582e88e 100644 --- a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py +++ b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py @@ -81,8 +81,7 @@ class Eiger1p5MDetector(Device): self.username = "e20588" # TODO get from config def _get_current_scan_msg(self) -> messages.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return messages.ScanStatusMessage.loads(msg) + return self.device_manager.connector.get(MessageEndpoints.scan_status()) def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None): if leading_zeros is None: @@ -159,9 +158,7 @@ class Eiger1p5MDetector(Device): self.detector_control.put("stop") signals = {"config": self.read(), "data": self.file_name} msg = messages.DeviceMessage(signals=signals, metadata=self.metadata) - self.device_manager.producer.set_and_publish( - MessageEndpoints.device_read(self.name), msg.dumps() - ) + self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg) self._stopped = False return super().unstage() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 219453f..9873326 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -301,20 +301,20 @@ class Eiger9MSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index fea5173..09d61ff 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -244,20 +244,20 @@ class FalconSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index f87b809..5158b78 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -136,8 +136,8 @@ class MCSSetup(CustomDetectorMixin): msg = messages.DeviceMessage( signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata, - ).dumps() - self.parent.producer.xadd( + ) + self.parent.connector.xadd( topic=MessageEndpoints.device_async_readback( scanID=self.parent.scaninfo.scanID, device=self.parent.name ), diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index ee13b98..390db83 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -331,7 +331,7 @@ class PilatusSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage( file_path=self.parent.filepath, @@ -345,13 +345,13 @@ class PilatusSetup(CustomDetectorMixin): successful=successful, metadata={"input_path": self.parent.filepath_raw}, ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/psi_delay_generator_base.py b/ophyd_devices/epics/devices/psi_delay_generator_base.py index 76641c6..5068343 100644 --- a/ophyd_devices/epics/devices/psi_delay_generator_base.py +++ b/ophyd_devices/epics/devices/psi_delay_generator_base.py @@ -391,7 +391,7 @@ class PSIDelayGeneratorBase(Device): self.device_manager = device_manager else: self.device_manager = bec_utils.DMMock() - self.producer = self.device_manager.producer + self.connector = self.device_manager.connector self._update_scaninfo() self._init() diff --git a/ophyd_devices/epics/devices/psi_detector_base.py b/ophyd_devices/epics/devices/psi_detector_base.py index 176f82c..36450d2 100644 --- a/ophyd_devices/epics/devices/psi_detector_base.py +++ b/ophyd_devices/epics/devices/psi_detector_base.py @@ -228,7 +228,7 @@ class PSIDetectorBase(Device): self.device_manager = bec_utils.DMMock() base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/" self.service_cfg = {"base_path": os.path.expanduser(base_path)} - self.producer = self.device_manager.producer + self.connector = self.device_manager.connector self._update_scaninfo() self._update_filewriter() self._init() diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 0e70d64..72bbd8a 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -308,9 +308,9 @@ class RtLamniController(Controller): def _update_flyer_device_info(self): flyer_info = self._get_flyer_device_info() - self.get_device_manager().producer.set( + self.get_device_manager().connector.set( MessageEndpoints.device_info("rt_scan"), - messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info), ) def _get_flyer_device_info(self) -> dict: @@ -385,11 +385,11 @@ class RtLamniController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata - ).dumps(), + ), ) # while scan is running while mode > 0: @@ -420,11 +420,11 @@ class RtLamniController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata - ).dumps(), + ), ) logger.info( @@ -432,12 +432,12 @@ class RtLamniController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().producer.send( + self.get_device_manager().connector.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata}, - ).dumps(), + ), ) def feedback_status_angle_lamni(self) -> bool: diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 01e1508..496710d 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -476,34 +476,32 @@ class SynFlyer(Device, PositionerBase): } }, 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() + device.device_manager.connector.send( + MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.producer.set_and_publish( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.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( + device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ).dumps(), + ), ) print("done") diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index 2030b66..6f3029c 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -115,34 +115,32 @@ class SynFlyerLamNI(Device, PositionerBase): } }, 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() + device.device_manager.connector.send( + MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.producer.set_and_publish( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.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( + device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ).dumps(), + ), ) print("done") diff --git a/ophyd_devices/sim/sim_xtreme.py b/ophyd_devices/sim/sim_xtreme.py index fea60b5..fcd80b5 100644 --- a/ophyd_devices/sim/sim_xtreme.py +++ b/ophyd_devices/sim/sim_xtreme.py @@ -357,8 +357,8 @@ class SynXtremeOtfReplay(FlyerInterface, Device): } msg = messages.DeviceMessage( signals=signals, metadata=self._device_manager.devices.otf.metadata - ).dumps() - self._device_manager.producer.set_and_publish( + ) + self._device_manager.connector.set_and_publish( MessageEndpoints.device_readback("signals"), msg ) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index 417259b..f005927 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -89,7 +89,7 @@ class BecScaninfoMixin: messages.ScanStatusMessage: messages.ScanStatusMessage object """ if not self.sim_mode: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + msg = self.device_manager.connector.get(MessageEndpoints.scan_status()) if not isinstance(msg, messages.ScanStatusMessage): return None return msg @@ -105,7 +105,7 @@ class BecScaninfoMixin: if self.sim_mode: return getpass.getuser() - msg = self.device_manager.producer.get(MessageEndpoints.account()) + msg = self.device_manager.connector.get(MessageEndpoints.account()) if msg: return msg return getpass.getuser() diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 94c9cf7..756716f 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -122,17 +122,17 @@ class ProducerMock: class PipelineMock: _pipe_buffer = [] - _producer = None + _connector = None - def __init__(self, producer) -> None: - self._producer = producer + def __init__(self, connector) -> None: + self._connector = connector def execute(self): - if not self._producer.store_data: + if not self._connector.store_data: self._pipe_buffer = [] return [] res = [ - getattr(self._producer, method)(*args, **kwargs) + getattr(self._connector, method)(*args, **kwargs) for method, args, kwargs in self._pipe_buffer ] self._pipe_buffer = [] @@ -142,13 +142,13 @@ class PipelineMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a producer. + The mocked DeviceManager creates a device containert and a connector. """ def __init__(self): self.devices = DeviceContainer() - self.producer = ProducerMock() + self.connector = ProducerMock() def add_device(self, name: str, value: float = 0.0): self.devices[name] = DeviceMock(name, value) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 00ffd80..cf25ec3 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -50,7 +50,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -428,24 +428,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls def test_stop(mock_det): diff --git a/tests/test_falcon_csaxs.py b/tests/test_falcon_csaxs.py index 2622a68..b07915e 100644 --- a/tests/test_falcon_csaxs.py +++ b/tests/test_falcon_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-SITORO:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -215,24 +215,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/test_mcs_card.py b/tests/test_mcs_card.py index ac62bc6..841dac6 100644 --- a/tests/test_mcs_card.py +++ b/tests/test_mcs_card.py @@ -32,7 +32,7 @@ def mock_det(): prefix = "X12SA-MCS:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -53,7 +53,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -184,7 +184,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): mock_det.custom_prepare._send_data_to_bec() device_metadata = mock_det.scaninfo.scan_msg.metadata metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()}) - data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata).dumps() + data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata) calls = mock.call( topic=MessageEndpoints.device_async_readback( scanID=metadata["scanID"], device=mock_det.name @@ -193,7 +193,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): expire=1800, ) - assert mock_det.producer.xadd.call_args == calls + assert mock_det.connector.xadd.call_args == calls @pytest.mark.parametrize( diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 81c4629..642e0e0 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -28,7 +28,7 @@ def mock_det(): prefix = "X12SA-ES-PILATUS300K:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -207,27 +207,27 @@ def test_publish_file_location(mock_det, scaninfo): file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, - ).dumps() + ) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, successful=scaninfo["successful"], - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/utils.py b/tests/utils.py index 84ebce8..8d0fd6b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -296,13 +296,13 @@ class DeviceMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a producer. + The mocked DeviceManager creates a device containert and a connector. """ def __init__(self): self.devices = DeviceContainer() - self.producer = ProducerMock() + self.connector = ProducerMock() def add_device(self, name: str, value: float = 0.0): """Add device to the DeviceManagerMock""" From 9642840714bd80fe4d7bb518a10c3c5c83fa828f Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sat, 17 Feb 2024 13:46:45 +0000 Subject: [PATCH 21/77] 0.21.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3321e4a..9373363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.21.1 (2024-02-17) + +### Fix + +* **deprecation:** Remove all remaining .dumps(), .loads() and producer->connector ([`4159f3e`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/4159f3e3ec20727b395808118f3c0c166d9d1c0c)) + ## v0.21.0 (2024-02-16) ### Feature diff --git a/setup.py b/setup.py index 6e955a0..d705123 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.21.0" +__version__ = "0.21.1" if __name__ == "__main__": setup( From 851a088b81cfd7e9d323955f923174a394155bfd Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 7 Feb 2024 15:34:41 +0100 Subject: [PATCH 22/77] feat: extend sim_data to allow execution from function of secondary devices extracted from lookup --- ophyd_devices/sim/sim.py | 1 - ophyd_devices/sim/sim_data.py | 246 +++++++++++++++++++++++----------- 2 files changed, 168 insertions(+), 79 deletions(-) diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 496710d..58be827 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -251,7 +251,6 @@ class SimPositioner(Device, PositionerBase): low_limit_travel = Cpt(SetableSignal, value=0, kind=Kind.omitted) unused = Cpt(Signal, value=1, kind=Kind.omitted) - # TODO add short description to these two lines and explain what this does SUB_READBACK = "readback" _default_sub = SUB_READBACK diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 22dbc8f..eab7618 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -1,6 +1,9 @@ -from abc import ABC, abstractmethod +from __future__ import annotations + from collections import defaultdict + import enum +import inspect import time as ttime import numpy as np @@ -28,6 +31,13 @@ class NoiseType(str, enum.Enum): POISSON = "poisson" +class HotPixelType(str, enum.Enum): + """Type of hot pixel to add to simulated data.""" + + CONSTANT = "constant" + FLUCTUATING = "fluctuating" + + class SimulatedDataBase: USER_ACCESS = [ "get_sim_params", @@ -38,12 +48,42 @@ class SimulatedDataBase: def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None: self.parent = parent - self.sim_state = defaultdict(lambda: {}) - self._all_params = defaultdict(lambda: {}) + self.sim_state = defaultdict(dict) + self._all_params = defaultdict(dict) self.device_manager = device_manager self._simulation_type = None + self.lookup_table = getattr(self.parent, "lookup_table", None) self.init_paramaters(**kwargs) self._active_params = self._all_params.get(self._simulation_type, None) + # self.register_in_lookup_table() + + # self.lookup_table = self.update_lookup_table() + + # def update_lookup_table(self) -> None: + # """Update the lookup table with the new value for the signal.""" + # table = getattr(self.device_manager.lookup_table, self.parent.name, None) + + # return getattr(self.device_manager.lookup_table, self.parent.name, None) + + # def register_in_lookup_table(self) -> None: + # """Register the simulated device in the lookup table.""" + # self.device_manager.lookup_table[self.parent.name] = {"obj": self, "method": "_compute_sim_state", "args": (), "kwargs": {}} + + def execute_simulation_method(self, *args, method=None, **kwargs) -> any: + """Execute the method from the lookup table.""" + + if self.lookup_table and self.parent.name in self.lookup_table: + # obj = self.parent.lookup_table[self.parent.name]["obj"] + method = self.lookup_table[self.parent.name]["method"] + args = self.lookup_table[self.parent.name]["args"] + kwargs = self.lookup_table[self.parent.name]["kwargs"] + # Do I need args and kwargs! Why!! + + if method is not None: + method_arguments = list(inspect.signature(method).parameters.keys()) + if all([True for arg in method_arguments if arg in args or arg in kwargs]): + return method(*args, **kwargs) + raise SimulatedDataException(f"Method {method} is not available for {self.parent.name}") def init_paramaters(self, **kwargs): """Initialize the parameters for the Simulated Data @@ -119,13 +159,16 @@ class SimulatedDataBase: self.sim_state[signal_name]["value"] = value self.sim_state[signal_name]["timestamp"] = ttime.time() - def _update_init_params(self, sim_type_default: SimulationType) -> None: + def _update_init_params( + self, + sim_type_default: SimulationType, + ) -> None: """Update the initial parameters of the simulated data with input from deviceConfig. Args: sim_type_default (SimulationType): Default simulation type to use if not specified in deviceConfig. """ - init_params = self.parent.init_sim_params + init_params = getattr(self.parent, "init_sim_params", None) for sim_type in self._all_params.values(): for sim_type_config_element in sim_type: if init_params: @@ -191,10 +234,13 @@ class SimulatedDataMonitor(SimulatedDataBase): signal_name (str): Name of the signal to update. """ if self.get_sim_type() == SimulationType.CONSTANT: - value = self._compute_constant() + method = "_compute_constant" + # value = self._compute_constant() elif self.get_sim_type() == SimulationType.GAUSSIAN: - value = self._compute_gaussian() + method = "_compute_gaussian" + # value = self._compute_gaussian() + value = self.execute_simulation_method(method=getattr(self, method)) self.update_sim_state(signal_name, value) def _compute_constant(self) -> float: @@ -210,12 +256,10 @@ class SimulatedDataMonitor(SimulatedDataBase): v = self._active_params["amp"] return v else: - # TODO Propagate msg to client! - logger.warning( + raise SimulatedDataException( f"Unknown noise type {self._active_params['noise']}. Please choose from 'poisson'," - " 'uniform' or 'none'. Returning 0." + " 'uniform' or 'none'." ) - return 0 def _compute_gaussian(self) -> float: """Computes return value for sim_type = "gauss". @@ -243,12 +287,9 @@ class SimulatedDataMonitor(SimulatedDataBase): v += np.random.uniform(-1, 1) * params["noise_multiplier"] return v except SimulatedDataException as exc: - # TODO Propagate msg to client! - logger.warning( - f"Could not compute gaussian for {params['ref_motor']} with {exc} raised." - "Returning 0 instead." - ) - return 0 + raise SimulatedDataException( + f"Could not compute gaussian for {self.parent.name} with {exc} raised. Deactivate eiger to continue." + ) from exc class SimulatedDataCamera(SimulatedDataBase): @@ -282,13 +323,27 @@ class SimulatedDataCamera(SimulatedDataBase): "amp": 100, "noise": NoiseType.POISSON, "noise_multiplier": 0.1, + "hot_pixel": { + "coords": np.array([[100, 100], [200, 200]]), + "type": [HotPixelType.CONSTANT, HotPixelType.FLUCTUATING], + "value": [1e6, 1e4], + }, }, SimulationType.GAUSSIAN: { - "amp": 100, + "amp": 500, "cen_off": np.array([0, 0]), "cov": np.array([[10, 5], [5, 10]]), "noise": NoiseType.NONE, "noise_multiplier": 0.1, + "hot_pixel": { + "coords": np.array([[240, 240], [50, 20], [40, 400]]), + "type": [ + HotPixelType.FLUCTUATING, + HotPixelType.CONSTANT, + HotPixelType.FLUCTUATING, + ], + "value": np.array([1e4, 1e6, 1e4]), + }, }, } # Update init parameters and set simulation type to Gaussian if not specified otherwise in init_sim_params @@ -304,36 +359,33 @@ class SimulatedDataCamera(SimulatedDataBase): signal_name (str): Name of the signal to update. """ if self.get_sim_type() == SimulationType.CONSTANT: - value = self._compute_constant() + method = "_compute_constant" + # value = self._compute_constant() elif self.get_sim_type() == SimulationType.GAUSSIAN: - value = self._compute_gaussian() + method = "_compute_gaussian" + # value = self._compute_gaussian() + + value = self.execute_simulation_method(method=getattr(self, method)) self.update_sim_state(signal_name, value) def _compute_constant(self) -> float: """Compute a return value for sim_type = Constant.""" - - # tuple with shape - shape = self.sim_state[self.parent.image_shape.name]["value"] - v = self._active_params["amp"] * np.ones(shape, dtype=np.uint16) - if self._active_params["noise"] == NoiseType.POISSON: - v = np.random.poisson(np.round(v), v.shape) - return v - if self._active_params["noise"] == NoiseType.UNIFORM: - multiplier = self._active_params["noise_multiplier"] - v += np.random.randint(-multiplier, multiplier, v.shape) - return v - if self._active_params["noise"] == NoiseType.NONE: - return v - # TODO Propagate msg to client! - logger.warning( - f"Unknown noise type {self._active_params['noise']}. Please choose from 'poisson'," - " 'uniform' or 'none'. Returning 0." - ) - return 0 + try: + shape = self.sim_state[self.parent.image_shape.name]["value"] + v = self._active_params["amp"] * np.ones(shape, dtype=np.uint16) + return self._add_noise(v, self._active_params["noise"]) + except SimulatedDataException as exc: + raise SimulatedDataException( + f"Could not compute constant for {self.parent.name} with {exc} raised. Deactivate eiger to continue." + ) from exc def _compute_multivariate_gaussian( - self, pos: np.ndarray, cen_off: np.ndarray, cov: np.ndarray + self, + pos: np.ndarray | list, + cen_off: np.ndarray | list, + cov: np.ndarray | list, + amp: float, ) -> np.ndarray: """Computes and returns the multivariate Gaussian distribution. @@ -345,16 +397,80 @@ class SimulatedDataCamera(SimulatedDataBase): Returns: np.ndarray: Multivariate Gaussian distribution. """ - + if isinstance(pos, list): + pos = np.array(pos) + if isinstance(cen_off, list): + cen_off = np.array(cen_off) + if isinstance(cov, list): + cov = np.array(cov) dim = cen_off.shape[0] cov_det = np.linalg.det(cov) cov_inv = np.linalg.inv(cov) - N = np.sqrt((2 * np.pi) ** dim * cov_det) + norm = np.sqrt((2 * np.pi) ** dim * cov_det) # This einsum call calculates (x-mu)T.Sigma-1.(x-mu) in a vectorized # way across all the input variables. fac = np.einsum("...k,kl,...l->...", pos - cen_off, cov_inv, pos - cen_off) + v = np.exp(-fac / 2) / norm + v *= amp / np.max(v) + return v - return np.exp(-fac / 2) / N + def _prepare_params_gauss(self, params: dict, shape: tuple) -> tuple: + """Prepare the positions for the gaussian. + + Args: + params (dict): Parameters for the gaussian. + shape (tuple): Shape of the image. + Returns: + tuple: Positions, offset and covariance matrix for the gaussian. + """ + x, y = np.meshgrid( + np.linspace(-shape[0] / 2, shape[0] / 2, shape[0]), + np.linspace(-shape[1] / 2, shape[1] / 2, shape[1]), + ) + pos = np.empty((*x.shape, 2)) + pos[:, :, 0] = x + pos[:, :, 1] = y + + offset = params["cen_off"] + cov = params["cov"] + amp = params["amp"] + return pos, offset, cov, amp + + def _add_noise(self, v: np.ndarray, noise: NoiseType) -> np.ndarray: + """Add noise to the simulated data. + + Args: + v (np.ndarray): Simulated data. + noise (NoiseType): Type of noise to add. + """ + if noise == NoiseType.POISSON: + v = np.random.poisson(np.round(v), v.shape) + return v + if noise == NoiseType.UNIFORM: + multiplier = self._active_params["noise_multiplier"] + v += np.random.uniform(-multiplier, multiplier, v.shape) + return v + if self._active_params["noise"] == NoiseType.NONE: + return v + + def _add_hot_pixel(self, v: np.ndarray, hot_pixel: dict) -> np.ndarray: + """Add hot pixels to the simulated data. + + Args: + v (np.ndarray): Simulated data. + hot_pixel (dict): Hot pixel parameters. + """ + for coords, hot_pixel_type, value in zip( + hot_pixel["coords"], hot_pixel["type"], hot_pixel["value"] + ): + if coords[0] < v.shape[0] and coords[1] < v.shape[1]: + if hot_pixel_type == HotPixelType.CONSTANT: + v[coords[0], coords[1]] = value + elif hot_pixel_type == HotPixelType.FLUCTUATING: + maximum = np.max(v) if np.max(v) != 0 else 1 + if v[coords[0], coords[1]] / maximum > 0.5: + v[coords[0], coords[1]] = value + return v def _compute_gaussian(self) -> float: """Computes return value for sim_type = "gauss". @@ -367,41 +483,15 @@ class SimulatedDataCamera(SimulatedDataBase): Returns: float """ - params = self._active_params - shape = self.sim_state[self.parent.image_shape.name]["value"] try: - X, Y = np.meshgrid( - np.linspace(-shape[0] / 2, shape[0] / 2, shape[0]), - np.linspace(-shape[1] / 2, shape[1] / 2, shape[1]), - ) - pos = np.empty((*X.shape, 2)) - pos[:, :, 0] = X - pos[:, :, 1] = Y + params = self._active_params + shape = self.sim_state[self.parent.image_shape.name]["value"] + pos, offset, cov, amp = self._prepare_params_gauss(self._active_params, shape) - v = self._compute_multivariate_gaussian( - pos=pos, cen_off=params["cen_off"], cov=params["cov"] - ) - # divide by max(v) to ensure that maximum is params["amp"] - v *= params["amp"] / np.max(v) - - # TODO add dependency from motor position -> #transmission factor, sigmoidal form from 0 to 1 as a function of motor pos - # motor_pos = self.device_manager.devices[params["ref_motor"]].obj.read()[ - # params["ref_motor"] - # ]["value"] - - if params["noise"] == NoiseType.POISSON: - v = np.random.poisson(np.round(v), v.shape) - return v - if params["noise"] == NoiseType.UNIFORM: - multiplier = params["noise_multiplier"] - v += np.random.uniform(-multiplier, multiplier, v.shape) - return v - if self._active_params["noise"] == NoiseType.NONE: - return v + v = self._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp) + v = self._add_noise(v, params["noise"]) + return self._add_hot_pixel(v, params["hot_pixel"]) except SimulatedDataException as exc: - # TODO Propagate msg to client! - logger.warning( - f"Could not compute gaussian for {params['ref_motor']} with {exc} raised." - "Returning 0 instead." - ) - return 0 + raise SimulatedDataException( + f"Could not compute gaussian for {self.parent.name} with {exc} raised. Deactivate eiger to continue." + ) from exc From 491e105af0871449cd0f48b08c126023aa28445b Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 7 Feb 2024 15:37:34 +0100 Subject: [PATCH 23/77] feat: Add simulation framework for pinhole scan --- ophyd_devices/sim/sim_frameworks.py | 121 ++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 ophyd_devices/sim/sim_frameworks.py diff --git a/ophyd_devices/sim/sim_frameworks.py b/ophyd_devices/sim/sim_frameworks.py new file mode 100644 index 0000000..ad9f4b6 --- /dev/null +++ b/ophyd_devices/sim/sim_frameworks.py @@ -0,0 +1,121 @@ +import numpy as np + +from scipy.ndimage import gaussian_filter + +from collections import defaultdict +from ophyd_devices.sim.sim_data import NoiseType + + +class PinholeLookup: + """Pinhole lookup table for simulated devices. + + When activated, it will create a lookup table for a simulated camera based on the config. + The lookup table will be used to simulate the effect of a pinhole on the camera image. + An example config is shown below, for the dev.eiger, with dev.samx and dev.samy as reference motors. + + eiger: + cen_off: [0, 0] # [x,y] + cov: [[1000, 500], [200, 1000]] # [[x,x],[y,y]] + pixel_size: 0.01 + signal: image + ref_motors: [samx, samy] + slit_width: [1, 2] + motor_dir: [0, 1] # x:0 , y:1, z:2 coordinates + """ + + def __init__( + self, + *args, + name, + device_manager=None, + config: dict = None, + **kwargs, + ): + self.name = name + self.device_manager = device_manager + self.config = config + self._enabled = True + self._lookup = defaultdict(dict) + self._gaussian_blur_sigma = 8 + self._compile_lookup() + + @property + def lookup(self): + """lookup property""" + return ( + self._lookup + if getattr(self.device_manager.devices, self.name).enabled is True + else None + ) + + def _compile_lookup(self): + """Compile the lookup table for the simulated camera.""" + for device_name in self.config.keys(): + self.lookup[device_name] = { + "obj": self, + "method": self._compute, + "args": {}, + "kwargs": {"device_name": device_name}, + } + + def _compute(self, *args, device_name: str = None, **kwargs) -> np.ndarray: + """ + Compute the lookup table for the simulated camera. + It copies the sim_camera bevahiour and adds a mask to simulate the effect of a pinhole. + + Args: + device_name (str): Name of the device. + + Returns: + np.ndarray: Lookup table for the simulated camera. + """ + device_obj = self.device_manager.devices.get(device_name) + params = device_obj.sim._all_params.get("gauss") + shape = device_obj.image_shape.get() + params.update( + { + "noise": NoiseType.POISSON, + "cov": np.array(self.config[device_name]["cov"]), + "cen_off": np.array(self.config[device_name]["cen_off"]), + } + ) + + pos, offset, cov, amp = device_obj.sim._prepare_params_gauss(params, shape) + v = device_obj.sim._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp) + device_pos = self.config[device_name]["pixel_size"] * pos + valid_mask = self._create_mask( + device_pos=device_pos, + ref_motors=self.config[device_name]["ref_motors"], + width=self.config[device_name]["slit_width"], + dir=self.config[device_name]["motor_dir"], + ) + valid_mask = self._blur_image(valid_mask, sigma=self._gaussian_blur_sigma) + v *= valid_mask + v = device_obj.sim._add_noise(v, params["noise"]) + v = device_obj.sim._add_hot_pixel(v, params["hot_pixel"]) + return v + + def _blur_image(self, image: np.ndarray, sigma: float = 5) -> np.ndarray: + """Blur the image with a gaussian filter. + + Args: + image (np.ndarray): Image to be blurred. + sigma (float): Sigma for the gaussian filter. + + Returns: + np.ndarray: Blurred image. + """ + return gaussian_filter(image, sigma=sigma) + + def _create_mask( + self, device_pos: np.ndarray, ref_motors: list[str], width: list[float], dir: list[int] + ): + mask = np.ones_like(device_pos, dtype=bool) + for ii, motor_name in enumerate(ref_motors): + motor_pos = self.device_manager.devices.get(motor_name).read()[motor_name]["value"] + edges = [motor_pos + width[ii] / 2, motor_pos - width[ii] / 2] + mask[..., dir[ii]] = np.logical_and( + device_pos[..., dir[ii]] > np.min(edges), device_pos[..., dir[ii]] < np.max(edges) + ) + + return np.prod(mask, axis=2) From 01c8559319836cca3d5d61267ddfb19791aea902 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 8 Feb 2024 11:05:21 +0100 Subject: [PATCH 24/77] refactor: add DeviceProxy class to sim_framework refactor(__init__): remove bec_device_base from import refactor: cleanup __init__ refactor: cleanup refactor: cleanup, renaming and small fixes to sim_framework. refactor: cleanup imports refactor: cleanup --- ophyd_devices/__init__.py | 1 + ophyd_devices/sim/__init__.py | 2 + ophyd_devices/sim/sim.py | 23 +++++- ophyd_devices/sim/sim_data.py | 36 +++------ ophyd_devices/sim/sim_frameworks.py | 117 +++++++++++++++++++--------- 5 files changed, 114 insertions(+), 65 deletions(-) diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index 1f88e9f..eccecdf 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -14,6 +14,7 @@ from .sim.sim import SimPositioner from .sim.sim import SimPositioner as SynAxisOPAAS from .sim.sim import SynDeviceOPAAS, SynFlyer from .sim.sim_signals import ReadOnlySignal +from .sim.sim_frameworks import DeviceProxy, SlitLookup from .sim.sim_signals import ReadOnlySignal as SynSignalRO from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor diff --git a/ophyd_devices/sim/__init__.py b/ophyd_devices/sim/__init__.py index f185011..2cd6e77 100644 --- a/ophyd_devices/sim/__init__.py +++ b/ophyd_devices/sim/__init__.py @@ -8,3 +8,5 @@ from .sim import ( from .sim_xtreme import SynXtremeOtf from .sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal + +from .sim_frameworks import SlitLookup diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 58be827..d390592 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -1,3 +1,4 @@ +from collections import defaultdict import os import threading import time as ttime @@ -68,11 +69,17 @@ class SimMonitor(Device): self.precision = precision self.init_sim_params = sim_init self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) + self._lookup_table = [] super().__init__(name=name, parent=parent, kind=kind, **kwargs) self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) self.readback.name = self.name + @property + def lookup_table(self) -> None: + """lookup_table property""" + return self._lookup_table + class SimCamera(Device): """A simulated device mimic any 2D camera. @@ -127,6 +134,7 @@ class SimCamera(Device): ): self.device_manager = device_manager self.init_sim_params = sim_init + self._lookup_table = [] self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) super().__init__(name=name, parent=parent, kind=kind, **kwargs) @@ -135,6 +143,11 @@ class SimCamera(Device): self.scaninfo = None self._update_scaninfo() + @property + def lookup_table(self) -> None: + """lookup_table property""" + return self._lookup_table + def trigger(self) -> DeviceStatus: """Trigger the camera to acquire images. @@ -277,6 +290,7 @@ class SimPositioner(Device, PositionerBase): self.precision = precision self.tolerance = tolerance self.init_sim_params = sim + self._lookup_table = [] self.speed = speed self.update_frequency = update_frequency @@ -286,7 +300,7 @@ class SimPositioner(Device, PositionerBase): # initialize inner dictionary with simulated state self.sim = self.sim_cls(parent=self, **kwargs) - super().__init__(name=name, labels=labels, kind=kind, **kwargs) + super().__init__(name=name, labels=labels, parent=parent, kind=kind, **kwargs) # Rename self.readback.name to self.name, also in self.sim_state self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) self.readback.name = self.name @@ -311,6 +325,11 @@ class SimPositioner(Device, PositionerBase): """Return the high limit of the simulated device.""" return self.limits[1] + @property + def lookup_table(self) -> None: + """lookup_table property""" + return self._lookup_table + def check_value(self, value: any): """ Check that requested position is within existing limits. @@ -519,4 +538,4 @@ class SynDeviceSubOPAAS(Device): class SynDeviceOPAAS(Device): x = Cpt(SimPositioner, name="x") y = Cpt(SimPositioner, name="y") - z = Cpt(SimPositioner, name="z") + z = Cpt(SynDeviceSubOPAAS, name="z") diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index eab7618..3179503 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections import defaultdict import enum -import inspect import time as ttime import numpy as np @@ -52,37 +51,22 @@ class SimulatedDataBase: self._all_params = defaultdict(dict) self.device_manager = device_manager self._simulation_type = None - self.lookup_table = getattr(self.parent, "lookup_table", None) + self.lookup_table = getattr(self.parent, "lookup_table", []) self.init_paramaters(**kwargs) self._active_params = self._all_params.get(self._simulation_type, None) - # self.register_in_lookup_table() - - # self.lookup_table = self.update_lookup_table() - - # def update_lookup_table(self) -> None: - # """Update the lookup table with the new value for the signal.""" - # table = getattr(self.device_manager.lookup_table, self.parent.name, None) - - # return getattr(self.device_manager.lookup_table, self.parent.name, None) - - # def register_in_lookup_table(self) -> None: - # """Register the simulated device in the lookup table.""" - # self.device_manager.lookup_table[self.parent.name] = {"obj": self, "method": "_compute_sim_state", "args": (), "kwargs": {}} def execute_simulation_method(self, *args, method=None, **kwargs) -> any: """Execute the method from the lookup table.""" - - if self.lookup_table and self.parent.name in self.lookup_table: - # obj = self.parent.lookup_table[self.parent.name]["obj"] - method = self.lookup_table[self.parent.name]["method"] - args = self.lookup_table[self.parent.name]["args"] - kwargs = self.lookup_table[self.parent.name]["kwargs"] - # Do I need args and kwargs! Why!! + if self.lookup_table and self.device_manager.devices.get(self.lookup_table[0]) is not None: + sim_device = self.device_manager.devices.get(self.lookup_table[0]) + # pylint: disable=protected-access + if sim_device.enabled is True: + method = sim_device.obj.lookup[self.parent.name]["method"] + args = sim_device.obj.lookup[self.parent.name]["args"] + kwargs = sim_device.obj.lookup[self.parent.name]["kwargs"] if method is not None: - method_arguments = list(inspect.signature(method).parameters.keys()) - if all([True for arg in method_arguments if arg in args or arg in kwargs]): - return method(*args, **kwargs) + return method(*args, **kwargs) raise SimulatedDataException(f"Method {method} is not available for {self.parent.name}") def init_paramaters(self, **kwargs): @@ -330,7 +314,7 @@ class SimulatedDataCamera(SimulatedDataBase): }, }, SimulationType.GAUSSIAN: { - "amp": 500, + "amp": 100, "cen_off": np.array([0, 0]), "cov": np.array([[10, 5], [5, 10]]), "noise": NoiseType.NONE, diff --git a/ophyd_devices/sim/sim_frameworks.py b/ophyd_devices/sim/sim_frameworks.py index ad9f4b6..4c6f784 100644 --- a/ophyd_devices/sim/sim_frameworks.py +++ b/ophyd_devices/sim/sim_frameworks.py @@ -1,64 +1,95 @@ import numpy as np - from scipy.ndimage import gaussian_filter from collections import defaultdict from ophyd_devices.sim.sim_data import NoiseType +from ophyd_devices.utils.bec_device_base import BECDeviceBase -class PinholeLookup: - """Pinhole lookup table for simulated devices. +class DeviceProxy(BECDeviceBase): + """DeviceProxy class inherits from BECDeviceBase.""" - When activated, it will create a lookup table for a simulated camera based on the config. - The lookup table will be used to simulate the effect of a pinhole on the camera image. - An example config is shown below, for the dev.eiger, with dev.samx and dev.samy as reference motors. - eiger: - cen_off: [0, 0] # [x,y] - cov: [[1000, 500], [200, 1000]] # [[x,x],[y,y]] - pixel_size: 0.01 - signal: image - ref_motors: [samx, samy] - slit_width: [1, 2] - motor_dir: [0, 1] # x:0 , y:1, z:2 coordinates +class SlitLookup(DeviceProxy): """ + Simulation framework to immidate the behaviour of slits. + + This device is a proxy that is meant to overrides the behaviour of a SimCamera. + You may use this to simulate the effect of slits on the camera image. + + Parameters can be configured via the deviceConfig field in the device_config. + The example below shows the configuration for a pinhole simulation on an Eiger detector, + where the pinhole is defined by the position of motors samx and samy. These devices must + exist in your config. + + To update for instance the pixel_size directly, you can directly access the DeviceConfig via + `dev.eiger.get_device_config()` or update it `dev.eiger.get_device_config({'eiger' : {'pixel_size': 0.1}})` + + slit_sim: + readoutPriority: on_request + deviceClass: SlitLookup + deviceConfig: + eiger: + cen_off: [0, 0] # [x,y] + cov: [[1000, 500], [200, 1000]] # [[x,x],[y,y]] + pixel_size: 0.01 + ref_motors: [samx, samy] + slit_width: [1, 1] + motor_dir: [0, 1] # x:0 , y:1, z:2 coordinates + enabled: true + readOnly: false + """ + + USER_ACCESS = ["enabled", "lookup", "help"] def __init__( self, - *args, name, + *args, device_manager=None, - config: dict = None, **kwargs, ): self.name = name self.device_manager = device_manager - self.config = config - self._enabled = True + self.config = None self._lookup = defaultdict(dict) - self._gaussian_blur_sigma = 8 + self._gaussian_blur_sigma = 5 + super().__init__(name, *args, **kwargs) + + def help(self) -> None: + """Print documentation for the SlitLookup device.""" + print(self.__doc__) + + def _update_device_config(self, config: dict) -> None: + """Update the config from the device_config for the pinhole lookup table. + + Args: + config (dict): Config dictionary. + """ + self.config = config self._compile_lookup() @property def lookup(self): """lookup property""" - return ( - self._lookup - if getattr(self.device_manager.devices, self.name).enabled is True - else None - ) + return self._lookup + + @lookup.setter + def lookup(self, update: dict) -> None: + """lookup setter""" + self._lookup.update(update) def _compile_lookup(self): """Compile the lookup table for the simulated camera.""" for device_name in self.config.keys(): - self.lookup[device_name] = { - "obj": self, + self._lookup[device_name] = { + # "obj": self, "method": self._compute, - "args": {}, - "kwargs": {"device_name": device_name}, + "args": (device_name,), + "kwargs": {}, } - def _compute(self, *args, device_name: str = None, **kwargs) -> np.ndarray: + def _compute(self, device_name: str, *args, **kwargs) -> np.ndarray: """ Compute the lookup table for the simulated camera. It copies the sim_camera bevahiour and adds a mask to simulate the effect of a pinhole. @@ -69,7 +100,7 @@ class PinholeLookup: Returns: np.ndarray: Lookup table for the simulated camera. """ - device_obj = self.device_manager.devices.get(device_name) + device_obj = self.device_manager.devices.get(device_name).obj params = device_obj.sim._all_params.get("gauss") shape = device_obj.image_shape.get() params.update( @@ -87,7 +118,7 @@ class PinholeLookup: device_pos=device_pos, ref_motors=self.config[device_name]["ref_motors"], width=self.config[device_name]["slit_width"], - dir=self.config[device_name]["motor_dir"], + direction=self.config[device_name]["motor_dir"], ) valid_mask = self._blur_image(valid_mask, sigma=self._gaussian_blur_sigma) v *= valid_mask @@ -95,7 +126,7 @@ class PinholeLookup: v = device_obj.sim._add_hot_pixel(v, params["hot_pixel"]) return v - def _blur_image(self, image: np.ndarray, sigma: float = 5) -> np.ndarray: + def _blur_image(self, image: np.ndarray, sigma: float = 1) -> np.ndarray: """Blur the image with a gaussian filter. Args: @@ -108,14 +139,26 @@ class PinholeLookup: return gaussian_filter(image, sigma=sigma) def _create_mask( - self, device_pos: np.ndarray, ref_motors: list[str], width: list[float], dir: list[int] + self, + device_pos: np.ndarray, + ref_motors: list[str], + width: list[float], + direction: list[int], ): - mask = np.ones_like(device_pos, dtype=bool) + mask = np.ones_like(device_pos) for ii, motor_name in enumerate(ref_motors): - motor_pos = self.device_manager.devices.get(motor_name).read()[motor_name]["value"] + motor_pos = self.device_manager.devices.get(motor_name).obj.read()[motor_name]["value"] edges = [motor_pos + width[ii] / 2, motor_pos - width[ii] / 2] - mask[..., dir[ii]] = np.logical_and( - device_pos[..., dir[ii]] > np.min(edges), device_pos[..., dir[ii]] < np.max(edges) + mask[..., direction[ii]] = np.logical_and( + device_pos[..., direction[ii]] > np.min(edges), + device_pos[..., direction[ii]] < np.max(edges), ) return np.prod(mask, axis=2) + + +if __name__ == "__main__": + # Example usage + pinhole = SlitLookup(name="pinhole", device_manager=None) + pinhole.describe() + print(pinhole) From 65b9f23332f440a5d467fde789747166c18e1458 Mon Sep 17 00:00:00 2001 From: appel_c Date: Sat, 17 Feb 2024 16:46:52 +0100 Subject: [PATCH 25/77] refactor: quickfix connector/producer import in scaninfo mixin --- ophyd_devices/utils/bec_scaninfo_mixin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index f005927..fbcbc39 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -1,4 +1,5 @@ import getpass +import warnings from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger @@ -65,6 +66,12 @@ class BecScaninfoMixin: self, device_manager: DeviceManagerBase = None, sim_mode: bool = False, bec_info_msg=None ) -> None: self.device_manager = device_manager + if self.device_manager and hasattr(self.device_manager, "producer"): + warnings.warn( + "The producer attribute is deprecated and will be removed in the future.", + DeprecationWarning, + ) + self.device_manager.connector = self.device_manager.producer self.sim_mode = sim_mode self.scan_msg = None self.scanID = None From 257b1fa46604ab6c69f4fb7e096a14099b7d4b85 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sat, 17 Feb 2024 16:05:20 +0000 Subject: [PATCH 26/77] 0.22.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9373363..76e115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## v0.22.0 (2024-02-17) + +### Feature + +* Add simulation framework for pinhole scan ([`491e105`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/491e105af0871449cd0f48b08c126023aa28445b)) +* Extend sim_data to allow execution from function of secondary devices extracted from lookup ([`851a088`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/851a088b81cfd7e9d323955f923174a394155bfd)) + ## v0.21.1 (2024-02-17) ### Fix diff --git a/setup.py b/setup.py index d705123..671dca6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.21.1" +__version__ = "0.22.0" if __name__ == "__main__": setup( From b12292246fd9d8204c76cd1f4927da5a1b981857 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 17:44:52 +0100 Subject: [PATCH 27/77] Revert "fix(deprecation): remove all remaining .dumps(), .loads() and producer->connector" This reverts commit 4159f3e3ec20727b395808118f3c0c166d9d1c0c --- ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py | 7 +++++-- ophyd_devices/epics/devices/eiger9m_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/falcon_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/mcs_csaxs.py | 4 ++-- ophyd_devices/epics/devices/pilatus_csaxs.py | 10 +++++----- .../epics/devices/psi_delay_generator_base.py | 2 +- .../epics/devices/psi_detector_base.py | 2 +- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 16 ++++++++-------- ophyd_devices/sim/sim.py | 18 ++++++++++-------- ophyd_devices/sim/sim_test_devices.py | 18 ++++++++++-------- ophyd_devices/sim/sim_xtreme.py | 4 ++-- ophyd_devices/utils/bec_scaninfo_mixin.py | 4 ++-- ophyd_devices/utils/bec_utils.py | 14 +++++++------- tests/test_eiger9m_csaxs.py | 14 +++++++------- tests/test_falcon_csaxs.py | 12 ++++++------ tests/test_mcs_card.py | 8 ++++---- tests/test_pilatus_csaxs.py | 12 ++++++------ tests/utils.py | 4 ++-- 18 files changed, 88 insertions(+), 81 deletions(-) diff --git a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py index 582e88e..256b124 100644 --- a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py +++ b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py @@ -81,7 +81,8 @@ class Eiger1p5MDetector(Device): self.username = "e20588" # TODO get from config def _get_current_scan_msg(self) -> messages.ScanStatusMessage: - return self.device_manager.connector.get(MessageEndpoints.scan_status()) + msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + return messages.ScanStatusMessage.loads(msg) def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None): if leading_zeros is None: @@ -158,7 +159,9 @@ class Eiger1p5MDetector(Device): self.detector_control.put("stop") signals = {"config": self.read(), "data": self.file_name} msg = messages.DeviceMessage(signals=signals, metadata=self.metadata) - self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg) + self.device_manager.producer.set_and_publish( + MessageEndpoints.device_read(self.name), msg.dumps() + ) self._stopped = False return super().unstage() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 9873326..219453f 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -301,20 +301,20 @@ class Eiger9MSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.connector.pipeline() + pipe = self.parent.producer.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.connector.set_and_publish( + self.parent.producer.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg, + msg.dumps(), pipe=pipe, ) - self.parent.connector.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + self.parent.producer.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 09d61ff..fea5173 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -244,20 +244,20 @@ class FalconSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.connector.pipeline() + pipe = self.parent.producer.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.connector.set_and_publish( + self.parent.producer.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg, + msg.dumps(), pipe=pipe, ) - self.parent.connector.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + self.parent.producer.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index 5158b78..f87b809 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -136,8 +136,8 @@ class MCSSetup(CustomDetectorMixin): msg = messages.DeviceMessage( signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata, - ) - self.parent.connector.xadd( + ).dumps() + self.parent.producer.xadd( topic=MessageEndpoints.device_async_readback( scanID=self.parent.scaninfo.scanID, device=self.parent.name ), diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 390db83..ee13b98 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -331,7 +331,7 @@ class PilatusSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.connector.pipeline() + pipe = self.parent.producer.pipeline() if successful is None: msg = messages.FileMessage( file_path=self.parent.filepath, @@ -345,13 +345,13 @@ class PilatusSetup(CustomDetectorMixin): successful=successful, metadata={"input_path": self.parent.filepath_raw}, ) - self.parent.connector.set_and_publish( + self.parent.producer.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg, + msg.dumps(), pipe=pipe, ) - self.parent.connector.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe + self.parent.producer.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/psi_delay_generator_base.py b/ophyd_devices/epics/devices/psi_delay_generator_base.py index 5068343..76641c6 100644 --- a/ophyd_devices/epics/devices/psi_delay_generator_base.py +++ b/ophyd_devices/epics/devices/psi_delay_generator_base.py @@ -391,7 +391,7 @@ class PSIDelayGeneratorBase(Device): self.device_manager = device_manager else: self.device_manager = bec_utils.DMMock() - self.connector = self.device_manager.connector + self.producer = self.device_manager.producer self._update_scaninfo() self._init() diff --git a/ophyd_devices/epics/devices/psi_detector_base.py b/ophyd_devices/epics/devices/psi_detector_base.py index 36450d2..176f82c 100644 --- a/ophyd_devices/epics/devices/psi_detector_base.py +++ b/ophyd_devices/epics/devices/psi_detector_base.py @@ -228,7 +228,7 @@ class PSIDetectorBase(Device): self.device_manager = bec_utils.DMMock() base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/" self.service_cfg = {"base_path": os.path.expanduser(base_path)} - self.connector = self.device_manager.connector + self.producer = self.device_manager.producer self._update_scaninfo() self._update_filewriter() self._init() diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 72bbd8a..0e70d64 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -308,9 +308,9 @@ class RtLamniController(Controller): def _update_flyer_device_info(self): flyer_info = self._get_flyer_device_info() - self.get_device_manager().connector.set( + self.get_device_manager().producer.set( MessageEndpoints.device_info("rt_scan"), - messages.DeviceInfoMessage(device="rt_scan", info=flyer_info), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), ) def _get_flyer_device_info(self) -> dict: @@ -385,11 +385,11 @@ class RtLamniController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().producer.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata - ), + ).dumps(), ) # while scan is running while mode > 0: @@ -420,11 +420,11 @@ class RtLamniController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().producer.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata - ), + ).dumps(), ) logger.info( @@ -432,12 +432,12 @@ class RtLamniController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().connector.send( + self.get_device_manager().producer.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata}, - ), + ).dumps(), ) def feedback_status_angle_lamni(self) -> bool: diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index d390592..6dcae0f 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -494,32 +494,34 @@ class SynFlyer(Device, PositionerBase): } }, metadata={"pointID": ii, **metadata}, - ) + ).dumps() ) ttime.sleep(exp_time) elapsed_time += exp_time if elapsed_time > buffer_time: elapsed_time = 0 - device.device_manager.connector.send( - MessageEndpoints.device_read(device.name), bundle + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() ) bundle = messages.BundleMessage() - device.device_manager.connector.set_and_publish( + device.device_manager.producer.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=1, metadata={"pointID": ii, **metadata}, - ), + ).dumps(), ) - device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) - device.device_manager.connector.set_and_publish( + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() + ) + device.device_manager.producer.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ), + ).dumps(), ) print("done") diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index 6f3029c..2030b66 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -115,32 +115,34 @@ class SynFlyerLamNI(Device, PositionerBase): } }, metadata={"pointID": ii, **metadata}, - ) + ).dumps() ) ttime.sleep(exp_time) elapsed_time += exp_time if elapsed_time > buffer_time: elapsed_time = 0 - device.device_manager.connector.send( - MessageEndpoints.device_read(device.name), bundle + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() ) bundle = messages.BundleMessage() - device.device_manager.connector.set_and_publish( + device.device_manager.producer.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=1, metadata={"pointID": ii, **metadata}, - ), + ).dumps(), ) - device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) - device.device_manager.connector.set_and_publish( + device.device_manager.producer.send( + MessageEndpoints.device_read(device.name), bundle.dumps() + ) + device.device_manager.producer.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ), + ).dumps(), ) print("done") diff --git a/ophyd_devices/sim/sim_xtreme.py b/ophyd_devices/sim/sim_xtreme.py index fcd80b5..fea60b5 100644 --- a/ophyd_devices/sim/sim_xtreme.py +++ b/ophyd_devices/sim/sim_xtreme.py @@ -357,8 +357,8 @@ class SynXtremeOtfReplay(FlyerInterface, Device): } msg = messages.DeviceMessage( signals=signals, metadata=self._device_manager.devices.otf.metadata - ) - self._device_manager.connector.set_and_publish( + ).dumps() + self._device_manager.producer.set_and_publish( MessageEndpoints.device_readback("signals"), msg ) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index fbcbc39..64ba158 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -96,7 +96,7 @@ class BecScaninfoMixin: messages.ScanStatusMessage: messages.ScanStatusMessage object """ if not self.sim_mode: - msg = self.device_manager.connector.get(MessageEndpoints.scan_status()) + msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) if not isinstance(msg, messages.ScanStatusMessage): return None return msg @@ -112,7 +112,7 @@ class BecScaninfoMixin: if self.sim_mode: return getpass.getuser() - msg = self.device_manager.connector.get(MessageEndpoints.account()) + msg = self.device_manager.producer.get(MessageEndpoints.account()) if msg: return msg return getpass.getuser() diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 756716f..94c9cf7 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -122,17 +122,17 @@ class ProducerMock: class PipelineMock: _pipe_buffer = [] - _connector = None + _producer = None - def __init__(self, connector) -> None: - self._connector = connector + def __init__(self, producer) -> None: + self._producer = producer def execute(self): - if not self._connector.store_data: + if not self._producer.store_data: self._pipe_buffer = [] return [] res = [ - getattr(self._connector, method)(*args, **kwargs) + getattr(self._producer, method)(*args, **kwargs) for method, args, kwargs in self._pipe_buffer ] self._pipe_buffer = [] @@ -142,13 +142,13 @@ class PipelineMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a connector. + The mocked DeviceManager creates a device containert and a producer. """ def __init__(self): self.devices = DeviceContainer() - self.connector = ProducerMock() + self.producer = ProducerMock() def add_device(self, name: str, value: float = 0.0): self.devices[name] = DeviceMock(name, value) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index cf25ec3..00ffd80 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -50,7 +50,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -428,24 +428,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ) + ).dumps() expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), ] - assert mock_det.connector.set_and_publish.call_args_list == expected_calls + assert mock_det.producer.set_and_publish.call_args_list == expected_calls def test_stop(mock_det): diff --git a/tests/test_falcon_csaxs.py b/tests/test_falcon_csaxs.py index b07915e..2622a68 100644 --- a/tests/test_falcon_csaxs.py +++ b/tests/test_falcon_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-SITORO:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -215,24 +215,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ) + ).dumps() expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), ] - assert mock_det.connector.set_and_publish.call_args_list == expected_calls + assert mock_det.producer.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/test_mcs_card.py b/tests/test_mcs_card.py index 841dac6..ac62bc6 100644 --- a/tests/test_mcs_card.py +++ b/tests/test_mcs_card.py @@ -32,7 +32,7 @@ def mock_det(): prefix = "X12SA-MCS:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -53,7 +53,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -184,7 +184,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): mock_det.custom_prepare._send_data_to_bec() device_metadata = mock_det.scaninfo.scan_msg.metadata metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()}) - data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata) + data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata).dumps() calls = mock.call( topic=MessageEndpoints.device_async_readback( scanID=metadata["scanID"], device=mock_det.name @@ -193,7 +193,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): expire=1800, ) - assert mock_det.connector.xadd.call_args == calls + assert mock_det.producer.xadd.call_args == calls @pytest.mark.parametrize( diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 642e0e0..81c4629 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -28,7 +28,7 @@ def mock_det(): prefix = "X12SA-ES-PILATUS300K:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "connector"): + with mock.patch.object(dm, "producer"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -207,27 +207,27 @@ def test_publish_file_location(mock_det, scaninfo): file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, - ) + ).dumps() else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, successful=scaninfo["successful"], - ) + ).dumps() expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.connector.pipeline.return_value, + pipe=mock_det.producer.pipeline.return_value, ), ] - assert mock_det.connector.set_and_publish.call_args_list == expected_calls + assert mock_det.producer.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/utils.py b/tests/utils.py index 8d0fd6b..84ebce8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -296,13 +296,13 @@ class DeviceMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a connector. + The mocked DeviceManager creates a device containert and a producer. """ def __init__(self): self.devices = DeviceContainer() - self.connector = ProducerMock() + self.producer = ProducerMock() def add_device(self, name: str, value: float = 0.0): """Add device to the DeviceManagerMock""" From 8d5b32ebd6e01d6870755c4894f31078ade40317 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 17:49:38 +0100 Subject: [PATCH 28/77] Revert "refactor: quickfix connector/producer import in scaninfo mixin" This reverts commit 65b9f23332f440a5d467fde789747166c18e1458 --- ophyd_devices/utils/bec_scaninfo_mixin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index 64ba158..417259b 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -1,5 +1,4 @@ import getpass -import warnings from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger @@ -66,12 +65,6 @@ class BecScaninfoMixin: self, device_manager: DeviceManagerBase = None, sim_mode: bool = False, bec_info_msg=None ) -> None: self.device_manager = device_manager - if self.device_manager and hasattr(self.device_manager, "producer"): - warnings.warn( - "The producer attribute is deprecated and will be removed in the future.", - DeprecationWarning, - ) - self.device_manager.connector = self.device_manager.producer self.sim_mode = sim_mode self.scan_msg = None self.scanID = None From 7a97e05e04478394423e398f871629fc9c3ef345 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 16:40:01 +0100 Subject: [PATCH 29/77] test(flomni): added more tests --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 9 ++++--- tests/test_rt_flomni.py | 31 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index 1627aaf..7d1deb5 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -298,10 +298,11 @@ class RtFlomniController(RtController): } def laser_tracker_galil_enable(self): - self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("tracken=1") - self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("trackyct=0") - self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("trackzct=0") - self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed("XQ#Tracker") + ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller + ftrackz_con.socket_put_confirmed("tracken=1") + ftrackz_con.socket_put_confirmed("trackyct=0") + ftrackz_con.socket_put_confirmed("trackzct=0") + ftrackz_con.socket_put_confirmed("XQ#Tracker") def laser_tracker_on_target(self) -> bool: self.laser_update_tracker_info() diff --git a/tests/test_rt_flomni.py b/tests/test_rt_flomni.py index fa754a5..8f2bc27 100644 --- a/tests/test_rt_flomni.py +++ b/tests/test_rt_flomni.py @@ -4,6 +4,7 @@ import pytest from utils import SocketMock from ophyd_devices.rt_lamni import RtFlomniController, RtFlomniMotor +from ophyd_devices.rt_lamni.rt_ophyd import RtError @pytest.fixture() @@ -56,3 +57,33 @@ def test_feedback_enable_with_reset(rt_flomni): rt_flomni.feedback_enable_with_reset() laser_tracker_on.assert_called_once() + + +def test_move_samx_to_scan_region(rt_flomni): + device_manager = rt_flomni.get_device_manager() + device_manager.devices.rtx.user_parameter.get.return_value = 1 + rt_flomni.move_samx_to_scan_region(20, 2) + assert mock.call(b"v0\n") not in rt_flomni.sock.put.mock_calls + assert mock.call(b"v1\n") in rt_flomni.sock.put.mock_calls + + +def test_feedback_enable_without_reset(rt_flomni): + with mock.patch.object(rt_flomni, "set_device_enabled") as set_device_enabled: + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + rt_flomni.feedback_enable_without_reset() + laser_tracker_on.assert_called_once() + assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls + assert mock.call("fsamx", False) in set_device_enabled.mock_calls + assert mock.call("fsamy", False) in set_device_enabled.mock_calls + assert mock.call("foptx", False) in set_device_enabled.mock_calls + assert mock.call("fopty", False) in set_device_enabled.mock_calls + + +def test_feedback_enable_without_reset_raises(rt_flomni): + with mock.patch.object(rt_flomni, "feedback_is_running", return_value=False): + with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on: + with pytest.raises(RtError): + rt_flomni.feedback_enable_without_reset() + laser_tracker_on.assert_called_once() + assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls From 406f27c27ea9742bc1f33028234e06520cd891be Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 17:04:16 +0100 Subject: [PATCH 30/77] ci: added environment variable for downstream pipelines --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f1aea68..057a9f2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -121,6 +121,8 @@ trigger_bec: trigger: project: bec/bec strategy: depend + variables: + OPHYD_DEVICES_BRANCH: $CI_COMMIT_REF_NAME rules: - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"' From 84fed4ee82980d6a44650bd97070b615d36aa4b2 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 20 Feb 2024 21:35:29 +0100 Subject: [PATCH 31/77] refactor: made BECDeviceBase a protocol --- ophyd_devices/utils/bec_device_base.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py index 9123353..3597ebf 100644 --- a/ophyd_devices/utils/bec_device_base.py +++ b/ophyd_devices/utils/bec_device_base.py @@ -1,15 +1,16 @@ -class BECDeviceBase: +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class BECDeviceBase(Protocol): """Base class for BEC devices with zero functionality.""" - def __init__(self, name: str, *args, **kwargs): - self.name = name - self._connected = True - self._destroyed = False + name: str + _destroyed: bool @property - def hints(self): + def hints(self) -> dict: """hints property""" - return {} @property def connected(self) -> bool: @@ -19,12 +20,10 @@ class BECDeviceBase: Returns: bool: True if connected, False otherwise """ - return self._connected @connected.setter def connected(self, value: bool): """connected setter""" - self._connected = value def describe(self) -> dict: """describe method @@ -35,7 +34,6 @@ class BECDeviceBase: Returns: dict: Dictionary with dictionaries with signal descriptions ('source', 'dtype', 'shape') """ - return {} def describe_configuration(self) -> dict: """describe method @@ -46,7 +44,6 @@ class BECDeviceBase: Returns: dict: Dictionary with dictionaries with signal descriptions ('source', 'dtype', 'shape') """ - return {} def read_configuration(self) -> dict: """read_configuration method @@ -57,7 +54,6 @@ class BECDeviceBase: dict: Dictionary with nested dictionary of signals with kind.config: {'signal_name' : {'value' : .., "timestamp" : ..}, ...} """ - return {} def read(self) -> dict: """read method @@ -68,8 +64,6 @@ class BECDeviceBase: dict: Dictionary with nested dictionary of signals with kind.normal or kind.hinted: {'signal_name' : {'value' : .., "timestamp" : ..}, ...} """ - return {} - def destroy(self): + def destroy(self) -> None: """Destroy method""" - self._destroyed = True From 82cfefb3b969f0fdebc357f8bd5b404ec503d7ce Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 20 Feb 2024 21:36:04 +0100 Subject: [PATCH 32/77] feat(static_device_test): added check against BECDeviceBase protocol --- ophyd_devices/utils/static_device_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/utils/static_device_test.py b/ophyd_devices/utils/static_device_test.py index b981562..a0914bc 100644 --- a/ophyd_devices/utils/static_device_test.py +++ b/ophyd_devices/utils/static_device_test.py @@ -7,6 +7,8 @@ import ophyd import yaml from bec_lib.scibec_validator import SciBecValidator +from ophyd_devices.utils.bec_device_base import BECDeviceBase + try: from bec_plugins import devices as plugin_devices except ImportError: @@ -176,7 +178,7 @@ class StaticDeviceTest: Returns: """ - + assert isinstance(obj, BECDeviceBase) assert isinstance(obj.name, str) assert isinstance(obj.read(), dict) assert isinstance(obj.read_configuration(), dict) From 2f2cef10f7fb77e502cbf274a6b350f2feb0ad22 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 21 Feb 2024 10:10:46 +0100 Subject: [PATCH 33/77] fix: separate BECDevice and BECDeviceBase --- ophyd_devices/utils/bec_device_base.py | 53 ++++++++++++++++++++++- ophyd_devices/utils/static_device_test.py | 4 +- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py index 3597ebf..c1eda27 100644 --- a/ophyd_devices/utils/bec_device_base.py +++ b/ophyd_devices/utils/bec_device_base.py @@ -2,8 +2,8 @@ from typing import Protocol, runtime_checkable @runtime_checkable -class BECDeviceBase(Protocol): - """Base class for BEC devices with zero functionality.""" +class BECDevice(Protocol): + """Protocol for BEC devices with zero functionality.""" name: str _destroyed: bool @@ -67,3 +67,52 @@ class BECDeviceBase(Protocol): def destroy(self) -> None: """Destroy method""" + + +class BECDeviceBase: + """Base class for BEC devices with minimum functionality. + + Device will be initiated and connected,e.g. obj.connected will be True. + + """ + + def __init__(self, name: str): + self.name = name + self._connected = True + self._destroyed = False + + @property + def hints(self) -> dict: + """hints property""" + return {} + + @property + def connected(self) -> bool: + """connected property""" + return self._connected + + @connected.setter + def connected(self, value: bool): + """connected setter""" + self._connected = value + + def describe(self) -> dict: + """describe method""" + return {} + + def describe_configuration(self) -> dict: + """describe_configuration method""" + return {} + + def read(self) -> dict: + """read method""" + return {} + + def read_configuration(self) -> dict: + """read_configuration method""" + return {} + + def destroy(self) -> None: + """destroy method""" + self._destroyed = True + self.connected = False diff --git a/ophyd_devices/utils/static_device_test.py b/ophyd_devices/utils/static_device_test.py index a0914bc..ece936d 100644 --- a/ophyd_devices/utils/static_device_test.py +++ b/ophyd_devices/utils/static_device_test.py @@ -7,7 +7,7 @@ import ophyd import yaml from bec_lib.scibec_validator import SciBecValidator -from ophyd_devices.utils.bec_device_base import BECDeviceBase +from ophyd_devices.utils.bec_device_base import BECDevice try: from bec_plugins import devices as plugin_devices @@ -178,7 +178,7 @@ class StaticDeviceTest: Returns: """ - assert isinstance(obj, BECDeviceBase) + assert isinstance(obj, BECDevice) assert isinstance(obj.name, str) assert isinstance(obj.read(), dict) assert isinstance(obj.read_configuration(), dict) From 399d6d94cc3ce29a67ae7c6daba536aa66df9d76 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 21 Feb 2024 10:11:28 +0100 Subject: [PATCH 34/77] test(BECDeviceBase): add test --- tests/test_simulation.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/test_simulation.py diff --git a/tests/test_simulation.py b/tests/test_simulation.py new file mode 100644 index 0000000..d0a7712 --- /dev/null +++ b/tests/test_simulation.py @@ -0,0 +1,8 @@ +from ophyd_devices.utils.bec_device_base import BECDeviceBase, BECDevice + + +def test_BECDeviceBase(): + # Test the BECDeviceBase class + test = BECDeviceBase(name="test") + assert isinstance(test, BECDevice) + assert test.connected is True From 8fe8aa0ec781f8f2ba20263ef1a108411f9cbeda Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 21 Feb 2024 09:36:30 +0000 Subject: [PATCH 35/77] 0.23.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e115a..fa6f192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ +## v0.23.0 (2024-02-21) + +### Feature + +* **static_device_test:** Added check against BECDeviceBase protocol ([`82cfefb`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/82cfefb3b969f0fdebc357f8bd5b404ec503d7ce)) + +### Fix + +* Separate BECDevice and BECDeviceBase ([`2f2cef1`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/2f2cef10f7fb77e502cbf274a6b350f2feb0ad22)) + ## v0.22.0 (2024-02-17) ### Feature diff --git a/setup.py b/setup.py index 671dca6..2425b40 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.22.0" +__version__ = "0.23.0" if __name__ == "__main__": setup( From f91d0c482d194e5f69c7206d0f6ad0971f84b0e1 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 18:19:34 +0100 Subject: [PATCH 36/77] fix: replaced outdated enable_set by read_only --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 8 +++--- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 25 +++++-------------- ophyd_devices/rt_lamni/rt_ophyd.py | 23 ++++------------- ophyd_devices/utils/bec_utils.py | 30 +++++++---------------- tests/utils.py | 20 +++++++-------- 5 files changed, 34 insertions(+), 72 deletions(-) diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index 7d1deb5..ded8dfd 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -126,9 +126,9 @@ class RtFlomniController(RtController): ) if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01): - fsamx.enabled_set = True + fsamx.read_only = False fsamx.obj.move(fsamx_in, wait=True) - fsamx.enabled_set = False + fsamx.read_only = True time.sleep(1) self.socket_put("l1") @@ -169,13 +169,13 @@ class RtFlomniController(RtController): wait_on_exit = True self.socket_put("v0") fsamx = self.get_device_manager().devices.fsamx - fsamx.enabled_set = True + fsamx.read_only = False fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007 logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}") fsamx_in = fsamx.user_parameter.get("in") fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True) - fsamx.enabled_set = False + fsamx.read_only = True time.sleep(0.1) self.laser_tracker_on() time.sleep(0.01) diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 0e70d64..6a67912 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -3,7 +3,7 @@ import threading import time import numpy as np -from bec_lib import messages, MessageEndpoints, bec_logger +from bec_lib import MessageEndpoints, bec_logger, messages from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait @@ -435,8 +435,7 @@ class RtLamniController(Controller): self.get_device_manager().producer.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( - signals=signals, - metadata={"pointID": pointID, **self.readout_metadata}, + signals=signals, metadata={"pointID": pointID, **self.readout_metadata} ).dumps(), ) @@ -532,7 +531,7 @@ class RtLamniController(Controller): f"Device {device_name} is not configured and cannot be enabled/disabled." ) return - self.get_device_manager().devices[device_name].enabled_set = enabled + self.get_device_manager().devices[device_name].read_only = not enabled class RtLamniSignalBase(SocketSignal): @@ -623,11 +622,7 @@ class RtLamniMotorIsMoving(RtLamniSignalRO): def get(self): val = super().get() if val is not None: - self._run_subs( - sub_type=self.SUB_VALUE, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) return val @@ -642,11 +637,7 @@ class RtLamniFeedbackRunning(RtLamniSignalRO): class RtLamniMotor(Device, PositionerBase): USER_ACCESS = ["controller"] - readback = Cpt( - RtLamniReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint") motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal") @@ -769,11 +760,7 @@ class RtLamniMotor(Device, PositionerBase): while self.motor_is_moving.get(): print("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.01) print("Move finished") self._done_moving() diff --git a/ophyd_devices/rt_lamni/rt_ophyd.py b/ophyd_devices/rt_lamni/rt_ophyd.py index 202c7ef..3c70b33 100644 --- a/ophyd_devices/rt_lamni/rt_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_ophyd.py @@ -405,8 +405,7 @@ class RtController(Controller): self.get_device_manager().producer.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( - signals=signals, - metadata={"pointID": pointID, **self.readout_metadata}, + signals=signals, metadata={"pointID": pointID, **self.readout_metadata} ).dumps(), ) @@ -502,7 +501,7 @@ class RtController(Controller): f"Device {device_name} is not configured and cannot be enabled/disabled." ) return - self.get_device_manager().devices[device_name].enabled_set = enabled + self.get_device_manager().devices[device_name].read_only = not enabled class RtSignalBase(SocketSignal): @@ -593,11 +592,7 @@ class RtMotorIsMoving(RtSignalRO): def get(self): val = super().get() if val is not None: - self._run_subs( - sub_type=self.SUB_VALUE, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) return val @@ -612,11 +607,7 @@ class RtFeedbackRunning(RtSignalRO): class RtMotor(Device, PositionerBase): USER_ACCESS = ["controller"] - readback = Cpt( - RtReadbackSignal, - signal_name="readback", - kind="hinted", - ) + readback = Cpt(RtReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint") motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal") @@ -739,11 +730,7 @@ class RtMotor(Device, PositionerBase): while self.motor_is_moving.get(): print("motor is moving") val = self.readback.read() - self._run_subs( - sub_type=self.SUB_READBACK, - value=val, - timestamp=time.time(), - ) + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.01) print("Move finished") self._done_moving() diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 94c9cf7..c2cf88e 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -2,12 +2,10 @@ import time from bec_lib import bec_logger from bec_lib.devicemanager import DeviceContainer - -from ophyd import Signal, Kind +from ophyd import Kind, Signal from ophyd_devices.utils.socket import data_shape, data_type - logger = bec_logger.logger DEFAULT_EPICSSIGNAL_VALUE = object() @@ -18,7 +16,7 @@ class DeviceMock: self.name = name self.read_buffer = value self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} - self._enabled_set = True + self._read_only = False self._enabled = True def read(self): @@ -28,12 +26,12 @@ class DeviceMock: return self.read_buffer @property - def enabled_set(self) -> bool: - return self._enabled_set + def read_only(self) -> bool: + return self._read_only - @enabled_set.setter - def enabled_set(self, val: bool): - self._enabled_set = val + @read_only.setter + def read_only(self, val: bool): + self._read_only = val @property def enabled(self) -> bool: @@ -191,14 +189,7 @@ class ConfigSignal(Signal): self._readback = getattr(self.parent, self.storage_name)[self.name] return self._readback - def put( - self, - value, - connection_timeout=1, - callback=None, - timeout=1, - **kwargs, - ): + def put(self, value, connection_timeout=1, callback=None, timeout=1, **kwargs): """Using channel access, set the write PV to `value`. Keyword arguments are passed on to callbacks @@ -224,10 +215,7 @@ class ConfigSignal(Signal): getattr(self.parent, self.storage_name)[self.name] = value super().put(value, timestamp=timestamp, force=True) self._run_subs( - sub_type=self.SUB_VALUE, - old_value=old_value, - value=value, - timestamp=timestamp, + sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=timestamp ) def describe(self): diff --git a/tests/utils.py b/tests/utils.py index 84ebce8..f06eea8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,8 @@ +from unittest import mock + from bec_lib.devicemanager import DeviceContainer from bec_lib.tests.utils import ProducerMock -from unittest import mock - class SocketMock: """Socket Mock. Used for testing""" @@ -251,7 +251,7 @@ class DeviceMock: self.name = name self.read_buffer = value self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} - self._enabled_set = True + self._read_only = False self._enabled = True def read(self): @@ -263,14 +263,14 @@ class DeviceMock: return self.read_buffer @property - def enabled_set(self) -> bool: - """enabled_set property""" - return self._enabled_set + def read_only(self) -> bool: + """read only property""" + return self._read_only - @enabled_set.setter - def enabled_set(self, val: bool): - """enabled_set setter""" - self._enabled_set = val + @read_only.setter + def read_only(self, val: bool): + """read only setter""" + self._read_only = val @property def enabled(self) -> bool: From 63767a20119a74416cff69a9a1ea422b867f3bf7 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 21 Feb 2024 10:03:20 +0000 Subject: [PATCH 37/77] 0.23.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa6f192..1e84b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.23.1 (2024-02-21) + +### Fix + +* Replaced outdated enable_set by read_only ([`f91d0c4`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/f91d0c482d194e5f69c7206d0f6ad0971f84b0e1)) + ## v0.23.0 (2024-02-21) ### Feature diff --git a/setup.py b/setup.py index 2425b40..68e2049 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.23.0" +__version__ = "0.23.1" if __name__ == "__main__": setup( From bc973467b75d8ee494463afcd82fb84289371586 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 22 Feb 2024 21:37:42 +0100 Subject: [PATCH 38/77] test: added devices for e2e tests --- ophyd_devices/utils/bec_utils.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index c2cf88e..12d75af 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -2,7 +2,7 @@ import time from bec_lib import bec_logger from bec_lib.devicemanager import DeviceContainer -from ophyd import Kind, Signal +from ophyd import Device, Kind, Signal from ophyd_devices.utils.socket import data_shape, data_type @@ -244,3 +244,19 @@ class ConfigSignal(Signal): "shape": data_shape(val), } } + + +class DeviceClassConnectionError(Device): + + @property + def connected(self): + return False + + def wait_for_connection(self, all_signals=False, timeout=2): + raise RuntimeError("Connection error") + + +class DeviceClassInitError(Device): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + raise RuntimeError("Init error") From 2da6379e8eb346d856a68a8e5bc678dfff5b1600 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 22 Feb 2024 21:40:13 +0100 Subject: [PATCH 39/77] docs: added doc strings --- ophyd_devices/utils/bec_utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 12d75af..b711328 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -247,6 +247,10 @@ class ConfigSignal(Signal): class DeviceClassConnectionError(Device): + """ + Device that always raises a connection error when trying to connect. + It is used to test the wait_for_connection method in the DeviceServer. + """ @property def connected(self): @@ -257,6 +261,11 @@ class DeviceClassConnectionError(Device): class DeviceClassInitError(Device): + """ + Device that always raises an error when trying to construct the object. + It is used to test the error handling in the DeviceServer. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) raise RuntimeError("Init error") From 800c22e9592e288f8fe8dea2fb572b81742c6841 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 22 Feb 2024 14:42:35 +0100 Subject: [PATCH 40/77] feat: add lmfit for SimMonitor, refactored sim_data with baseclass, introduce slitproxy --- ophyd_devices/__init__.py | 6 +- ophyd_devices/sim/__init__.py | 8 +- ophyd_devices/sim/sim.py | 246 ++++---- ophyd_devices/sim/sim_data.py | 809 ++++++++++++++++---------- ophyd_devices/sim/sim_frameworks.py | 42 +- ophyd_devices/sim/sim_signals.py | 171 ++---- ophyd_devices/sim/sim_test_devices.py | 13 +- ophyd_devices/sim/test.py | 50 ++ tests/test_simulation.py | 12 +- 9 files changed, 822 insertions(+), 535 deletions(-) create mode 100644 ophyd_devices/sim/test.py diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index eccecdf..9b3073f 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -8,13 +8,15 @@ from .npoint.npoint import NPointAxis from .rt_lamni import RtFlomniMotor, RtLamniMotor from .sim.sim import SimCamera from .sim.sim import SimMonitor +from .sim.sim import SimFlyer +from .sim.sim import SimFlyer as SynFlyer from .sim.sim import SimMonitor as SynAxisMonitor from .sim.sim import SimMonitor as SynGaussBEC from .sim.sim import SimPositioner from .sim.sim import SimPositioner as SynAxisOPAAS -from .sim.sim import SynDeviceOPAAS, SynFlyer +from .sim.sim import SynDeviceOPAAS from .sim.sim_signals import ReadOnlySignal -from .sim.sim_frameworks import DeviceProxy, SlitLookup +from .sim.sim_frameworks import DeviceProxy, SlitProxy from .sim.sim_signals import ReadOnlySignal as SynSignalRO from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor diff --git a/ophyd_devices/sim/__init__.py b/ophyd_devices/sim/__init__.py index 2cd6e77..48b5c93 100644 --- a/ophyd_devices/sim/__init__.py +++ b/ophyd_devices/sim/__init__.py @@ -2,11 +2,11 @@ from .sim import ( SimPositioner, SimMonitor, SimCamera, - SynDynamicComponents, - SynFlyer, + SimFlyer, + SimFlyer as SynFlyer, ) from .sim_xtreme import SynXtremeOtf -from .sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal +from .sim_signals import SetableSignal, ReadOnlySignal -from .sim_frameworks import SlitLookup +from .sim_frameworks import SlitProxy diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 6dcae0f..fd64603 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -1,23 +1,32 @@ -from collections import defaultdict import os import threading import time as ttime -import warnings - import numpy as np + from bec_lib import MessageEndpoints, bec_logger, messages + from ophyd import Component as Cpt from ophyd import DynamicDeviceComponent as Dcpt from ophyd import Device, DeviceStatus, Kind -from ophyd import PositionerBase, Signal +from ophyd import PositionerBase + +from ophyd.flyers import FlyerInterface + from ophyd.sim import SynSignal +from ophyd.status import StatusBase from ophyd.utils import LimitError + from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin -from ophyd_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor + +from ophyd_devices.sim.sim_data import ( + SimulatedPositioner, + SimulatedDataCamera, + SimulatedDataMonitor, +) from ophyd_devices.sim.sim_test_devices import DummyController -from ophyd_devices.sim.sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal +from ophyd_devices.sim.sim_signals import SetableSignal, ReadOnlySignal logger = bec_logger.logger @@ -46,11 +55,11 @@ class SimMonitor(Device): """ - USER_ACCESS = ["sim"] + USER_ACCESS = ["sim", "registered_proxies"] sim_cls = SimulatedDataMonitor - readback = Cpt(ComputedReadOnlySignal, value=0, kind=Kind.hinted) + readback = Cpt(ReadOnlySignal, value=0, kind=Kind.hinted, compute_readback=True) SUB_READBACK = "readback" _default_sub = SUB_READBACK @@ -69,16 +78,16 @@ class SimMonitor(Device): self.precision = precision self.init_sim_params = sim_init self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) - self._lookup_table = [] + self._registered_proxies = {} super().__init__(name=name, parent=parent, kind=kind, **kwargs) self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) self.readback.name = self.name @property - def lookup_table(self) -> None: - """lookup_table property""" - return self._lookup_table + def registered_proxies(self) -> None: + """Dictionary of registered signal_names and proxies.""" + return self._registered_proxies class SimCamera(Device): @@ -100,7 +109,7 @@ class SimCamera(Device): """ - USER_ACCESS = ["sim"] + USER_ACCESS = ["sim", "registered_proxies"] sim_cls = SimulatedDataCamera SHAPE = (100, 100) @@ -116,9 +125,10 @@ class SimCamera(Device): image_shape = Cpt(SetableSignal, name="image_shape", value=SHAPE, kind=Kind.config) image = Cpt( - ComputedReadOnlySignal, + ReadOnlySignal, name="image", value=np.empty(SHAPE, dtype=np.uint16), + compute_readback=True, kind=Kind.omitted, ) @@ -134,7 +144,7 @@ class SimCamera(Device): ): self.device_manager = device_manager self.init_sim_params = sim_init - self._lookup_table = [] + self._registered_proxies = {} self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) super().__init__(name=name, parent=parent, kind=kind, **kwargs) @@ -144,9 +154,9 @@ class SimCamera(Device): self._update_scaninfo() @property - def lookup_table(self) -> None: - """lookup_table property""" - return self._lookup_table + def registered_proxies(self) -> None: + """Dictionary of registered signal_names and proxies.""" + return self._registered_proxies def trigger(self) -> DeviceStatus: """Trigger the camera to acquire images. @@ -225,30 +235,29 @@ class SimPositioner(Device, PositionerBase): """ A simulated device mimicing any 1D Axis device (position, temperature, rotation). + >>> motor = SimPositioner(name="motor") + Parameters ---------- - name : string, keyword only - readback_func : callable, optional - When the Device is set to ``x``, its readback will be updated to - ``f(x)``. This can be used to introduce random noise or a systematic - offset. - Expected signature: ``f(x) -> value``. - value : object, optional - The initial value. Default is 0. - delay : number, optional - Simulates how long it takes the device to "move". Default is 0 seconds. - precision : integer, optional - Digits of precision. Default is 3. - parent : Device, optional - Used internally if this Signal is made part of a larger Device. - kind : a member the Kind IntEnum (or equivalent integer), optional - Default is Kind.normal. See Kind for options. + name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.\ + Optional parameters: + ---------- + delay (int) : If 0, execution of move will be instant. If 1, exectution will depend on motor velocity. Default is 1. + update_frequency (int) : Frequency in Hz of the update of the simulated state during a move. Default is 2 Hz. + precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits. + tolerance (float) : Tolerance of the positioner to accept reaching target positions. Default is 0.5. + limits (tuple) : Tuple of the low and high limits of the positioner. Overrides low/high_limit_travel is specified. Default is None. + parent : Parent device, optional, is used internally if this signal/device is part of a larger device. + kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options. + device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically. + sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details. + """ # Specify which attributes are accessible via BEC client - USER_ACCESS = ["sim", "readback", "speed", "dummy_controller"] + USER_ACCESS = ["sim", "readback", "speed", "dummy_controller", "registered_proxies"] - sim_cls = SimulatedDataBase + sim_cls = SimulatedPositioner # Define the signals as class attributes readback = Cpt(ReadOnlySignal, name="readback", value=0, kind=Kind.hinted) @@ -256,60 +265,61 @@ class SimPositioner(Device, PositionerBase): motor_is_moving = Cpt(SetableSignal, value=0, kind=Kind.normal) # Config signals - velocity = Cpt(SetableSignal, value=1, kind=Kind.config) + velocity = Cpt(SetableSignal, value=100, kind=Kind.config) acceleration = Cpt(SetableSignal, value=1, kind=Kind.config) # Ommitted signals high_limit_travel = Cpt(SetableSignal, value=0, kind=Kind.omitted) low_limit_travel = Cpt(SetableSignal, value=0, kind=Kind.omitted) - unused = Cpt(Signal, value=1, kind=Kind.omitted) + unused = Cpt(SetableSignal, value=1, kind=Kind.omitted) SUB_READBACK = "readback" _default_sub = SUB_READBACK + # pylint: disable=too-many-arguments def __init__( self, - *, name, - readback_func=None, - value=0, - delay=1, - speed=1, + *, + delay: int = 1, update_frequency=2, precision=3, - parent=None, - labels=None, - kind=None, - limits=None, tolerance: float = 0.5, - sim: dict = None, + limits=None, + parent=None, + kind=None, + device_manager=None, + sim_init: dict = None, + # TODO remove after refactoring config + speed: float = 100, **kwargs, ): - # Whether motions should be instantaneous or depend on motor velocity self.delay = delay + self.device_manager = device_manager self.precision = precision self.tolerance = tolerance - self.init_sim_params = sim - self._lookup_table = [] + self.init_sim_params = sim_init + self._registered_proxies = {} - self.speed = speed self.update_frequency = update_frequency self._stopped = False self.dummy_controller = DummyController() - # initialize inner dictionary with simulated state self.sim = self.sim_cls(parent=self, **kwargs) - super().__init__(name=name, labels=labels, parent=parent, kind=kind, **kwargs) - # Rename self.readback.name to self.name, also in self.sim_state + super().__init__(name=name, parent=parent, kind=kind, **kwargs) self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) self.readback.name = self.name - # Init limits from deviceConfig if limits is not None: assert len(limits) == 2 self.low_limit_travel.put(limits[0]) self.high_limit_travel.put(limits[1]) + # @property + # def connected(self): + # """Return the connected state of the simulated device.""" + # return self.dummy_controller.connected + @property def limits(self): """Return the limits of the simulated device.""" @@ -325,11 +335,11 @@ class SimPositioner(Device, PositionerBase): """Return the high limit of the simulated device.""" return self.limits[1] - @property - def lookup_table(self) -> None: - """lookup_table property""" - return self._lookup_table + def registered_proxies(self) -> None: + """Dictionary of registered signal_names and proxies.""" + return self._registered_proxies + # pylint: disable=arguments-differ def check_value(self, value: any): """ Check that requested position is within existing limits. @@ -375,42 +385,41 @@ class SimPositioner(Device, PositionerBase): st = DeviceStatus(device=self) if self.delay: - # If self.delay is not 0, we use the speed and updated frequency of the device to compute the motion + def move_and_finish(): """Move the simulated device and finish the motion.""" success = True try: - # Compute final position with some jitter move_val = self._get_sim_state( self.setpoint.name ) + self.tolerance * np.random.uniform(-1, 1) - # Compute the number of updates needed to reach the final position with the given speed + updates = np.ceil( - np.abs(old_setpoint - move_val) / self.speed * self.update_frequency + np.abs(old_setpoint - move_val) + / self.velocity.get() + * self.update_frequency ) - # Loop over the updates and update the state of the simulated device + for ii in np.linspace(old_setpoint, move_val, int(updates)): ttime.sleep(1 / self.update_frequency) update_state(ii) - # Update the state of the simulated device to the final position + update_state(move_val) self._set_sim_state(self.motor_is_moving, 0) except DeviceStop: success = False finally: self._stopped = False - # Call function from positioner base to indicate that motion finished with success self._done_moving(success=success) - # Set status to finished + self._set_sim_state(self.motor_is_moving.name, 0) st.set_finished() - # Start motion in Thread threading.Thread(target=move_and_finish, daemon=True).start() else: - # If self.delay is 0, we move the simulated device instantaneously update_state(value) self._done_moving() + self._set_sim_state(self.motor_is_moving.name, 0) st.set_finished() return st @@ -420,7 +429,7 @@ class SimPositioner(Device, PositionerBase): self._stopped = True @property - def position(self): + def position(self) -> float: """Return the current position of the simulated device.""" return self.readback.get() @@ -430,57 +439,81 @@ class SimPositioner(Device, PositionerBase): return "mm" -class SynFlyer(Device, PositionerBase): +class SimFlyer(Device, PositionerBase, FlyerInterface): + """A simulated device mimicing any 2D Flyer device (position, temperature, rotation). + + The corresponding simulation class is sim_cls=SimulatedPositioner, more details on defaults within the simulation class. + + >>> flyer = SimFlyer(name="flyer") + + Parameters + ---------- + name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device. + precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits. + parent : Parent device, optional, is used internally if this signal/device is part of a larger device. + kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options. + device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically. + """ + + USER_ACCESS = ["sim", "registered_proxies"] + + sim_cls = SimulatedPositioner + + readback = Cpt( + ReadOnlySignal, name="readback", value=0, kind=Kind.hinted, compute_readback=False + ) + def __init__( self, + name: str, *, - name, - readback_func=None, - value=0, - delay=0, - speed=1, - update_frequency=2, - precision=3, + precision: int = 3, parent=None, - labels=None, kind=None, device_manager=None, + # TODO remove after refactoring config + speed: float = 100, + delay: int = 1, + update_frequency: int = 100, **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.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs) self.precision = precision - self.tolerance = kwargs.pop("tolerance", 0.5) self.device_manager = device_manager + self._registered_proxies = {} - # initialize values - self.sim_state["readback"] = readback_func(value) - self.sim_state["readback_ts"] = ttime.time() + super().__init__(name=name, parent=parent, kind=kind, **kwargs) + self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None) + self.readback.name = self.name - super().__init__(name=name, parent=parent, labels=labels, kind=kind, **kwargs) + @property + def registered_proxies(self) -> None: + """Dictionary of registered signal_names and proxies.""" + return self._registered_proxies @property def hints(self): + """Return the hints of the simulated device.""" return {"fields": ["flyer_samx", "flyer_samy"]} + @property + def egu(self) -> str: + """Return the engineering units of the simulated device.""" + return "mm" + + def complete(self) -> StatusBase: + """Complete the motion of the simulated device.""" + status = DeviceStatus(self) + status.set_finished() + return status + def kickoff(self, metadata, num_pos, positions, exp_time: float = 0): + """Kickoff the flyer to execute code during the scan.""" positions = np.asarray(positions) def produce_data(device, metadata): + """Simulate the data being produced by the flyer.""" buffer_time = 0.2 elapsed_time = 0 bundle = messages.BundleMessage() @@ -529,10 +562,6 @@ class SynFlyer(Device, PositionerBase): flyer.start() -class SynDynamicComponents(Device): - messages = Dcpt({f"message{i}": (SynSignal, None, {"name": f"msg{i}"}) for i in range(1, 6)}) - - class SynDeviceSubOPAAS(Device): zsub = Cpt(SimPositioner, name="zsub") @@ -541,3 +570,12 @@ class SynDeviceOPAAS(Device): x = Cpt(SimPositioner, name="x") y = Cpt(SimPositioner, name="y") z = Cpt(SynDeviceSubOPAAS, name="z") + + +class SynDynamicComponents(Device): + messages = Dcpt({f"message{i}": (SynSignal, None, {"name": f"msg{i}"}) for i in range(1, 6)}) + + +if __name__ == "__main__": + cam = SimCamera(name="cam") + cam.image.read() diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 3179503..4443515 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -1,10 +1,15 @@ from __future__ import annotations from collections import defaultdict +from abc import ABC, abstractmethod + +from prettytable import PrettyTable import enum +import inspect import time as ttime import numpy as np +from lmfit import models, Model from bec_lib import bec_logger @@ -15,11 +20,11 @@ class SimulatedDataException(Exception): """Exception raised when there is an issue with the simulated data.""" -class SimulationType(str, enum.Enum): +class SimulationType2D(str, enum.Enum): """Type of simulation to steer simulated data.""" CONSTANT = "constant" - GAUSSIAN = "gauss" + GAUSSIAN = "gaussian" class NoiseType(str, enum.Enum): @@ -37,178 +42,374 @@ class HotPixelType(str, enum.Enum): FLUCTUATING = "fluctuating" -class SimulatedDataBase: +DEFAULT_PARAMS_LMFIT = { + "c0": 1, + "c1": 1, + "c2": 1, + "c3": 1, + "c4": 1, + "c": 100, + "amplitude": 100, + "center": 0, + "sigma": 1, +} + +DEFAULT_PARAMS_NOISE = { + "noise": NoiseType.UNIFORM, + "noise_multiplier": 10, +} + +DEFAULT_PARAMS_MOTOR = { + "ref_motor": "samx", +} + +DEFAULT_PARAMS_CAMERA_GAUSSIAN = { + "amplitude": 100, + "center_offset": np.array([0, 0]), + "covariance": np.array([[400, 100], [100, 400]]), +} + +DEFAULT_PARAMS_CAMERA_CONSTANT = { + "amplitude": 100, +} + +DEFAULT_PARAMS_HOT_PIXEL = { + "hot_pixel_coords": np.array([[24, 24], [50, 20], [4, 40]]), + "hot_pixel_types": [ + HotPixelType.FLUCTUATING, + HotPixelType.CONSTANT, + HotPixelType.FLUCTUATING, + ], + "hot_pixel_values": np.array([1e4, 1e6, 1e4]), +} + + +class SimulatedDataBase(ABC): + """Abstract base class for simulated data. + + This class should be subclassed to implement the simulated data for a specific device. + It provides the basic functionality to set and get data from the simulated data class + + --------------------- + The class provides the following methods: + + - execute_simulation_method: execute a method from the simulated data class or reroute execution to device proxy class + - sim_select_model: select the active simulation model + - sim_params: get the parameters for the active simulation mdoel + - sim_models: get the available simulation models + - update_sim_state: update the simulated state of the device + """ + USER_ACCESS = [ - "get_sim_params", - "set_sim_params", - "get_sim_type", - "set_sim_type", + "sim_params", + "sim_select_model", + "sim_get_models", + "sim_show_all", ] def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None: + """ + Note: + self._model_params duplicates parameters from _params that are solely relevant for the model used. + This facilitates easier and faster access for computing the simulated state using the lmfit package. + """ self.parent = parent - self.sim_state = defaultdict(dict) - self._all_params = defaultdict(dict) self.device_manager = device_manager - self._simulation_type = None - self.lookup_table = getattr(self.parent, "lookup_table", []) - self.init_paramaters(**kwargs) - self._active_params = self._all_params.get(self._simulation_type, None) + self.sim_state = defaultdict(dict) + self.registered_proxies = getattr(self.parent, "registered_proxies", {}) + self._model = {} + self._model_params = None + self._params = {} - def execute_simulation_method(self, *args, method=None, **kwargs) -> any: - """Execute the method from the lookup table.""" - if self.lookup_table and self.device_manager.devices.get(self.lookup_table[0]) is not None: - sim_device = self.device_manager.devices.get(self.lookup_table[0]) - # pylint: disable=protected-access - if sim_device.enabled is True: - method = sim_device.obj.lookup[self.parent.name]["method"] - args = sim_device.obj.lookup[self.parent.name]["args"] - kwargs = sim_device.obj.lookup[self.parent.name]["kwargs"] + def execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any: + """ + Execute either the provided method or reroutes the method execution + to a device proxy in case it is registered in self.parentregistered_proxies. + """ + if self.registered_proxies and self.device_manager: + for proxy_name, signal in self.registered_proxies.items(): + if signal == signal_name or f"{self.parent.name}_{signal}" == signal_name: + sim_proxy = self.device_manager.devices.get(proxy_name, None) + if sim_proxy and sim_proxy.enabled is True: + method = sim_proxy.obj.lookup[self.parent.name]["method"] + args = sim_proxy.obj.lookup[self.parent.name]["args"] + kwargs = sim_proxy.obj.lookup[self.parent.name]["kwargs"] + break if method is not None: return method(*args, **kwargs) raise SimulatedDataException(f"Method {method} is not available for {self.parent.name}") - def init_paramaters(self, **kwargs): - """Initialize the parameters for the Simulated Data - - This methods should be implemented by the subclass. - - It sets the default parameters for the simulated data in - self._params and calls self._update_init_params() + def sim_select_model(self, model: str) -> None: """ - - def get_sim_params(self) -> dict: - """Return the currently parameters for the active simulation type in sim_type. - - These parameters can be changed with set_sim_params. - - Returns: - dict: Parameters of the currently active simulation in sim_type. - """ - return self._active_params - - def set_sim_params(self, params: dict) -> None: - """Change the current set of parameters for the active simulation type. + Method to select the active simulation model. + It will initiate the model_cls and parameters for the model. Args: - params (dict): New parameters for the active simulation type. + model (str): Name of the simulation model to select. - Raises: - SimulatedDataException: If the new parameters can not be set or is not part of the parameters initiated. """ - for k, v in params.items(): - try: - if k == "noise": - self._active_params[k] = NoiseType(v) - else: - self._active_params[k] = v - except Exception as exc: - raise SimulatedDataException( - f"Could not set {k} to {v} in {self._active_params} with exception {exc}" - ) from exc + model_cls = self.get_model_cls(model) + self._model = model_cls() if callable(model_cls) else model_cls + self._params = self.get_params_for_model_cls() + self._params.update(self._get_additional_params()) + print(self._get_table_active_simulation()) - def get_sim_type(self) -> SimulationType: - """Return the simulation type of the simulation. + @property + def sim_params(self) -> dict: + """ + Property that returns the parameters for the active simulation model. It can also + be used to set the parameters for the active simulation updating the parameters of the model. Returns: - SimulationType: Type of simulation (e.g. "constant" or "gauss). + dict: Parameters for the active simulation model. + + The following example shows how to update the noise parameter of the current simulation. + >>> dev..sim.sim_params = {"noise": "poisson"} """ - return self._simulation_type + return self._params - def set_sim_type(self, simulation_type: SimulationType) -> None: - """Set the simulation type of the simulation.""" - try: - self._simulation_type = SimulationType(simulation_type) - except ValueError as exc: - raise SimulatedDataException( - f"Could not set simulation type to {simulation_type}. Valid options are 'constant'" - " and 'gauss'" - ) from exc - self._active_params = self._all_params.get(self._simulation_type, None) - - def _compute_sim_state(self, signal_name: str) -> None: - """Update the simulated state of the device. - - If no computation is relevant, ignore this method. - Otherwise implement it in the subclass. + @sim_params.setter + def sim_params(self, params: dict): """ + Method to set the parameters for the active simulation model. + """ + for k, v in params.items(): + if k in self.sim_params: + if k == "noise": + self._params[k] = NoiseType(v) + elif k == "hot_pixel_types": + self._params[k] = [HotPixelType(entry) for entry in v] + else: + self._params[k] = v + if isinstance(self._model, Model) and k in self._model_params: + self._model_params[k].value = v + else: + raise SimulatedDataException(f"Parameter {k} not found in {self.sim_params}.") + + def sim_get_models(self) -> list: + """ + Method to get the all available simulation models. + """ + return self.get_all_sim_models() def update_sim_state(self, signal_name: str, value: any) -> None: """Update the simulated state of the device. Args: signal_name (str): Name of the signal to update. + value (any): Value to update in the simulated state. """ self.sim_state[signal_name]["value"] = value self.sim_state[signal_name]["timestamp"] = ttime.time() - def _update_init_params( - self, - sim_type_default: SimulationType, - ) -> None: - """Update the initial parameters of the simulated data with input from deviceConfig. + @abstractmethod + def _get_additional_params(self) -> dict: + """Initialize the default parameters for the noise.""" - Args: - sim_type_default (SimulationType): Default simulation type to use if not specified in deviceConfig. + @abstractmethod + def get_model_cls(self, model: str) -> any: """ - init_params = getattr(self.parent, "init_sim_params", None) - for sim_type in self._all_params.values(): - for sim_type_config_element in sim_type: - if init_params: - if sim_type_config_element in init_params: - sim_type[sim_type_config_element] = init_params[sim_type_config_element] - # Set simulation type to default if not specified in deviceConfig - sim_type_select = ( - init_params.get("sim_type", sim_type_default) if init_params else sim_type_default + Method to get the class for the active simulation model_cls + """ + + @abstractmethod + def get_params_for_model_cls(self) -> dict: + """ + Method to get the parameters for the active simulation model. + """ + + @abstractmethod + def get_all_sim_models(self) -> list[str]: + """ + Method to get all names from the available simulation models. + + Returns: + list: List of available simulation models. + """ + + @abstractmethod + def compute_sim_state(self, signal_name: str, compute_readback: bool) -> None: + """ + Method to compute the simulated state of the device. + """ + + def _get_table_active_simulation(self, width: int = 140) -> PrettyTable: + """Return a table with the active simulation model and parameters.""" + table = PrettyTable() + table.title = f"Currently active model: {self._model}" + table.field_names = ["Parameter", "Value", "Type"] + for k, v in self.sim_params.items(): + table.add_row([k, f"{v}", f"{type(v)}"]) + table._min_width["Parameter"] = 25 if width > 75 else width // 3 + table._min_width["Type"] = 25 if width > 75 else width // 3 + table.max_table_width = width + table._min_table_width = width + + return table + + def _get_table_method_information(self, width: int = 140) -> PrettyTable: + """Return a table with the information about methods.""" + table = PrettyTable() + table.max_width["Value"] = 120 + table.hrules = 1 + table.title = "Available methods within the simulation module" + table.field_names = ["Method", "Docstring"] + + table.add_row( + [ + self.sim_get_models.__name__, + f"{self.sim_get_models.__doc__}", + ] ) - self.set_sim_type(sim_type_select) + table.add_row([self.sim_select_model.__name__, self.sim_select_model.__doc__]) + table.add_row(["sim_params", self.__class__.sim_params.__doc__]) + table.max_table_width = width + table._min_table_width = width + table.align["Docstring"] = "l" + + return table + + def sim_show_all(self): + """Returns a summary about the active simulation and available methods.""" + width = 150 + print(self._get_table_active_simulation(width=width)) + print(self._get_table_method_information(width=width)) + table = PrettyTable() + table.title = "Simulation module for current device" + table.field_names = ["All available models"] + table.add_row([", ".join(self.get_all_sim_models())]) + table.max_table_width = width + table._min_table_width = width + print(table) + + +class SimulatedPositioner(SimulatedDataBase): + """Simulated data class for a positioner.""" + + def _init_default_additional_params(self) -> None: + """No need to init additional parameters for Positioner.""" + + def get_model_cls(self, model: str) -> any: + """For the simulated positioners, no simulation models are currently implemented.""" + return None + + def get_params_for_model_cls(self) -> dict: + """For the simulated positioners, no simulation models are currently implemented.""" + return {} + + def get_all_sim_models(self) -> list[str]: + """ + For the simulated positioners, no simulation models are currently implemented. + + Returns: + list: List of available simulation models. + """ + return [] + + def _get_additional_params(self) -> dict: + """No need to add additional parameters for Positioner.""" + return {} + + def compute_sim_state(self, signal_name: str, compute_readback: bool) -> None: + """ + For the simulated positioners, a computed signal is currently not used. + The position is updated by the parent device, and readback/setpoint values + have a jitter/tolerance introduced directly in the parent class (SimPositioner). + """ + if compute_readback: + method = None + value = self.execute_simulation_method(method=method, signal_name=signal_name) + self.update_sim_state(signal_name, value) class SimulatedDataMonitor(SimulatedDataBase): - """Simulated data for a monitor.""" + """Simulated data class for a monitor.""" - def init_paramaters(self, **kwargs): - """Initialize the parameters for the simulated data + def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None: + self._model_lookup = self.init_lmfit_models() + super().__init__(*args, parent=parent, device_manager=device_manager, **kwargs) + self._init_default() - This method will fill self._all_params with the default parameters for - SimulationType.CONSTANT and SimulationType.GAUSSIAN. - New simulation types can be added by adding a new key to self._all_params, - together with the required parameters for that simulation type. Please - also complement the docstring of this method with the new simulation type. + def _get_additional_params(self) -> None: + params = DEFAULT_PARAMS_NOISE.copy() + params.update(DEFAULT_PARAMS_MOTOR.copy()) + return params - For SimulationType.CONSTANT: - Amp is the amplitude of the constant value. - Noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'. - Noise multiplier is the multiplier of the noise, only relevant for uniform noise. + def _init_default(self) -> None: + """Initialize the default parameters for the simulated data.""" + self.sim_select_model("ConstantModel") - For SimulationType.GAUSSIAN: - ref_motor is the motor that is used as reference to compute the gaussian. - amp is the amplitude of the gaussian. - cen is the center of the gaussian. - sig is the sigma of the gaussian. - noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'. - noise multiplier is the multiplier of the noise, only relevant for uniform noise. + def get_model_cls(self, model: str) -> any: + """Get the class for the active simulation model.""" + if model not in self._model_lookup: + raise SimulatedDataException(f"Model {model} not found in {self._model_lookup.keys()}.") + return self._model_lookup[model] + + def get_all_sim_models(self) -> list[str]: """ - self._all_params = { - SimulationType.CONSTANT: { - "amp": 100, - "noise": NoiseType.POISSON, - "noise_multiplier": 0.1, - }, - SimulationType.GAUSSIAN: { - "ref_motor": "samx", - "amp": 100, - "cen": 0, - "sig": 1, - "noise": NoiseType.NONE, - "noise_multiplier": 0.1, - }, - } - # Update init parameters and set simulation type to Constant if not specified otherwise in init_sim_params - self._update_init_params(sim_type_default=SimulationType.CONSTANT) + Method to get all names from the available simulation models from the lmfit.models pool. - def _compute_sim_state(self, signal_name: str) -> None: + Returns: + list: List of available simulation models. + """ + return list(self._model_lookup.keys()) + + def get_params_for_model_cls(self) -> dict: + """Get the parameters for the active simulation model. + + Check if default parameters are available for lmfit parameters. + + Args: + sim_model (str): Name of the simulation model. + Returns: + dict: {name: value} for the active simulation model. + """ + rtr = {} + params = self._model.make_params() + for name, parameter in params.items(): + if name in DEFAULT_PARAMS_LMFIT: + rtr[name] = DEFAULT_PARAMS_LMFIT[name] + parameter.value = rtr[name] + else: + if not any([np.isnan(parameter.value), np.isinf(parameter.value)]): + rtr[name] = parameter.value + else: + rtr[name] = 1 + parameter.value = 1 + self._model_params = params + return rtr + + def model_lookup(self): + """Get available models from lmfit.models.""" + return self._model_lookup + + def init_lmfit_models(self) -> dict: + """ + Get available models from lmfit.models. + + Exclude Gaussian2dModel, ExpressionModel, Model, SplineModel. + + Returns: + dictionary of model name : model class pairs for available models from LMFit. + """ + model_lookup = {} + for name, model_cls in inspect.getmembers(models): + try: + is_model = issubclass(model_cls, Model) + except TypeError: + is_model = False + if is_model and name not in [ + "Gaussian2dModel", + "ExpressionModel", + "Model", + "SplineModel", + ]: + model_lookup[name] = model_cls + + return model_lookup + + def compute_sim_state(self, signal_name: str, compute_readback: bool) -> None: """Update the simulated state of the device. It will update the value in self.sim_state with the value computed by @@ -217,153 +418,193 @@ class SimulatedDataMonitor(SimulatedDataBase): Args: signal_name (str): Name of the signal to update. """ - if self.get_sim_type() == SimulationType.CONSTANT: - method = "_compute_constant" - # value = self._compute_constant() - elif self.get_sim_type() == SimulationType.GAUSSIAN: - method = "_compute_gaussian" - # value = self._compute_gaussian() + if compute_readback: + method = self._compute + value = self.execute_simulation_method(method=method, signal_name=signal_name) + self.update_sim_state(signal_name, value) - value = self.execute_simulation_method(method=getattr(self, method)) + def _compute(self, *args, **kwargs) -> float: + mot_name = self.sim_params["ref_motor"] + if self.device_manager and mot_name in self.device_manager.devices: + motor_pos = self.device_manager.devices[mot_name].obj.read()[mot_name]["value"] + else: + motor_pos = 0 + method = self._model + value = float(method.eval(params=self._model_params, x=motor_pos)) + return self._add_noise(value, self.sim_params["noise"], self.sim_params["noise_multiplier"]) + + def _add_noise(self, v: float, noise: NoiseType, noise_multiplier: float) -> float: + """ + Add the currently activated noise to the simulated data. + If NoiseType.NONE is active, the value will be returned + + Args: + v (float): Value to add noise to. + Returns: + float: Value with added noise. + """ + if noise == NoiseType.POISSON: + ceiled_v = np.ceil(v) + v = np.random.poisson(ceiled_v, 1)[0] if ceiled_v > 0 else ceiled_v + return v + elif noise == NoiseType.UNIFORM: + v += np.random.uniform(-1, 1) * noise_multiplier + return v + return v + + +class SimulatedDataCamera(SimulatedDataBase): + """Simulated class to compute data for a 2D camera.""" + + def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None: + self._model_lookup = self.init_2D_models() + self._all_default_model_params = defaultdict(dict) + self._init_default_camera_params() + super().__init__(*args, parent=parent, device_manager=device_manager, **kwargs) + self._init_default() + + def _init_default(self) -> None: + """Initialize the default model for a simulated camera + + Use the default model "Gaussian". + """ + self.sim_select_model(SimulationType2D.GAUSSIAN) + + def init_2D_models(self) -> dict: + """ + Get the available models for 2D camera simulations. + """ + model_lookup = {} + for _, model_cls in inspect.getmembers(SimulationType2D): + if isinstance(model_cls, SimulationType2D): + model_lookup[model_cls.value] = model_cls + return model_lookup + + def _get_additional_params(self) -> None: + params = DEFAULT_PARAMS_NOISE.copy() + params.update(DEFAULT_PARAMS_HOT_PIXEL.copy()) + return params + + def _init_default_camera_params(self) -> None: + """Initiate additional params for the simulated camera.""" + self._all_default_model_params.update( + { + self._model_lookup[ + SimulationType2D.CONSTANT.value + ]: DEFAULT_PARAMS_CAMERA_CONSTANT.copy() + } + ) + self._all_default_model_params.update( + { + self._model_lookup[ + SimulationType2D.GAUSSIAN.value + ]: DEFAULT_PARAMS_CAMERA_GAUSSIAN.copy() + } + ) + + def get_model_cls(self, model: str) -> any: + """For the simulated positioners, no simulation models are currently implemented.""" + if model not in self._model_lookup: + raise SimulatedDataException(f"Model {model} not found in {self._model_lookup.keys()}.") + return self._model_lookup[model] + + def get_params_for_model_cls(self) -> dict: + """For the simulated positioners, no simulation models are currently implemented.""" + return self._all_default_model_params[self._model.value] + + def get_all_sim_models(self) -> list[str]: + """ + For the simulated positioners, no simulation models are currently implemented. + + Returns: + list: List of available simulation models. + """ + return [entry.value for entry in self._model_lookup.values()] + + def compute_sim_state(self, signal_name: str, compute_readback: bool) -> None: + """Update the simulated state of the device. + + It will update the value in self.sim_state with the value computed by + the chosen simulation type. + + Args: + signal_name (str) : Name of the signal to update. + compute_readback (bool) : Flag whether to compute readback based on function hosted in SimulatedData + """ + if compute_readback: + if self._model == SimulationType2D.CONSTANT: + method = "_compute_constant" + elif self._model == SimulationType2D.GAUSSIAN: + method = "_compute_gaussian" + value = self.execute_simulation_method( + signal_name=signal_name, method=getattr(self, method) + ) + else: + value = self._compute_empty_image() self.update_sim_state(signal_name, value) - def _compute_constant(self) -> float: - """Computes constant value and adds noise if activated.""" - v = self._active_params["amp"] - if self._active_params["noise"] == NoiseType.POISSON: - v = np.random.poisson(np.round(v), 1)[0] - return v - elif self._active_params["noise"] == NoiseType.UNIFORM: - v += np.random.uniform(-1, 1) * self._active_params["noise_multiplier"] - return v - elif self._active_params["noise"] == NoiseType.NONE: - v = self._active_params["amp"] - return v - else: + def _compute_empty_image(self) -> np.ndarray: + """Computes return value for sim_type = "empty_image". + + Returns: + float: 0 + """ + try: + shape = self.parent.image_shape.get() + return np.zeros(shape) + except SimulatedDataException as exc: raise SimulatedDataException( - f"Unknown noise type {self._active_params['noise']}. Please choose from 'poisson'," - " 'uniform' or 'none'." - ) + f"Could not compute empty image for {self.parent.name} with {exc} raised. Deactivate eiger to continue." + ) from exc + + def _compute_constant(self) -> np.ndarray: + """Compute a return value for SimulationType2D constant.""" + try: + shape = self.parent.image_shape.get() + v = self._model_params.get("amplitude") * np.ones(shape, dtype=np.uint16) + return self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"]) + except SimulatedDataException as exc: + raise SimulatedDataException( + f"Could not compute constant for {self.parent.name} with {exc} raised. Deactivate eiger to continue." + ) from exc def _compute_gaussian(self) -> float: """Computes return value for sim_type = "gauss". The value is based on the parameters for the gaussian in - self._active_params and the position of the ref_motor - and adds noise based on the noise type. + self._active_params and adds noise based on the noise type. If computation fails, it returns 0. Returns: float """ - params = self._active_params try: - motor_pos = self.device_manager.devices[params["ref_motor"]].obj.read()[ - params["ref_motor"] - ]["value"] - v = params["amp"] * np.exp( - -((motor_pos - params["cen"]) ** 2) / (2 * params["sig"] ** 2) + amp = self.sim_params.get("amplitude") + cov = self.sim_params.get("covariance") + cen_off = self.sim_params.get("center_offset") + shape = self.sim_state[self.parent.image_shape.name]["value"] + pos, offset, cov, amp = self._prepare_params_gauss( + amp=amp, cov=cov, offset=cen_off, shape=shape + ) + + v = self._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp) + v = self._add_noise( + v, + noise=self.sim_params["noise"], + noise_multiplier=self.sim_params["noise_multiplier"], + ) + return self._add_hot_pixel( + v, + coords=self.sim_params["hot_pixel_coords"], + hot_pixel_types=self.sim_params["hot_pixel_types"], + values=self.sim_params["hot_pixel_values"], ) - if params["noise"] == NoiseType.POISSON: - v = np.random.poisson(np.round(v), 1)[0] - elif params["noise"] == NoiseType.UNIFORM: - v += np.random.uniform(-1, 1) * params["noise_multiplier"] - return v except SimulatedDataException as exc: raise SimulatedDataException( f"Could not compute gaussian for {self.parent.name} with {exc} raised. Deactivate eiger to continue." ) from exc - -class SimulatedDataCamera(SimulatedDataBase): - """Simulated class to compute data for a 2D camera.""" - - def init_paramaters(self, **kwargs): - """Initialize the parameters for the simulated data - - This method will fill self._all_params with the default parameters for - SimulationType.CONSTANT and SimulationType.GAUSSIAN. - New simulation types can be added by adding a new key to self._all_params, - together with the required parameters for that simulation type. Please - also complement the docstring of this method with the new simulation type. - - For SimulationType.CONSTANT: - Amp is the amplitude of the constant value. - Noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'. - Noise multiplier is the multiplier of the noise, only relevant for uniform noise. - - For SimulationType.GAUSSIAN: - amp is the amplitude of the gaussian. - cen_off is the pixel offset from the center of the gaussian from the center of the image. - It is passed as a numpy array. - cov is the 2D covariance matrix used to specify the shape of the gaussian. - It is a 2x2 matrix and will be passed as a numpy array. - noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'. - noise multiplier is the multiplier of the noise, only relevant for uniform noise. - """ - self._all_params = { - SimulationType.CONSTANT: { - "amp": 100, - "noise": NoiseType.POISSON, - "noise_multiplier": 0.1, - "hot_pixel": { - "coords": np.array([[100, 100], [200, 200]]), - "type": [HotPixelType.CONSTANT, HotPixelType.FLUCTUATING], - "value": [1e6, 1e4], - }, - }, - SimulationType.GAUSSIAN: { - "amp": 100, - "cen_off": np.array([0, 0]), - "cov": np.array([[10, 5], [5, 10]]), - "noise": NoiseType.NONE, - "noise_multiplier": 0.1, - "hot_pixel": { - "coords": np.array([[240, 240], [50, 20], [40, 400]]), - "type": [ - HotPixelType.FLUCTUATING, - HotPixelType.CONSTANT, - HotPixelType.FLUCTUATING, - ], - "value": np.array([1e4, 1e6, 1e4]), - }, - }, - } - # Update init parameters and set simulation type to Gaussian if not specified otherwise in init_sim_params - self._update_init_params(sim_type_default=SimulationType.GAUSSIAN) - - def _compute_sim_state(self, signal_name: str) -> None: - """Update the simulated state of the device. - - It will update the value in self.sim_state with the value computed by - the chosen simulation type. - - Args: - signal_name (str): Name of the signal to update. - """ - if self.get_sim_type() == SimulationType.CONSTANT: - method = "_compute_constant" - # value = self._compute_constant() - elif self.get_sim_type() == SimulationType.GAUSSIAN: - method = "_compute_gaussian" - # value = self._compute_gaussian() - - value = self.execute_simulation_method(method=getattr(self, method)) - - self.update_sim_state(signal_name, value) - - def _compute_constant(self) -> float: - """Compute a return value for sim_type = Constant.""" - try: - shape = self.sim_state[self.parent.image_shape.name]["value"] - v = self._active_params["amp"] * np.ones(shape, dtype=np.uint16) - return self._add_noise(v, self._active_params["noise"]) - except SimulatedDataException as exc: - raise SimulatedDataException( - f"Could not compute constant for {self.parent.name} with {exc} raised. Deactivate eiger to continue." - ) from exc - def _compute_multivariate_gaussian( self, pos: np.ndarray | list, @@ -375,7 +616,7 @@ class SimulatedDataCamera(SimulatedDataBase): Args: pos (np.ndarray): Position of the gaussian. - cen_off (np.ndarray): Offset from cener of image for the gaussian. + cen_off (np.ndarray): Offset from center of image for the gaussian. cov (np.ndarray): Covariance matrix of the gaussian. Returns: @@ -398,11 +639,15 @@ class SimulatedDataCamera(SimulatedDataBase): v *= amp / np.max(v) return v - def _prepare_params_gauss(self, params: dict, shape: tuple) -> tuple: + def _prepare_params_gauss( + self, amp: float, cov: np.ndarray, offset: np.ndarray, shape: tuple + ) -> tuple: """Prepare the positions for the gaussian. Args: - params (dict): Parameters for the gaussian. + amp (float): Amplitude of the gaussian. + cov (np.ndarray): Covariance matrix of the gaussian. + offset (np.ndarray): Offset from the center of the image. shape (tuple): Shape of the image. Returns: tuple: Positions, offset and covariance matrix for the gaussian. @@ -415,12 +660,9 @@ class SimulatedDataCamera(SimulatedDataBase): pos[:, :, 0] = x pos[:, :, 1] = y - offset = params["cen_off"] - cov = params["cov"] - amp = params["amp"] return pos, offset, cov, amp - def _add_noise(self, v: np.ndarray, noise: NoiseType) -> np.ndarray: + def _add_noise(self, v: np.ndarray, noise: NoiseType, noise_multiplier: float) -> np.ndarray: """Add noise to the simulated data. Args: @@ -431,51 +673,26 @@ class SimulatedDataCamera(SimulatedDataBase): v = np.random.poisson(np.round(v), v.shape) return v if noise == NoiseType.UNIFORM: - multiplier = self._active_params["noise_multiplier"] - v += np.random.uniform(-multiplier, multiplier, v.shape) + v += np.random.uniform(-noise_multiplier, noise_multiplier, v.shape) return v - if self._active_params["noise"] == NoiseType.NONE: + if noise == NoiseType.NONE: return v - def _add_hot_pixel(self, v: np.ndarray, hot_pixel: dict) -> np.ndarray: + def _add_hot_pixel( + self, v: np.ndarray, coords: list, hot_pixel_types: list, values: list + ) -> np.ndarray: """Add hot pixels to the simulated data. Args: v (np.ndarray): Simulated data. hot_pixel (dict): Hot pixel parameters. """ - for coords, hot_pixel_type, value in zip( - hot_pixel["coords"], hot_pixel["type"], hot_pixel["value"] - ): - if coords[0] < v.shape[0] and coords[1] < v.shape[1]: + for coord, hot_pixel_type, value in zip(coords, hot_pixel_types, values): + if coord[0] < v.shape[0] and coord[1] < v.shape[1]: if hot_pixel_type == HotPixelType.CONSTANT: - v[coords[0], coords[1]] = value + v[coord[0], coord[1]] = value elif hot_pixel_type == HotPixelType.FLUCTUATING: maximum = np.max(v) if np.max(v) != 0 else 1 - if v[coords[0], coords[1]] / maximum > 0.5: - v[coords[0], coords[1]] = value + if v[coord[0], coord[1]] / maximum > 0.5: + v[coord[0], coord[1]] = value return v - - def _compute_gaussian(self) -> float: - """Computes return value for sim_type = "gauss". - - The value is based on the parameters for the gaussian in - self._active_params and adds noise based on the noise type. - - If computation fails, it returns 0. - - Returns: float - """ - - try: - params = self._active_params - shape = self.sim_state[self.parent.image_shape.name]["value"] - pos, offset, cov, amp = self._prepare_params_gauss(self._active_params, shape) - - v = self._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp) - v = self._add_noise(v, params["noise"]) - return self._add_hot_pixel(v, params["hot_pixel"]) - except SimulatedDataException as exc: - raise SimulatedDataException( - f"Could not compute gaussian for {self.parent.name} with {exc} raised. Deactivate eiger to continue." - ) from exc diff --git a/ophyd_devices/sim/sim_frameworks.py b/ophyd_devices/sim/sim_frameworks.py index 4c6f784..fe13cfb 100644 --- a/ophyd_devices/sim/sim_frameworks.py +++ b/ophyd_devices/sim/sim_frameworks.py @@ -10,7 +10,7 @@ class DeviceProxy(BECDeviceBase): """DeviceProxy class inherits from BECDeviceBase.""" -class SlitLookup(DeviceProxy): +class SlitProxy(DeviceProxy): """ Simulation framework to immidate the behaviour of slits. @@ -27,11 +27,12 @@ class SlitLookup(DeviceProxy): slit_sim: readoutPriority: on_request - deviceClass: SlitLookup + deviceClass: SlitProxy deviceConfig: eiger: - cen_off: [0, 0] # [x,y] - cov: [[1000, 500], [200, 1000]] # [[x,x],[y,y]] + signal_name: image + center_offset: [0, 0] # [x,y] + covariance: [[1000, 500], [200, 1000]] # [[x,x],[y,y]] pixel_size: 0.01 ref_motors: [samx, samy] slit_width: [1, 1] @@ -61,7 +62,9 @@ class SlitLookup(DeviceProxy): print(self.__doc__) def _update_device_config(self, config: dict) -> None: - """Update the config from the device_config for the pinhole lookup table. + """ + BEC will call this method on every object upon initializing devices to pass over the deviceConfig + from the config file. It can be conveniently be used to hand over initial parameters to the device. Args: config (dict): Config dictionary. @@ -83,8 +86,8 @@ class SlitLookup(DeviceProxy): """Compile the lookup table for the simulated camera.""" for device_name in self.config.keys(): self._lookup[device_name] = { - # "obj": self, "method": self._compute, + "signal_name": self.config[device_name]["signal_name"], "args": (device_name,), "kwargs": {}, } @@ -96,22 +99,28 @@ class SlitLookup(DeviceProxy): Args: device_name (str): Name of the device. + signal_name (str): Name of the signal. Returns: np.ndarray: Lookup table for the simulated camera. """ device_obj = self.device_manager.devices.get(device_name).obj - params = device_obj.sim._all_params.get("gauss") + params = device_obj.sim.sim_params shape = device_obj.image_shape.get() params.update( { "noise": NoiseType.POISSON, - "cov": np.array(self.config[device_name]["cov"]), - "cen_off": np.array(self.config[device_name]["cen_off"]), + "covariance": np.array(self.config[device_name]["covariance"]), + "center_offset": np.array(self.config[device_name]["center_offset"]), } ) + amp = params.get("amplitude") + cov = params.get("covariance") + cen_off = params.get("center_offset") - pos, offset, cov, amp = device_obj.sim._prepare_params_gauss(params, shape) + pos, offset, cov, amp = device_obj.sim._prepare_params_gauss( + amp=amp, cov=cov, offset=cen_off, shape=shape + ) v = device_obj.sim._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp) device_pos = self.config[device_name]["pixel_size"] * pos valid_mask = self._create_mask( @@ -122,8 +131,15 @@ class SlitLookup(DeviceProxy): ) valid_mask = self._blur_image(valid_mask, sigma=self._gaussian_blur_sigma) v *= valid_mask - v = device_obj.sim._add_noise(v, params["noise"]) - v = device_obj.sim._add_hot_pixel(v, params["hot_pixel"]) + v = device_obj.sim._add_noise( + v, noise=params["noise"], noise_multiplier=params["noise_multiplier"] + ) + v = device_obj.sim._add_hot_pixel( + v, + coords=params["hot_pixel_coords"], + hot_pixel_types=params["hot_pixel_types"], + values=params["hot_pixel_values"], + ) return v def _blur_image(self, image: np.ndarray, sigma: float = 1) -> np.ndarray: @@ -159,6 +175,6 @@ class SlitLookup(DeviceProxy): if __name__ == "__main__": # Example usage - pinhole = SlitLookup(name="pinhole", device_manager=None) + pinhole = SlitProxy(name="pinhole", device_manager=None) pinhole.describe() print(pinhole) diff --git a/ophyd_devices/sim/sim_signals.py b/ophyd_devices/sim/sim_signals.py index f0ae27c..51c6004 100644 --- a/ophyd_devices/sim/sim_signals.py +++ b/ophyd_devices/sim/sim_signals.py @@ -1,28 +1,40 @@ -import time as ttime +import time +import numpy as np from bec_lib import bec_logger -import numpy as np from ophyd import Signal, Kind from ophyd.utils import ReadOnlyError logger = bec_logger.logger -# Readout precision for Setable/Readonly/ComputedReadonly signals +# Readout precision for Setable/ReadOnlySignal signals PRECISION = 3 class SetableSignal(Signal): """Setable signal for simulated devices. - It will return the value of the readback signal based on the position - created in the sim_state dictionary of the parent device. + The signal will store the value in sim_state of the SimulatedData class of the parent device. + It will also return the value from sim_state when get is called. Compared to the ReadOnlySignal, + this signal can be written to. + + >>> signal = SetableSignal(name="signal", parent=parent, value=0) + + Parameters + ---------- + + name (string) : Name of the signal + parent (object) : Parent object of the signal, default none. + value (any) : Initial value of the signal, default 0. + kind (int) : Kind of the signal, default Kind.normal. + precision (float) : Precision of the signal, default PRECISION. """ def __init__( self, - *args, name: str, - value: any = None, + *args, + value: any = 0, kind: int = Kind.normal, precision: float = PRECISION, **kwargs, @@ -34,7 +46,6 @@ class SetableSignal(Signal): ) self._value = value self.precision = precision - # Init the sim_state, if self.parent.sim available, use it, else use self.parent self.sim = getattr(self.parent, "sim", self.parent) self._update_sim_state(value) @@ -50,6 +61,7 @@ class SetableSignal(Signal): """Update the timestamp of the readback value.""" return self.sim.sim_state[self.name]["timestamp"] + # pylint: disable=arguments-differ def get(self): """Get the current position of the simulated device. @@ -58,6 +70,7 @@ class SetableSignal(Signal): self._value = self._get_value() return self._value + # pylint: disable=arguments-differ def put(self, value): """Put the value to the simulated device. @@ -83,111 +96,55 @@ class SetableSignal(Signal): class ReadOnlySignal(Signal): - """Readonly signal for simulated devices. + """Computed readback signal for simulated devices. - If initiated without a value, it will set the initial value to 0. + The readback will be computed from a function hosted in the SimulatedData class from the parent device + if compute_readback is True. Else, it will return the value stored int sim.sim_state directly. + + >>> signal = ComputedReadOnlySignal(name="signal", parent=parent, value=0, compute_readback=True) + + Parameters + ---------- + + name (string) : Name of the signal + parent (object) : Parent object of the signal, default none. + value (any) : Initial value of the signal, default 0. + kind (int) : Kind of the signal, default Kind.normal. + precision (float) : Precision of the signal, default PRECISION. + compute_readback (bool) : Flag whether to compute readback based on function hosted in SimulatedData + class. If False, sim_state value will be returned, if True, new value will be computed """ def __init__( self, - *args, name: str, + *args, + parent=None, value: any = 0, kind: int = Kind.normal, precision: float = PRECISION, + compute_readback: bool = False, **kwargs, ): - super().__init__(*args, name=name, value=value, kind=kind, **kwargs) + super().__init__(*args, name=name, parent=parent, value=value, kind=kind, **kwargs) self._metadata.update( connected=True, write_access=False, ) - self.precision = precision self._value = value - # Init the sim_state, if self.parent.sim available, use it, else use self.parent + self.precision = precision + self.compute_readback = compute_readback self.sim = getattr(self.parent, "sim", None) - self._init_sim_state() + if self.sim: + self._init_sim_state() def _init_sim_state(self) -> None: - """Init the readback value and timestamp in sim_state""" - if self.sim: - self.sim.update_sim_state(self.name, self._value) - - def _get_value(self) -> any: - """Get the value of the readback from sim_state.""" - if self.sim: - return self.sim.sim_state[self.name]["value"] - else: - return np.random.rand() - - def _get_timestamp(self) -> any: - """Get the timestamp of the readback from sim_state.""" - if self.sim: - return self.sim.sim_state[self.name]["timestamp"] - else: - return ttime.time() - - def get(self) -> any: - """Get the current position of the simulated device. - - Core function for signal. - """ - self._value = self._get_value() - return self._value - - def put(self, value) -> None: - """Put method, should raise ReadOnlyError since the signal is readonly.""" - raise ReadOnlyError(f"The signal {self.name} is readonly.") - - def describe(self): - """Describe the readback signal. - - Core function for signal. - """ - res = super().describe() - if self.precision is not None: - res[self.name]["precision"] = self.precision - return res - - @property - def timestamp(self): - """Timestamp of the readback value""" - return self._get_timestamp() - - -class ComputedReadOnlySignal(Signal): - """Computed readback signal for simulated devices. - - It will return the value computed from the sim_state of the signal. - This can be configured in parent.sim. - """ - - def __init__( - self, - *args, - name: str, - value: any = None, - kind: int = Kind.normal, - precision: float = PRECISION, - **kwargs, - ): - super().__init__(*args, name=name, value=value, kind=kind, **kwargs) - self._metadata.update( - connected=True, - write_access=False, - ) - self._value = value - self.precision = precision - # Init the sim_state, if self.parent.sim available, use it, else use self.parent - self.sim = getattr(self.parent, "sim", self.parent) - self._update_sim_state() + """Create the initial sim_state in the SimulatedData class of the parent device.""" + self.sim.update_sim_state(self.name, self._value) def _update_sim_state(self) -> None: - """Update the readback value. - - Call _compute_sim_state in parent device which updates the sim_state. - """ - self.sim._compute_sim_state(self.name) + """Update the readback value.""" + self.sim.compute_sim_state(signal_name=self.name, compute_readback=self.compute_readback) def _get_value(self) -> any: """Update the timestamp of the readback value.""" @@ -197,15 +154,16 @@ class ComputedReadOnlySignal(Signal): """Update the timestamp of the readback value.""" return self.sim.sim_state[self.name]["timestamp"] + # pylint: disable=arguments-differ def get(self): - """Get the current position of the simulated device. - - Core function for signal. - """ - self._update_sim_state() - self._value = self._get_value() - return self._value + """Get the current position of the simulated device.""" + if self.sim: + self._update_sim_state() + self._value = self._get_value() + return self._value + return np.random.rand() + # pylint: disable=arguments-differ def put(self, value) -> None: """Put method, should raise ReadOnlyError since the signal is readonly.""" raise ReadOnlyError(f"The signal {self.name} is readonly.") @@ -223,15 +181,6 @@ class ComputedReadOnlySignal(Signal): @property def timestamp(self): """Timestamp of the readback value""" - return self._get_timestamp() - - -if __name__ == "__main__": - from ophyd_devices.sim import SimPositioner - - positioner = SimPositioner(name="positioner", parent=None) - print(positioner.velocity.get()) - positioner.velocity.put(10) - print(positioner.velocity.get()) - positioner.velocity.put(1) - print(positioner.velocity.get()) + if self.sim: + return self._get_timestamp() + return time.time() diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index 2030b66..3f1b1ea 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -14,6 +14,7 @@ class DummyControllerDevice(Device): class DummyController: USER_ACCESS = [ "some_var", + "some_var_property", "controller_show_all", "_func_with_args", "_func_with_args_and_kwargs", @@ -23,11 +24,19 @@ class DummyController: some_var = 10 another_var = 20 + def __init__(self) -> None: + self._some_var_property = None + self.connected = False + + @property + def some_var_property(self): + return self._some_var_property + def on(self): - self._connected = True + self.connected = True def off(self): - self._connected = False + self.connected = False def _func_with_args(self, *args): return args diff --git a/ophyd_devices/sim/test.py b/ophyd_devices/sim/test.py new file mode 100644 index 0000000..7ffdb0d --- /dev/null +++ b/ophyd_devices/sim/test.py @@ -0,0 +1,50 @@ +import lmfit +import inspect + + +class LmfitModelMixin: + + # def __init__(self): + # self.model = lmfit.models.GaussianModel() + # self.params = self.model.make_params() + # self.params["center"].set(value=0) + # self.params["amplitude"].set(value=1) + # self.params["sigma"].set(value=1) + + @staticmethod + def available_models() -> dict: + """ + Get available models from lmfit.models. + + Exclude Gaussian2dModel, ExpressionModel, Model, SplineModel. + """ + avail_models = {} + for name, model_cls in inspect.getmembers(lmfit.models): + try: + is_model = issubclass(model_cls, lmfit.model.Model) + except TypeError: + is_model = False + if is_model and name not in [ + "Gaussian2dModel", + "ExpressionModel", + "Model", + "SplineModel", + ]: + avail_models[name] = model_cls + return avail_models + + def create_properties(self): + """ + Create properties for model parameters. + """ + for name in self.available_models(): + setattr(self, name, param) + + @staticmethod + def get_model(model: str) -> lmfit.Model: + """Get model for given string.""" + if isinstance(model, str): + model = getattr(lmfit.models, model, None) + if not model: + raise ValueError(f"Model {model} not found.") + return model diff --git a/tests/test_simulation.py b/tests/test_simulation.py index d0a7712..c046f23 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1,8 +1,14 @@ from ophyd_devices.utils.bec_device_base import BECDeviceBase, BECDevice +from ophyd import Device, Signal + def test_BECDeviceBase(): # Test the BECDeviceBase class - test = BECDeviceBase(name="test") - assert isinstance(test, BECDevice) - assert test.connected is True + bec_device_base = BECDeviceBase(name="test") + assert isinstance(bec_device_base, BECDevice) + assert bec_device_base.connected is True + signal = Signal(name="signal") + assert isinstance(signal, BECDevice) + device = Device(name="device") + assert isinstance(device, BECDevice) From 00f1898a354cd2f557854b02223df11a18f4dde5 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 22 Feb 2024 17:37:33 +0100 Subject: [PATCH 41/77] refactor: bugfix in camera data, model constant --- ophyd_devices/sim/sim_data.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 4443515..ad96d17 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -424,6 +424,12 @@ class SimulatedDataMonitor(SimulatedDataBase): self.update_sim_state(signal_name, value) def _compute(self, *args, **kwargs) -> float: + """ + Compute the return value for given motor position and active model. + + Returns: + float: Value computed by the active model. + """ mot_name = self.sim_params["ref_motor"] if self.device_manager and mot_name in self.device_manager.devices: motor_pos = self.device_manager.devices[mot_name].obj.read()[mot_name]["value"] @@ -561,8 +567,14 @@ class SimulatedDataCamera(SimulatedDataBase): """Compute a return value for SimulationType2D constant.""" try: shape = self.parent.image_shape.get() - v = self._model_params.get("amplitude") * np.ones(shape, dtype=np.uint16) - return self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"]) + v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.float64) + v = self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"]) + return self._add_hot_pixel( + v, + coords=self.sim_params["hot_pixel_coords"], + hot_pixel_types=self.sim_params["hot_pixel_types"], + values=self.sim_params["hot_pixel_values"], + ) except SimulatedDataException as exc: raise SimulatedDataException( f"Could not compute constant for {self.parent.name} with {exc} raised. Deactivate eiger to continue." From db00803f539791ceefd5f4f0424b00c0e2ae91e6 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 22 Feb 2024 19:56:13 +0100 Subject: [PATCH 42/77] fix: extend bec_device with root, parent, kind --- ophyd_devices/utils/bec_device_base.py | 45 +++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py index c1eda27..8539795 100644 --- a/ophyd_devices/utils/bec_device_base.py +++ b/ophyd_devices/utils/bec_device_base.py @@ -1,4 +1,5 @@ from typing import Protocol, runtime_checkable +from ophyd import Kind @runtime_checkable @@ -8,6 +9,22 @@ class BECDevice(Protocol): name: str _destroyed: bool + @property + def kind(self) -> int: + """kind property""" + + @kind.setter + def kind(self, value: int): + """kind setter""" + + @property + def parent(self): + """Property to find the parent device""" + + @property + def root(self): + """Property to fint the root device""" + @property def hints(self) -> dict: """hints property""" @@ -76,10 +93,36 @@ class BECDeviceBase: """ - def __init__(self, name: str): + def __init__(self, name: str, *args, parent=None, kind=None, **kwargs): self.name = name self._connected = True self._destroyed = False + self._parent = parent + self._kind = kind if kind else Kind.normal + + @property + def kind(self) -> int: + """Kind property, stems from ophyd.""" + return self._kind + + @kind.setter + def kind(self, value: int): + """kind setter""" + self._kind = value + + @property + def parent(self): + """Property to find the parent device""" + return self._parent + + @property + def root(self): + """Property to fint the root device""" + root = self + while True: + if root.parent is None: + return root + root = root.parent @property def hints(self) -> dict: From 8b04b5c84eb4e42c8a0ec7e28727ff907a584a4f Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 23 Feb 2024 10:59:02 +0100 Subject: [PATCH 43/77] refactor: fix Kind import in bec_device_base --- ophyd_devices/utils/bec_device_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ophyd_devices/utils/bec_device_base.py b/ophyd_devices/utils/bec_device_base.py index 8539795..e03f677 100644 --- a/ophyd_devices/utils/bec_device_base.py +++ b/ophyd_devices/utils/bec_device_base.py @@ -10,11 +10,11 @@ class BECDevice(Protocol): _destroyed: bool @property - def kind(self) -> int: + def kind(self) -> Kind: """kind property""" @kind.setter - def kind(self, value: int): + def kind(self, value: Kind): """kind setter""" @property @@ -101,12 +101,12 @@ class BECDeviceBase: self._kind = kind if kind else Kind.normal @property - def kind(self) -> int: + def kind(self) -> Kind: """Kind property, stems from ophyd.""" return self._kind @kind.setter - def kind(self, value: int): + def kind(self, value: Kind): """kind setter""" self._kind = value From 12fda019dffd5f62899803a695b196b3d5706d1b Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 23 Feb 2024 16:12:57 +0000 Subject: [PATCH 44/77] 0.24.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e84b53..c129814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ +## v0.24.0 (2024-02-23) + +### Feature + +* Add lmfit for SimMonitor, refactored sim_data with baseclass, introduce slitproxy ([`800c22e`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/800c22e9592e288f8fe8dea2fb572b81742c6841)) + +### Fix + +* Extend bec_device with root, parent, kind ([`db00803`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/db00803f539791ceefd5f4f0424b00c0e2ae91e6)) + +### Documentation + +* Added doc strings ([`2da6379`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/2da6379e8eb346d856a68a8e5bc678dfff5b1600)) + ## v0.23.1 (2024-02-21) ### Fix diff --git a/setup.py b/setup.py index 68e2049..dbb46fe 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.23.1" +__version__ = "0.24.0" if __name__ == "__main__": setup( From f1e9d1ceaa00d38f38bccd7516913bd57e10bf49 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 17 Feb 2024 17:56:44 +0100 Subject: [PATCH 45/77] Revert "Revert "fix(deprecation): remove all remaining .dumps(), .loads() and producer->connector"" This reverts commit b12292246fd9d8204c76cd1f4927da5a1b981857 --- ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py | 7 ++----- ophyd_devices/epics/devices/eiger9m_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/falcon_csaxs.py | 10 +++++----- ophyd_devices/epics/devices/mcs_csaxs.py | 4 ++-- ophyd_devices/epics/devices/pilatus_csaxs.py | 10 +++++----- .../epics/devices/psi_delay_generator_base.py | 2 +- .../epics/devices/psi_detector_base.py | 2 +- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 6 +++--- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 16 ++++++++-------- ophyd_devices/rt_lamni/rt_ophyd.py | 8 ++++---- ophyd_devices/sim/sim.py | 18 ++++++++---------- ophyd_devices/sim/sim_test_devices.py | 18 ++++++++---------- ophyd_devices/sim/sim_xtreme.py | 4 ++-- ophyd_devices/utils/bec_scaninfo_mixin.py | 4 ++-- ophyd_devices/utils/bec_utils.py | 16 ++++++++-------- tests/test_eiger9m_csaxs.py | 14 +++++++------- tests/test_falcon_csaxs.py | 12 ++++++------ tests/test_mcs_card.py | 8 ++++---- tests/test_pilatus_csaxs.py | 12 ++++++------ tests/utils.py | 6 +++--- 20 files changed, 90 insertions(+), 97 deletions(-) diff --git a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py index 256b124..582e88e 100644 --- a/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py +++ b/ophyd_devices/eiger1p5m_csaxs/eiger1p5m.py @@ -81,8 +81,7 @@ class Eiger1p5MDetector(Device): self.username = "e20588" # TODO get from config def _get_current_scan_msg(self) -> messages.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return messages.ScanStatusMessage.loads(msg) + return self.device_manager.connector.get(MessageEndpoints.scan_status()) def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None): if leading_zeros is None: @@ -159,9 +158,7 @@ class Eiger1p5MDetector(Device): self.detector_control.put("stop") signals = {"config": self.read(), "data": self.file_name} msg = messages.DeviceMessage(signals=signals, metadata=self.metadata) - self.device_manager.producer.set_and_publish( - MessageEndpoints.device_read(self.name), msg.dumps() - ) + self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg) self._stopped = False return super().unstage() diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 219453f..9873326 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -301,20 +301,20 @@ class Eiger9MSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index fea5173..09d61ff 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -244,20 +244,20 @@ class FalconSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage(file_path=self.parent.filepath, done=done) else: msg = messages.FileMessage( file_path=self.parent.filepath, done=done, successful=successful ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/mcs_csaxs.py b/ophyd_devices/epics/devices/mcs_csaxs.py index f87b809..5158b78 100644 --- a/ophyd_devices/epics/devices/mcs_csaxs.py +++ b/ophyd_devices/epics/devices/mcs_csaxs.py @@ -136,8 +136,8 @@ class MCSSetup(CustomDetectorMixin): msg = messages.DeviceMessage( signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata, - ).dumps() - self.parent.producer.xadd( + ) + self.parent.connector.xadd( topic=MessageEndpoints.device_async_readback( scanID=self.parent.scaninfo.scanID, device=self.parent.name ), diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index ee13b98..390db83 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -331,7 +331,7 @@ class PilatusSetup(CustomDetectorMixin): done (bool): True if scan is finished successful (bool): True if scan was successful """ - pipe = self.parent.producer.pipeline() + pipe = self.parent.connector.pipeline() if successful is None: msg = messages.FileMessage( file_path=self.parent.filepath, @@ -345,13 +345,13 @@ class PilatusSetup(CustomDetectorMixin): successful=successful, metadata={"input_path": self.parent.filepath_raw}, ) - self.parent.producer.set_and_publish( + self.parent.connector.set_and_publish( MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), + msg, pipe=pipe, ) - self.parent.producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + self.parent.connector.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe ) pipe.execute() diff --git a/ophyd_devices/epics/devices/psi_delay_generator_base.py b/ophyd_devices/epics/devices/psi_delay_generator_base.py index 76641c6..5068343 100644 --- a/ophyd_devices/epics/devices/psi_delay_generator_base.py +++ b/ophyd_devices/epics/devices/psi_delay_generator_base.py @@ -391,7 +391,7 @@ class PSIDelayGeneratorBase(Device): self.device_manager = device_manager else: self.device_manager = bec_utils.DMMock() - self.producer = self.device_manager.producer + self.connector = self.device_manager.connector self._update_scaninfo() self._init() diff --git a/ophyd_devices/epics/devices/psi_detector_base.py b/ophyd_devices/epics/devices/psi_detector_base.py index 176f82c..36450d2 100644 --- a/ophyd_devices/epics/devices/psi_detector_base.py +++ b/ophyd_devices/epics/devices/psi_detector_base.py @@ -228,7 +228,7 @@ class PSIDetectorBase(Device): self.device_manager = bec_utils.DMMock() base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/" self.service_cfg = {"base_path": os.path.expanduser(base_path)} - self.producer = self.device_manager.producer + self.connector = self.device_manager.connector self._update_scaninfo() self._update_filewriter() self._init() diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index ded8dfd..bbb9c08 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -425,7 +425,7 @@ class RtFlomniController(RtController): # if not (mode==2 or mode==3): # error - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata @@ -460,7 +460,7 @@ class RtFlomniController(RtController): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata @@ -474,7 +474,7 @@ class RtFlomniController(RtController): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().producer.send( + self.get_device_manager().connector.send( MessageEndpoints.device_read("rt_flomni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 6a67912..5786ef5 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -308,9 +308,9 @@ class RtLamniController(Controller): def _update_flyer_device_info(self): flyer_info = self._get_flyer_device_info() - self.get_device_manager().producer.set( + self.get_device_manager().connector.set( MessageEndpoints.device_info("rt_scan"), - messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), + messages.DeviceInfoMessage(device="rt_scan", info=flyer_info), ) def _get_flyer_device_info(self) -> dict: @@ -385,11 +385,11 @@ class RtLamniController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata - ).dumps(), + ), ) # while scan is running while mode > 0: @@ -420,11 +420,11 @@ class RtLamniController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata - ).dumps(), + ), ) logger.info( @@ -432,11 +432,11 @@ class RtLamniController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().producer.send( + self.get_device_manager().connector.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} - ).dumps(), + ), ) def feedback_status_angle_lamni(self) -> bool: diff --git a/ophyd_devices/rt_lamni/rt_ophyd.py b/ophyd_devices/rt_lamni/rt_ophyd.py index 3c70b33..cf12a1f 100644 --- a/ophyd_devices/rt_lamni/rt_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_ophyd.py @@ -278,7 +278,7 @@ class RtController(Controller): def _update_flyer_device_info(self): flyer_info = self._get_flyer_device_info() - self.get_device_manager().producer.set( + self.get_device_manager().connector.set( MessageEndpoints.device_info("rt_scan"), messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(), ) @@ -355,7 +355,7 @@ class RtController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata @@ -390,7 +390,7 @@ class RtController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().producer.set_and_publish( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata @@ -402,7 +402,7 @@ class RtController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().producer.send( + self.get_device_manager().connector.send( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index fd64603..458923c 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -527,34 +527,32 @@ class SimFlyer(Device, PositionerBase, FlyerInterface): } }, 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() + device.device_manager.connector.send( + MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.producer.set_and_publish( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.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( + device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ).dumps(), + ), ) print("done") diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index 3f1b1ea..c2b095c 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -124,34 +124,32 @@ class SynFlyerLamNI(Device, PositionerBase): } }, 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() + device.device_manager.connector.send( + MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.producer.set_and_publish( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.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( + device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) + device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata}, - ).dumps(), + ), ) print("done") diff --git a/ophyd_devices/sim/sim_xtreme.py b/ophyd_devices/sim/sim_xtreme.py index fea60b5..fcd80b5 100644 --- a/ophyd_devices/sim/sim_xtreme.py +++ b/ophyd_devices/sim/sim_xtreme.py @@ -357,8 +357,8 @@ class SynXtremeOtfReplay(FlyerInterface, Device): } msg = messages.DeviceMessage( signals=signals, metadata=self._device_manager.devices.otf.metadata - ).dumps() - self._device_manager.producer.set_and_publish( + ) + self._device_manager.connector.set_and_publish( MessageEndpoints.device_readback("signals"), msg ) diff --git a/ophyd_devices/utils/bec_scaninfo_mixin.py b/ophyd_devices/utils/bec_scaninfo_mixin.py index 417259b..f005927 100644 --- a/ophyd_devices/utils/bec_scaninfo_mixin.py +++ b/ophyd_devices/utils/bec_scaninfo_mixin.py @@ -89,7 +89,7 @@ class BecScaninfoMixin: messages.ScanStatusMessage: messages.ScanStatusMessage object """ if not self.sim_mode: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) + msg = self.device_manager.connector.get(MessageEndpoints.scan_status()) if not isinstance(msg, messages.ScanStatusMessage): return None return msg @@ -105,7 +105,7 @@ class BecScaninfoMixin: if self.sim_mode: return getpass.getuser() - msg = self.device_manager.producer.get(MessageEndpoints.account()) + msg = self.device_manager.connector.get(MessageEndpoints.account()) if msg: return msg return getpass.getuser() diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index b711328..ae39478 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -50,7 +50,7 @@ class DeviceMock: return self -class ProducerMock: +class ConnectorMock: def __init__(self, store_data=True) -> None: self.message_sent = [] self._get_buffer = {} @@ -120,17 +120,17 @@ class ProducerMock: class PipelineMock: _pipe_buffer = [] - _producer = None + _connector = None - def __init__(self, producer) -> None: - self._producer = producer + def __init__(self, connector) -> None: + self._connector = connector def execute(self): - if not self._producer.store_data: + if not self._connector.store_data: self._pipe_buffer = [] return [] res = [ - getattr(self._producer, method)(*args, **kwargs) + getattr(self._connector, method)(*args, **kwargs) for method, args, kwargs in self._pipe_buffer ] self._pipe_buffer = [] @@ -140,13 +140,13 @@ class PipelineMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a producer. + The mocked DeviceManager creates a device containert and a connector. """ def __init__(self): self.devices = DeviceContainer() - self.producer = ProducerMock() + self.connector = ConnectorMock() def add_device(self, name: str, value: float = 0.0): self.devices[name] = DeviceMock(name, value) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 00ffd80..cf25ec3 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -50,7 +50,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -428,24 +428,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls def test_stop(mock_det): diff --git a/tests/test_falcon_csaxs.py b/tests/test_falcon_csaxs.py index 2622a68..b07915e 100644 --- a/tests/test_falcon_csaxs.py +++ b/tests/test_falcon_csaxs.py @@ -27,7 +27,7 @@ def mock_det(): prefix = "X12SA-SITORO:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -215,24 +215,24 @@ def test_publish_file_location(mock_det, scaninfo): done=scaninfo["done"], successful=scaninfo["successful"] ) if scaninfo["successful"] is None: - msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/test_mcs_card.py b/tests/test_mcs_card.py index ac62bc6..841dac6 100644 --- a/tests/test_mcs_card.py +++ b/tests/test_mcs_card.py @@ -32,7 +32,7 @@ def mock_det(): prefix = "X12SA-MCS:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( @@ -53,7 +53,7 @@ def test_init(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -184,7 +184,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): mock_det.custom_prepare._send_data_to_bec() device_metadata = mock_det.scaninfo.scan_msg.metadata metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()}) - data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata).dumps() + data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata) calls = mock.call( topic=MessageEndpoints.device_async_readback( scanID=metadata["scanID"], device=mock_det.name @@ -193,7 +193,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data): expire=1800, ) - assert mock_det.producer.xadd.call_args == calls + assert mock_det.connector.xadd.call_args == calls @pytest.mark.parametrize( diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 81c4629..642e0e0 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -28,7 +28,7 @@ def mock_det(): prefix = "X12SA-ES-PILATUS300K:" sim_mode = False dm = DMMock() - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "connector"): with mock.patch( "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ), mock.patch( @@ -207,27 +207,27 @@ def test_publish_file_location(mock_det, scaninfo): file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, - ).dumps() + ) else: msg = messages.FileMessage( file_path=scaninfo["filepath"], done=scaninfo["done"], metadata={"input_path": scaninfo["filepath_raw"]}, successful=scaninfo["successful"], - ).dumps() + ) expected_calls = [ mock.call( MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), mock.call( MessageEndpoints.file_event(mock_det.name), msg, - pipe=mock_det.producer.pipeline.return_value, + pipe=mock_det.connector.pipeline.return_value, ), ] - assert mock_det.producer.set_and_publish.call_args_list == expected_calls + assert mock_det.connector.set_and_publish.call_args_list == expected_calls @pytest.mark.parametrize( diff --git a/tests/utils.py b/tests/utils.py index f06eea8..d392f50 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from unittest import mock from bec_lib.devicemanager import DeviceContainer -from bec_lib.tests.utils import ProducerMock +from bec_lib.tests.utils import ConnectorMock class SocketMock: @@ -296,13 +296,13 @@ class DeviceMock: class DMMock: """Mock for DeviceManager - The mocked DeviceManager creates a device containert and a producer. + The mocked DeviceManager creates a device containert and a connector. """ def __init__(self): self.devices = DeviceContainer() - self.producer = ProducerMock() + self.connector = ConnectorMock() def add_device(self, name: str, value: float = 0.0): """Add device to the DeviceManagerMock""" From dc9634b73988b5c3cd430008eac5c94319b33ae1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 26 Feb 2024 11:29:00 +0100 Subject: [PATCH 46/77] fix: SimCamera return uint16, SimMonitor uint32 --- ophyd_devices/sim/sim.py | 6 ++++-- ophyd_devices/sim/sim_data.py | 28 ++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 458923c..3ca85fd 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -58,8 +58,9 @@ class SimMonitor(Device): USER_ACCESS = ["sim", "registered_proxies"] sim_cls = SimulatedDataMonitor + BIT_DEPTH = np.uint32 - readback = Cpt(ReadOnlySignal, value=0, kind=Kind.hinted, compute_readback=True) + readback = Cpt(ReadOnlySignal, value=BIT_DEPTH(0), kind=Kind.hinted, compute_readback=True) SUB_READBACK = "readback" _default_sub = SUB_READBACK @@ -113,6 +114,7 @@ class SimCamera(Device): sim_cls = SimulatedDataCamera SHAPE = (100, 100) + BIT_DEPTH = np.uint16 SUB_MONITOR = "monitor" _default_sub = SUB_MONITOR @@ -127,7 +129,7 @@ class SimCamera(Device): image = Cpt( ReadOnlySignal, name="image", - value=np.empty(SHAPE, dtype=np.uint16), + value=np.empty(SHAPE, dtype=BIT_DEPTH), compute_readback=True, kind=Kind.omitted, ) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index ad96d17..db54bab 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -329,6 +329,7 @@ class SimulatedDataMonitor(SimulatedDataBase): def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None: self._model_lookup = self.init_lmfit_models() super().__init__(*args, parent=parent, device_manager=device_manager, **kwargs) + self.bit_depth = self.parent.BIT_DEPTH self._init_default() def _get_additional_params(self) -> None: @@ -421,9 +422,9 @@ class SimulatedDataMonitor(SimulatedDataBase): if compute_readback: method = self._compute value = self.execute_simulation_method(method=method, signal_name=signal_name) - self.update_sim_state(signal_name, value) + self.update_sim_state(signal_name, self.bit_depth(value)) - def _compute(self, *args, **kwargs) -> float: + def _compute(self, *args, **kwargs) -> int: """ Compute the return value for given motor position and active model. @@ -436,25 +437,24 @@ class SimulatedDataMonitor(SimulatedDataBase): else: motor_pos = 0 method = self._model - value = float(method.eval(params=self._model_params, x=motor_pos)) + value = int(method.eval(params=self._model_params, x=motor_pos)) return self._add_noise(value, self.sim_params["noise"], self.sim_params["noise_multiplier"]) - def _add_noise(self, v: float, noise: NoiseType, noise_multiplier: float) -> float: + def _add_noise(self, v: int, noise: NoiseType, noise_multiplier: float) -> int: """ Add the currently activated noise to the simulated data. If NoiseType.NONE is active, the value will be returned Args: - v (float): Value to add noise to. + v (int): Value to add noise to. Returns: - float: Value with added noise. + int: Value with added noise. """ if noise == NoiseType.POISSON: - ceiled_v = np.ceil(v) - v = np.random.poisson(ceiled_v, 1)[0] if ceiled_v > 0 else ceiled_v + v = np.random.poisson(v) return v elif noise == NoiseType.UNIFORM: - v += np.random.uniform(-1, 1) * noise_multiplier + v += np.round(np.random.uniform(-1, 1) * noise_multiplier).astype(int) return v return v @@ -467,6 +467,7 @@ class SimulatedDataCamera(SimulatedDataBase): self._all_default_model_params = defaultdict(dict) self._init_default_camera_params() super().__init__(*args, parent=parent, device_manager=device_manager, **kwargs) + self.bit_depth = self.parent.BIT_DEPTH self._init_default() def _init_default(self) -> None: @@ -547,6 +548,7 @@ class SimulatedDataCamera(SimulatedDataBase): ) else: value = self._compute_empty_image() + value = value.astype(self.bit_depth) self.update_sim_state(signal_name, value) def _compute_empty_image(self) -> np.ndarray: @@ -567,7 +569,7 @@ class SimulatedDataCamera(SimulatedDataBase): """Compute a return value for SimulationType2D constant.""" try: shape = self.parent.image_shape.get() - v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.float64) + v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.uint16) v = self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"]) return self._add_hot_pixel( v, @@ -682,10 +684,12 @@ class SimulatedDataCamera(SimulatedDataBase): noise (NoiseType): Type of noise to add. """ if noise == NoiseType.POISSON: - v = np.random.poisson(np.round(v), v.shape) + v = np.random.poisson(np.round(v), v.shape).astype("uint16") return v if noise == NoiseType.UNIFORM: - v += np.random.uniform(-noise_multiplier, noise_multiplier, v.shape) + v += np.round(np.random.uniform(-noise_multiplier, noise_multiplier, v.shape)).astype( + "uint16" + ) return v if noise == NoiseType.NONE: return v From 961041e07299b1c811e9f0aaf6f7540bca688fd9 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 26 Feb 2024 13:59:49 +0100 Subject: [PATCH 47/77] refactor: cleanup --- ophyd_devices/sim/sim_data.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index db54bab..3911ce2 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -422,7 +422,8 @@ class SimulatedDataMonitor(SimulatedDataBase): if compute_readback: method = self._compute value = self.execute_simulation_method(method=method, signal_name=signal_name) - self.update_sim_state(signal_name, self.bit_depth(value)) + value = self.bit_depth(value) + self.update_sim_state(signal_name, value) def _compute(self, *args, **kwargs) -> int: """ @@ -454,7 +455,7 @@ class SimulatedDataMonitor(SimulatedDataBase): v = np.random.poisson(v) return v elif noise == NoiseType.UNIFORM: - v += np.round(np.random.uniform(-1, 1) * noise_multiplier).astype(int) + v += np.round(np.random.uniform(-1, 1) * noise_multiplier).astype(self.bit_depth) return v return v @@ -684,12 +685,10 @@ class SimulatedDataCamera(SimulatedDataBase): noise (NoiseType): Type of noise to add. """ if noise == NoiseType.POISSON: - v = np.random.poisson(np.round(v), v.shape).astype("uint16") + v = np.random.poisson(np.round(v), v.shape) return v if noise == NoiseType.UNIFORM: - v += np.round(np.random.uniform(-noise_multiplier, noise_multiplier, v.shape)).astype( - "uint16" - ) + v += np.random.uniform(-noise_multiplier, noise_multiplier, v.shape) return v if noise == NoiseType.NONE: return v From 6eca704adcdc7955d88a60ffc7075d32afba43c9 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 26 Feb 2024 14:31:33 +0100 Subject: [PATCH 48/77] refactor: cleanup and exclude ComplexConstantModel --- ophyd_devices/sim/sim_data.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 3911ce2..1a56678 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -401,6 +401,7 @@ class SimulatedDataMonitor(SimulatedDataBase): except TypeError: is_model = False if is_model and name not in [ + "ComplexConstantModel", "Gaussian2dModel", "ExpressionModel", "Model", @@ -455,7 +456,8 @@ class SimulatedDataMonitor(SimulatedDataBase): v = np.random.poisson(v) return v elif noise == NoiseType.UNIFORM: - v += np.round(np.random.uniform(-1, 1) * noise_multiplier).astype(self.bit_depth) + noise = np.ceil(np.random.uniform(0, 1) * noise_multiplier).astype(int) + v += noise * (np.random.randint(0, 2) * 2 - 1) return v return v @@ -549,7 +551,7 @@ class SimulatedDataCamera(SimulatedDataBase): ) else: value = self._compute_empty_image() - value = value.astype(self.bit_depth) + value = self.bit_depth(value) self.update_sim_state(signal_name, value) def _compute_empty_image(self) -> np.ndarray: @@ -570,7 +572,7 @@ class SimulatedDataCamera(SimulatedDataBase): """Compute a return value for SimulationType2D constant.""" try: shape = self.parent.image_shape.get() - v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.uint16) + v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.float32) v = self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"]) return self._add_hot_pixel( v, From a9fcdf64eb5acc02b66fd6c3eb474edb7ff601a5 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 26 Feb 2024 17:56:03 +0000 Subject: [PATCH 49/77] 0.24.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c129814..865439a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.24.1 (2024-02-26) + +### Fix + +* SimCamera return uint16, SimMonitor uint32 ([`dc9634b`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/dc9634b73988b5c3cd430008eac5c94319b33ae1)) + ## v0.24.0 (2024-02-23) ### Feature diff --git a/setup.py b/setup.py index dbb46fe..a28243f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.24.0" +__version__ = "0.24.1" if __name__ == "__main__": setup( From 5ca681212f7b8f2225236adcb0da67f29e20b4d5 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 26 Feb 2024 19:33:13 +0100 Subject: [PATCH 50/77] test: add tests for sim --- tests/test_simulation.py | 93 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index c046f23..02b383d 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1,8 +1,99 @@ -from ophyd_devices.utils.bec_device_base import BECDeviceBase, BECDevice +from unittest import mock +import pytest +import numpy as np +from ophyd_devices.utils.bec_device_base import BECDeviceBase, BECDevice +from ophyd_devices.sim.sim import SimMonitor, SimCamera, SimPositioner + +from tests.utils import DMMock from ophyd import Device, Signal +@pytest.fixture(scope="function") +def monitor(name="monitor"): + """Fixture for SimMonitor.""" + dm = DMMock() + mon = SimMonitor(name=name, device_manager=dm) + yield mon + + +@pytest.fixture(scope="function") +def camera(name="camera"): + """Fixture for SimCamera.""" + dm = DMMock() + cam = SimCamera(name=name, device_manager=dm) + yield cam + + +@pytest.fixture(scope="function") +def positioner(name="positioner"): + """Fixture for SimPositioner.""" + dm = DMMock() + pos = SimPositioner(name=name, device_manager=dm) + yield pos + + +def test_monitor__init__(monitor): + """Test the __init__ method of SimMonitor.""" + assert isinstance(monitor, SimMonitor) + assert isinstance(monitor, BECDevice) + + +def test_monitor_readback(monitor): + """Test the readback method of SimMonitor.""" + for model_name in monitor.sim.sim_get_models(): + monitor.sim.sim_select_model(model_name) + assert isinstance(monitor.read()[monitor.name]["value"], monitor.BIT_DEPTH) + + +def test_camera__init__(camera): + """Test the __init__ method of SimMonitor.""" + assert isinstance(camera, SimCamera) + assert isinstance(camera, BECDevice) + + +def test_camera_readback(camera): + """Test the readback method of SimMonitor.""" + for model_name in camera.sim.sim_get_models(): + camera.sim.sim_select_model(model_name) + assert camera.image.read()[camera.image.name]["value"].shape == camera.SHAPE + assert isinstance(camera.image.read()[camera.image.name]["value"][0, 0], camera.BIT_DEPTH) + + +def test_positioner__init__(positioner): + """Test the __init__ method of SimPositioner.""" + assert isinstance(positioner, SimPositioner) + assert isinstance(positioner, BECDevice) + + +def test_positioner_move(positioner): + """Test the move method of SimPositioner.""" + positioner.move(0).wait() + assert np.isclose(positioner.read()[positioner.name]["value"], 0, atol=positioner.tolerance) + positioner.move(10).wait() + assert np.isclose(positioner.read()[positioner.name]["value"], 10, atol=positioner.tolerance) + + +@pytest.mark.parametrize("proxy_active", [True, False]) +def test_sim_camera_proxies(camera, proxy_active): + """Test mocking compute_method with framework class""" + camera.device_manager.add_device("test_proxy") + if proxy_active: + camera._registered_proxies["test_proxy"] = camera.image.name + else: + camera._registered_proxies = {} + proxy = camera.device_manager.devices["test_proxy"] + mock_method = mock.MagicMock() + mock_obj = proxy.obj + mock_obj.lookup = mock.MagicMock() + mock_obj.lookup.return_value = {camera.name: {"method": mock_method, "args": 1, "kwargs": 1}} + camera.image.read() + if proxy_active: + assert len(mock_obj.lookup.mock_calls) > 0 + elif not proxy_active: + assert len(mock_obj.lookup.mock_calls) == 0 + + def test_BECDeviceBase(): # Test the BECDeviceBase class bec_device_base = BECDeviceBase(name="test") From 91e587b09271a436e7405c44dda60ea4b536865b Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 29 Feb 2024 14:12:42 +0100 Subject: [PATCH 51/77] fix: sim_monitor negative readback fixed --- ophyd_devices/sim/sim_data.py | 4 ++-- tests/test_simulation.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 1a56678..8db2e72 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -453,12 +453,12 @@ class SimulatedDataMonitor(SimulatedDataBase): int: Value with added noise. """ if noise == NoiseType.POISSON: - v = np.random.poisson(v) + v = np.ceil(np.random.poisson(v)).astype(int) return v elif noise == NoiseType.UNIFORM: noise = np.ceil(np.random.uniform(0, 1) * noise_multiplier).astype(int) v += noise * (np.random.randint(0, 2) * 2 - 1) - return v + return v if v > 0 else 0 return v diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 02b383d..004551e 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -39,11 +39,27 @@ def test_monitor__init__(monitor): assert isinstance(monitor, BECDevice) -def test_monitor_readback(monitor): +@pytest.mark.parametrize("center", [-10, 0, 10]) +def test_monitor_readback(monitor, center): """Test the readback method of SimMonitor.""" for model_name in monitor.sim.sim_get_models(): monitor.sim.sim_select_model(model_name) + monitor.sim.sim_params["noise_multipler"] = 10 + if "c" in monitor.sim.sim_params: + monitor.sim.sim_params["c"] = center + elif "center" in monitor.sim.sim_params: + monitor.sim.sim_params["center"] = center assert isinstance(monitor.read()[monitor.name]["value"], monitor.BIT_DEPTH) + expected_value = monitor.sim._model.eval(monitor.sim._model_params, x=0) + print(expected_value, monitor.read()[monitor.name]["value"]) + tolerance = ( + monitor.sim.sim_params["noise_multipler"] + 1 + ) # due to ceiling in calculation, but maximum +1int + assert np.isclose( + monitor.read()[monitor.name]["value"], + expected_value, + atol=monitor.sim.sim_params["noise_multipler"] + 1, + ) def test_camera__init__(camera): From 7ce9bcf73263ddbb3e17ef3842095c3f14f4c266 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 1 Mar 2024 06:17:15 +0000 Subject: [PATCH 52/77] 0.24.2 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 865439a..0d04089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.24.2 (2024-03-01) + +### Fix + +* Sim_monitor negative readback fixed ([`91e587b`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/91e587b09271a436e7405c44dda60ea4b536865b)) + ## v0.24.1 (2024-02-26) ### Fix diff --git a/setup.py b/setup.py index a28243f..a35c661 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.24.1" +__version__ = "0.24.2" if __name__ == "__main__": setup( From 5496b59ae2254495845a0fae2754cdd935b4fb7b Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Mar 2024 13:03:37 +0100 Subject: [PATCH 53/77] feat: add proxy for h5 image replay for SimCamera --- ophyd_devices/__init__.py | 2 +- ophyd_devices/sim/sim_frameworks.py | 276 +++++++++++++++++++++++----- ophyd_devices/sim/sim_signals.py | 148 ++++++++++++++- 3 files changed, 379 insertions(+), 47 deletions(-) diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index 9b3073f..8205c20 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -16,7 +16,7 @@ from .sim.sim import SimPositioner from .sim.sim import SimPositioner as SynAxisOPAAS from .sim.sim import SynDeviceOPAAS from .sim.sim_signals import ReadOnlySignal -from .sim.sim_frameworks import DeviceProxy, SlitProxy +from .sim.sim_frameworks import DeviceProxy, SlitProxy, H5ImageReplayProxy from .sim.sim_signals import ReadOnlySignal as SynSignalRO from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor diff --git a/ophyd_devices/sim/sim_frameworks.py b/ophyd_devices/sim/sim_frameworks.py index fe13cfb..45eeb08 100644 --- a/ophyd_devices/sim/sim_frameworks.py +++ b/ophyd_devices/sim/sim_frameworks.py @@ -1,13 +1,82 @@ import numpy as np from scipy.ndimage import gaussian_filter +from abc import ABC, abstractmethod + +import h5py +import hdf5plugin + +from ophyd import Staged, Kind from collections import defaultdict from ophyd_devices.sim.sim_data import NoiseType +from ophyd_devices.sim.sim_signals import CustomSetableSignal from ophyd_devices.utils.bec_device_base import BECDeviceBase -class DeviceProxy(BECDeviceBase): - """DeviceProxy class inherits from BECDeviceBase.""" +class DeviceProxy(BECDeviceBase, ABC): + """DeviceProxy class inherits from BECDeviceBase. + + It is an abstract class that is meant to be used as a base class for all device proxies. + The minimum requirement for a device proxy is to implement the _compute method. + """ + + def __init__( + self, + name, + *args, + device_manager=None, + **kwargs, + ): + self.name = name + self.device_manager = device_manager + self.config = None + self._lookup = defaultdict(dict) + super().__init__(name, *args, device_manager=device_manager, **kwargs) + self._signals = dict() + + @property + def lookup(self): + """lookup property""" + return self._lookup + + @lookup.setter + def lookup(self, update: dict) -> None: + """lookup setter""" + self._lookup.update(update) + + def _update_device_config(self, config: dict) -> None: + """ + BEC will call this method on every object upon initializing devices to pass over the deviceConfig + from the config file. It can be conveniently be used to hand over initial parameters to the device. + + Args: + config (dict): Config dictionary. + """ + self.config = config + self._compile_lookup() + + def _compile_lookup(self): + """Compile the lookup table for the device.""" + for device_name in self.config.keys(): + self._lookup[device_name] = { + "method": self._compute, + "signal_name": self.config[device_name]["signal_name"], + "args": (device_name,), + "kwargs": {}, + } + + @abstractmethod + def _compute(self, device_name: str, *args, **kwargs) -> any: + """ + The purpose of this method is to compute the readback value for the signal of the device + that this proxy is attached to. This method is meant to be overriden by the user. + P + + Args: + device_name (str): Name of the device. + + Returns: + """ class SlitProxy(DeviceProxy): @@ -26,7 +95,7 @@ class SlitProxy(DeviceProxy): `dev.eiger.get_device_config()` or update it `dev.eiger.get_device_config({'eiger' : {'pixel_size': 0.1}})` slit_sim: - readoutPriority: on_request + readoutPriority: baseline deviceClass: SlitProxy deviceConfig: eiger: @@ -50,48 +119,13 @@ class SlitProxy(DeviceProxy): device_manager=None, **kwargs, ): - self.name = name - self.device_manager = device_manager - self.config = None - self._lookup = defaultdict(dict) self._gaussian_blur_sigma = 5 - super().__init__(name, *args, **kwargs) + super().__init__(name, *args, device_manager=device_manager, **kwargs) def help(self) -> None: """Print documentation for the SlitLookup device.""" print(self.__doc__) - def _update_device_config(self, config: dict) -> None: - """ - BEC will call this method on every object upon initializing devices to pass over the deviceConfig - from the config file. It can be conveniently be used to hand over initial parameters to the device. - - Args: - config (dict): Config dictionary. - """ - self.config = config - self._compile_lookup() - - @property - def lookup(self): - """lookup property""" - return self._lookup - - @lookup.setter - def lookup(self, update: dict) -> None: - """lookup setter""" - self._lookup.update(update) - - def _compile_lookup(self): - """Compile the lookup table for the simulated camera.""" - for device_name in self.config.keys(): - self._lookup[device_name] = { - "method": self._compute, - "signal_name": self.config[device_name]["signal_name"], - "args": (device_name,), - "kwargs": {}, - } - def _compute(self, device_name: str, *args, **kwargs) -> np.ndarray: """ Compute the lookup table for the simulated camera. @@ -173,8 +207,166 @@ class SlitProxy(DeviceProxy): return np.prod(mask, axis=2) +class H5ImageReplayProxy(DeviceProxy): + """This Proxy clas can be used to reply images from an h5 file. + + If the requested images is larger than the available iamges, the images will be replayed from the beginning. + + h5_image_sim: + readoutPriority: baseline + deviceClass: H5ImageReplayProxy + deviceConfig: + eiger: + signal_name: image + file_source: /path/to/h5file.h5 + h5_entry: /entry/data + enabled: true + readOnly: false + """ + + USER_ACCESS = ["file_source", "h5_entry"] + + def __init__( + self, + name, + *args, + device_manager=None, + **kwargs, + ): + self.h5_file = None + self.h5_dataset = None + self._number_of_images = None + self.mode = "r" + self._staged = Staged.no + self._image = None + self._index = 0 + super().__init__(name, *args, device_manager=device_manager, **kwargs) + self.file_source = CustomSetableSignal( + name="file_source", value="", parent=self, kind=Kind.normal + ) + self.h5_entry = CustomSetableSignal( + name="h5_entry", value="", parent=self, kind=Kind.normal + ) + + @property + def component_names(self) -> list[str]: + """Return the names of the components.""" + return ["file_source", "h5_entry"] + + def _update_device_config(self, config: dict) -> None: + super()._update_device_config(config) + if len(config.keys()) > 1: + raise RuntimeError( + f"The current implementation of device {self.name} can only data for a single device. The config hosts multiple keys {config.keys()}" + ) + self._init_signals() + + def _init_signals(self): + """Initialize the signals for the device.""" + if "file_source" in self.config[list(self.config.keys())[0]]: + self.file_source.set(self.config[list(self.config.keys())[0]]["file_source"]) + if "h5_entry" in self.config[list(self.config.keys())[0]]: + self.h5_entry.set(self.config[list(self.config.keys())[0]]["h5_entry"]) + + def _open_h5_file(self) -> None: + """Open an HDF5 fiel and return a reference to the dataset without loading its content. + + Args: + fname (str): File name. + enty (str): Entry name. + mode (str): Mode of the file, default "r". + + Returns: + h5py.Dataset: Reference to the dataset. + """ + self.h5_file = h5py.File(self.file_source.get(), mode=self.mode) + self.h5_dataset = self.h5_file[self.h5_entry.get()] + self._number_of_images = self.h5_dataset.shape[0] + + def _close_h5_file(self) -> None: + """Close the HDF5 file.""" + self.h5_file.close() + + def stop(self) -> None: + """Stop the device.""" + if self.h5_file: + self._close_h5_file() + self.h5_file = None + self.h5_dataset = None + self._number_of_images = None + self._index = 0 + + def stage(self) -> list[object]: + """Stage the device. + This opens the HDF5 file, unstaging will close it. + """ + + if self._staged != Staged.no: + return [self] + try: + self._open_h5_file() + except Exception as exc: + if self.h5_file: + self.stop() + raise FileNotFoundError( + f"Could not open h5file {self.file_source.get()} or access data set {self.h5_dataset.get()} in file" + ) from exc + + self._staged = Staged.yes + return [self] + + def unstage(self) -> list[object]: + """Unstage the device.""" + if self.h5_file: + self.stop() + self._staged = Staged.no + return [self] + + def _load_image(self): + """Get the image from the h5 file. + + Args: + index (int): Index of the image. + + Returns: + np.ndarray: Image. + """ + if self.h5_file: + slice_nr = self._index % self._number_of_images + self._index = self._index + 1 + self._image = self.h5_dataset[slice_nr, ...] + return + try: + self.stage() + slice_nr = self._index % self._number_of_images + self._index = self._index + 1 + self._image = self.h5_dataset[slice_nr, ...] + self.unstage() + except Exception as exc: + raise FileNotFoundError( + f"Could not open h5file {self.file_source.get()} or access data set {self.h5_dataset.get()} in file" + ) from exc + + def _compute(self, device_name: str, *args, **kwargs) -> np.ndarray: + """Compute the image. + + Returns: + np.ndarray: Image. + """ + self._load_image() + return self._image + + if __name__ == "__main__": # Example usage - pinhole = SlitProxy(name="pinhole", device_manager=None) - pinhole.describe() - print(pinhole) + tmp = H5ImageReplayProxy(name="tmp", device_manager=None) + config = { + "eiger": { + "signal_name": "image", + "file_source": "/Users/appel_c/switchdrive/Sharefolder/AgBH_2D_gridscan/projection_000006_data_000001.h5", + "h5_entry": "/entry/data/data", + } + } + tmp._update_device_config(config) + tmp.stage() + print(tmp) diff --git a/ophyd_devices/sim/sim_signals.py b/ophyd_devices/sim/sim_signals.py index 51c6004..40c864a 100644 --- a/ophyd_devices/sim/sim_signals.py +++ b/ophyd_devices/sim/sim_signals.py @@ -5,6 +5,8 @@ from bec_lib import bec_logger from ophyd import Signal, Kind from ophyd.utils import ReadOnlyError +from ophyd_devices.utils.bec_device_base import BECDeviceBase + logger = bec_logger.logger # Readout precision for Setable/ReadOnlySignal signals @@ -17,6 +19,8 @@ class SetableSignal(Signal): The signal will store the value in sim_state of the SimulatedData class of the parent device. It will also return the value from sim_state when get is called. Compared to the ReadOnlySignal, this signal can be written to. + The setable signal inherits from the Signal class of ophyd, thus the class attribute needs to be + initiated as a Component (class from ophyd). >>> signal = SetableSignal(name="signal", parent=parent, value=0) @@ -46,20 +50,25 @@ class SetableSignal(Signal): ) self._value = value self.precision = precision - self.sim = getattr(self.parent, "sim", self.parent) + self.sim = getattr(self.parent, "sim", None) self._update_sim_state(value) def _update_sim_state(self, value: any) -> None: """Update the readback value.""" - self.sim.update_sim_state(self.name, value) + if self.sim: + self.sim.update_sim_state(self.name, value) def _get_value(self) -> any: """Update the timestamp of the readback value.""" - return self.sim.sim_state[self.name]["value"] + if self.sim: + return self.sim.sim_state[self.name]["value"] + return self._value def _get_timestamp(self) -> any: """Update the timestamp of the readback value.""" - return self.sim.sim_state[self.name]["timestamp"] + if self.sim: + return self.sim.sim_state[self.name]["timestamp"] + return time.time() # pylint: disable=arguments-differ def get(self): @@ -100,6 +109,8 @@ class ReadOnlySignal(Signal): The readback will be computed from a function hosted in the SimulatedData class from the parent device if compute_readback is True. Else, it will return the value stored int sim.sim_state directly. + The readonly signal inherits from the Signal class of ophyd, thus the class attribute needs to be + initiated as a Component (class from ophyd). >>> signal = ComputedReadOnlySignal(name="signal", parent=parent, value=0, compute_readback=True) @@ -184,3 +195,132 @@ class ReadOnlySignal(Signal): if self.sim: return self._get_timestamp() return time.time() + + +class CustomSetableSignal(BECDeviceBase): + """Custom signal for simulated devices. The custom signal can be read-only, setable or computed. + In comparison to above, this signal is not a class from ophyd, but an own implementation of a signal. + + It works in the same fashion as the SetableSignal and ReadOnlySignal, however, it is + not needed to initiate it as a Component (ophyd) within the parent device class. + + >>> signal = SetableSignal(name="signal", parent=parent, value=0) + + Parameters + ---------- + + name (string) : Name of the signal + parent (object) : Parent object of the signal, default none. + value (any) : Initial value of the signal, default 0. + kind (int) : Kind of the signal, default Kind.normal. + precision (float) : Precision of the signal, default PRECISION. + """ + + USER_ACCESS = ["put", "get", "set"] + + def __init__( + self, + name: str, + *args, + parent=None, + value: any = 0, + kind: int = Kind.normal, + precision: float = PRECISION, + **kwargs, + ): + if parent: + name = f"{parent.name}_{name}" + super().__init__(*args, name=name, parent=parent, kind=kind, **kwargs) + self._metadata = {"connected": self.connected, "write_access": True} + self._value = value + self._timestamp = time.time() + self._dtype = type(value) + self._shape = self._get_shape(value) + self.precision = precision + self.sim = getattr(self.parent, "sim", None) + self._update_sim_state(value) + + def _get_shape(self, value: any) -> list: + """Get the shape of the value. + **Note: This logic is from ophyd, and replicated here. + There would be more sophisticated ways, but to keep it consistent, it is replicated here.** + """ + if isinstance(value, np.ndarray): + return list(value.shape) + if isinstance(value, list): + return len(value) + return [] + + def _update_sim_state(self, value: any) -> None: + """Update the readback value.""" + if self.sim: + self.sim.update_sim_state(self.name, value) + + def _get_value(self) -> any: + """Update the timestamp of the readback value.""" + if self.sim: + return self.sim.sim_state[self.name]["value"] + return self._value + + def _get_timestamp(self) -> any: + """Update the timestamp of the readback value.""" + if self.sim: + return self.sim.sim_state[self.name]["timestamp"] + return self._timestamp + + # pylint: disable=arguments-differ + def get(self): + """Get the current position of the simulated device. + + Core function for signal. + """ + self._value = self._get_value() + return self._value + + # pylint: disable=arguments-differ + def put(self, value): + """Put the value to the simulated device. + + Core function for signal. + """ + self._update_sim_state(value) + self._value = value + self._timestamp = time.time() + + def describe(self): + """Describe the readback signal. + + Core function for signal. + """ + res = { + self.name: { + "source": str(self.__class__), + "dtype": self._dtype, + "shape": self._shape, + } + } + if self.precision is not None: + res[self.name]["precision"] = self.precision + return res + + def set(self, value): + """Set method""" + self.put(value) + + @property + def timestamp(self): + """Timestamp of the readback value""" + return self._get_timestamp() + + def read(self): + """Read method""" + return { + self.name: { + "value": self.get(), + "timestamp": self.timestamp, + } + } + + def read_configuration(self): + """Read method""" + return self.read() From f4141f0dbf8d98f1d1591c66ccd147099019afc7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Mar 2024 13:03:56 +0100 Subject: [PATCH 54/77] fix: fix bug in computation of negative data within SimMonitor --- ophyd_devices/sim/sim_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 8db2e72..857225b 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -423,7 +423,7 @@ class SimulatedDataMonitor(SimulatedDataBase): if compute_readback: method = self._compute value = self.execute_simulation_method(method=method, signal_name=signal_name) - value = self.bit_depth(value) + value = self.bit_depth(np.max(value, 0)) self.update_sim_state(signal_name, value) def _compute(self, *args, **kwargs) -> int: @@ -453,12 +453,12 @@ class SimulatedDataMonitor(SimulatedDataBase): int: Value with added noise. """ if noise == NoiseType.POISSON: - v = np.ceil(np.random.poisson(v)).astype(int) + v = np.random.poisson(v) return v elif noise == NoiseType.UNIFORM: - noise = np.ceil(np.random.uniform(0, 1) * noise_multiplier).astype(int) - v += noise * (np.random.randint(0, 2) * 2 - 1) - return v if v > 0 else 0 + noise = np.random.uniform(-1, 1) * noise_multiplier + v += noise + return v return v From eb4e10e86bba9b55623d089572f104d21d96601e Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Mar 2024 13:05:23 +0100 Subject: [PATCH 55/77] fix: add dependency for env --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index a35c661..ed87ba9 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,8 @@ if __name__ == "__main__": "std_daq_client", "pyepics", "pytest", + "h5py", + "hdf5plugin", ], extras_require={"dev": ["pytest", "pytest-random-order", "black", "coverage"]}, package_data={"ophyd_devices.smaract": ["smaract_sensors.json"]}, From 9a154f01e48e33c0e28921b8a5c59af4d4585aeb Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Mar 2024 13:13:03 +0100 Subject: [PATCH 56/77] refactor: small fix to int return --- ophyd_devices/sim/sim_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 857225b..c2584e5 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -458,7 +458,7 @@ class SimulatedDataMonitor(SimulatedDataBase): elif noise == NoiseType.UNIFORM: noise = np.random.uniform(-1, 1) * noise_multiplier v += noise - return v + return v if v > 0 else v return v From aff4cb227cd2bb857c3e0903e3c3e2710bd05ab7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 1 Mar 2024 13:15:14 +0100 Subject: [PATCH 57/77] refactor: fix _add_noise --- ophyd_devices/sim/sim_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index c2584e5..932d2ca 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -456,9 +456,9 @@ class SimulatedDataMonitor(SimulatedDataBase): v = np.random.poisson(v) return v elif noise == NoiseType.UNIFORM: - noise = np.random.uniform(-1, 1) * noise_multiplier - v += noise - return v if v > 0 else v + noise = np.ceil(np.random.uniform(0, 1) * noise_multiplier).astype(int) + v += noise * (np.random.randint(0, 2) * 2 - 1) + return v if v > 0 else 0 return v From e5fada8e9dc43fab6781624388d966743ebc1356 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 4 Mar 2024 07:39:28 +0100 Subject: [PATCH 58/77] refactor: fix docstrings --- ophyd_devices/sim/sim_frameworks.py | 33 +++++++---------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/ophyd_devices/sim/sim_frameworks.py b/ophyd_devices/sim/sim_frameworks.py index 45eeb08..5e2422d 100644 --- a/ophyd_devices/sim/sim_frameworks.py +++ b/ophyd_devices/sim/sim_frameworks.py @@ -208,9 +208,9 @@ class SlitProxy(DeviceProxy): class H5ImageReplayProxy(DeviceProxy): - """This Proxy clas can be used to reply images from an h5 file. + """This Proxy class can be used to replay images from an h5 file. - If the requested images is larger than the available iamges, the images will be replayed from the beginning. + If the number of requested images is larger than the number of available iamges, the images will be replayed from the beginning. h5_image_sim: readoutPriority: baseline @@ -236,7 +236,6 @@ class H5ImageReplayProxy(DeviceProxy): self.h5_file = None self.h5_dataset = None self._number_of_images = None - self.mode = "r" self._staged = Staged.no self._image = None self._index = 0 @@ -257,7 +256,7 @@ class H5ImageReplayProxy(DeviceProxy): super()._update_device_config(config) if len(config.keys()) > 1: raise RuntimeError( - f"The current implementation of device {self.name} can only data for a single device. The config hosts multiple keys {config.keys()}" + f"The current implementation of device {self.name} can only replay data for a single device. The config has information about multiple devices {config.keys()}" ) self._init_signals() @@ -269,17 +268,8 @@ class H5ImageReplayProxy(DeviceProxy): self.h5_entry.set(self.config[list(self.config.keys())[0]]["h5_entry"]) def _open_h5_file(self) -> None: - """Open an HDF5 fiel and return a reference to the dataset without loading its content. - - Args: - fname (str): File name. - enty (str): Entry name. - mode (str): Mode of the file, default "r". - - Returns: - h5py.Dataset: Reference to the dataset. - """ - self.h5_file = h5py.File(self.file_source.get(), mode=self.mode) + """Opens the HDF5 file found in the file_source signal and the HDF5 dataset specified by the h5_entry signal.""" + self.h5_file = h5py.File(self.file_source.get(), mode="r") self.h5_dataset = self.h5_file[self.h5_entry.get()] self._number_of_images = self.h5_dataset.shape[0] @@ -298,7 +288,7 @@ class H5ImageReplayProxy(DeviceProxy): def stage(self) -> list[object]: """Stage the device. - This opens the HDF5 file, unstaging will close it. + This opens the HDF5 dataset, unstaging will close it. """ if self._staged != Staged.no: @@ -316,21 +306,14 @@ class H5ImageReplayProxy(DeviceProxy): return [self] def unstage(self) -> list[object]: - """Unstage the device.""" + """Unstage the device, also closes the HDF5 dataset""" if self.h5_file: self.stop() self._staged = Staged.no return [self] def _load_image(self): - """Get the image from the h5 file. - - Args: - index (int): Index of the image. - - Returns: - np.ndarray: Image. - """ + """Try loading the image from the h5 dataset, and set it to self._image.""" if self.h5_file: slice_nr = self._index % self._number_of_images self._index = self._index + 1 From 704c89c25f2933ddaf0e0ea68c805dbee7a09ff0 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 4 Mar 2024 10:11:32 +0000 Subject: [PATCH 59/77] 0.25.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d04089..c07c0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ +## v0.25.0 (2024-03-04) + +### Feature + +* Add proxy for h5 image replay for SimCamera ([`5496b59`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/5496b59ae2254495845a0fae2754cdd935b4fb7b)) + +### Fix + +* Add dependency for env ([`eb4e10e`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/eb4e10e86bba9b55623d089572f104d21d96601e)) +* Fix bug in computation of negative data within SimMonitor ([`f4141f0`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/f4141f0dbf8d98f1d1591c66ccd147099019afc7)) + ## v0.24.2 (2024-03-01) ### Fix diff --git a/setup.py b/setup.py index ed87ba9..567323c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.24.2" +__version__ = "0.25.0" if __name__ == "__main__": setup( From afd7912329b14bc916e14fd565ebcf7506ecb045 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 5 Mar 2024 16:08:57 +0100 Subject: [PATCH 60/77] fix: device_read should use set_and_publish --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 2 +- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 2 +- ophyd_devices/rt_lamni/rt_ophyd.py | 2 +- ophyd_devices/sim/sim.py | 41 +++++++---------------- ophyd_devices/sim/sim_test_devices.py | 19 ++++------- 5 files changed, 23 insertions(+), 43 deletions(-) diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index bbb9c08..5430446 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -474,7 +474,7 @@ class RtFlomniController(RtController): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().connector.send( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_read("rt_flomni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 5786ef5..0e0a37d 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -432,7 +432,7 @@ class RtLamniController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().connector.send( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} diff --git a/ophyd_devices/rt_lamni/rt_ophyd.py b/ophyd_devices/rt_lamni/rt_ophyd.py index cf12a1f..cd56309 100644 --- a/ophyd_devices/rt_lamni/rt_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_ophyd.py @@ -402,7 +402,7 @@ class RtController(Controller): ) def publish_device_data(self, signals, pointID): - self.get_device_manager().connector.send( + self.get_device_manager().connector.set_and_publish( MessageEndpoints.device_read("rt_lamni"), messages.DeviceMessage( signals=signals, metadata={"pointID": pointID, **self.readout_metadata} diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 3ca85fd..042eddc 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -1,32 +1,26 @@ import os import threading import time as ttime + import numpy as np - from bec_lib import MessageEndpoints, bec_logger, messages - from ophyd import Component as Cpt +from ophyd import Device, DeviceStatus from ophyd import DynamicDeviceComponent as Dcpt -from ophyd import Device, DeviceStatus, Kind -from ophyd import PositionerBase - +from ophyd import Kind, PositionerBase from ophyd.flyers import FlyerInterface - from ophyd.sim import SynSignal from ophyd.status import StatusBase - from ophyd.utils import LimitError -from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin - from ophyd_devices.sim.sim_data import ( - SimulatedPositioner, SimulatedDataCamera, SimulatedDataMonitor, + SimulatedPositioner, ) +from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal from ophyd_devices.sim.sim_test_devices import DummyController - -from ophyd_devices.sim.sim_signals import SetableSignal, ReadOnlySignal +from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger @@ -135,14 +129,7 @@ class SimCamera(Device): ) def __init__( - self, - name, - *, - kind=None, - parent=None, - sim_init: dict = None, - device_manager=None, - **kwargs, + self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs ): self.device_manager = device_manager self.init_sim_params = sim_init @@ -535,25 +522,23 @@ class SimFlyer(Device, PositionerBase, FlyerInterface): elapsed_time += exp_time if elapsed_time > buffer_time: elapsed_time = 0 - device.device_manager.connector.send( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( - device=device.name, - status=1, - metadata={"pointID": ii, **metadata}, + device=device.name, status=1, metadata={"pointID": ii, **metadata} ), ) - device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) + device.device_manager.connector.set_and_publish( + MessageEndpoints.device_read(device.name), bundle + ) device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( - device=device.name, - status=0, - metadata={"pointID": num_pos, **metadata}, + device=device.name, status=0, metadata={"pointID": num_pos, **metadata} ), ) print("done") diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index c2b095c..b09f754 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -1,10 +1,9 @@ -import time as ttime import threading +import time as ttime + import numpy as np - -from ophyd import OphydObject, Device, PositionerBase - -from bec_lib import messages, MessageEndpoints +from bec_lib import MessageEndpoints, messages +from ophyd import Device, OphydObject, PositionerBase class DummyControllerDevice(Device): @@ -130,25 +129,21 @@ class SynFlyerLamNI(Device, PositionerBase): elapsed_time += exp_time if elapsed_time > buffer_time: elapsed_time = 0 - device.device_manager.connector.send( + device.device_manager.connector.set_and_publish( MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( - device=device.name, - status=1, - metadata={"pointID": ii, **metadata}, + device=device.name, status=1, metadata={"pointID": ii, **metadata} ), ) device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) device.device_manager.connector.set_and_publish( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( - device=device.name, - status=0, - metadata={"pointID": num_pos, **metadata}, + device=device.name, status=0, metadata={"pointID": num_pos, **metadata} ), ) print("done") From 6d179ea8a8e41374cfe2b92939e0b71b7962f7cb Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 5 Mar 2024 16:25:13 +0100 Subject: [PATCH 61/77] fix: device_status should use set --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 4 ++-- ophyd_devices/rt_lamni/rt_lamni_ophyd.py | 4 ++-- ophyd_devices/rt_lamni/rt_ophyd.py | 4 ++-- ophyd_devices/sim/sim.py | 4 ++-- ophyd_devices/sim/sim_test_devices.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index 5430446..abb5b4e 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -425,7 +425,7 @@ class RtFlomniController(RtController): # if not (mode==2 or mode==3): # error - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata @@ -460,7 +460,7 @@ class RtFlomniController(RtController): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 0e0a37d..9390ee1 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -385,7 +385,7 @@ class RtLamniController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata @@ -420,7 +420,7 @@ class RtLamniController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata diff --git a/ophyd_devices/rt_lamni/rt_ophyd.py b/ophyd_devices/rt_lamni/rt_ophyd.py index cd56309..59f7a7a 100644 --- a/ophyd_devices/rt_lamni/rt_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_ophyd.py @@ -355,7 +355,7 @@ class RtController(Controller): # if not (mode==2 or mode==3): # error - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=1, metadata=self.readout_metadata @@ -390,7 +390,7 @@ class RtController(Controller): signals = self._get_signals_from_table(return_table) self.publish_device_data(signals=signals, pointID=int(return_table[0])) - self.get_device_manager().connector.set_and_publish( + self.get_device_manager().connector.set( MessageEndpoints.device_status("rt_scan"), messages.DeviceStatusMessage( device="rt_scan", status=0, metadata=self.readout_metadata diff --git a/ophyd_devices/sim/sim.py b/ophyd_devices/sim/sim.py index 042eddc..4121f9a 100644 --- a/ophyd_devices/sim/sim.py +++ b/ophyd_devices/sim/sim.py @@ -526,7 +526,7 @@ class SimFlyer(Device, PositionerBase, FlyerInterface): MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.connector.set_and_publish( + device.device_manager.connector.set( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=1, metadata={"pointID": ii, **metadata} @@ -535,7 +535,7 @@ class SimFlyer(Device, PositionerBase, FlyerInterface): device.device_manager.connector.set_and_publish( MessageEndpoints.device_read(device.name), bundle ) - device.device_manager.connector.set_and_publish( + device.device_manager.connector.set( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata} diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index b09f754..5291c12 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -133,14 +133,14 @@ class SynFlyerLamNI(Device, PositionerBase): MessageEndpoints.device_read(device.name), bundle ) bundle = messages.BundleMessage() - device.device_manager.connector.set_and_publish( + device.device_manager.connector.set( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=1, metadata={"pointID": ii, **metadata} ), ) device.device_manager.connector.send(MessageEndpoints.device_read(device.name), bundle) - device.device_manager.connector.set_and_publish( + device.device_manager.connector.set( MessageEndpoints.device_status(device.name), messages.DeviceStatusMessage( device=device.name, status=0, metadata={"pointID": num_pos, **metadata} From bc1171c1994282ea2071dba7954b38d71d9784b7 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 5 Mar 2024 15:46:18 +0000 Subject: [PATCH 62/77] 0.25.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c07c0fb..8d9b0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## v0.25.1 (2024-03-05) + +### Fix + +* Device_status should use set ([`6d179ea`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/6d179ea8a8e41374cfe2b92939e0b71b7962f7cb)) +* Device_read should use set_and_publish ([`afd7912`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/afd7912329b14bc916e14fd565ebcf7506ecb045)) + ## v0.25.0 (2024-03-04) ### Feature diff --git a/setup.py b/setup.py index 567323c..c4f8286 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.25.0" +__version__ = "0.25.1" if __name__ == "__main__": setup( From 345a95dbe72c939150272445b07e017f6a203f87 Mon Sep 17 00:00:00 2001 From: Ivan Usov Date: Tue, 27 Feb 2024 15:58:43 +0100 Subject: [PATCH 63/77] cd: drop python/3.9 --- .gitlab-ci.yml | 24 +++++++++--------------- .pylintrc | 2 +- setup.cfg | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 057a9f2..8f1c684 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official language image. Look for the different tagged releases at: # https://hub.docker.com/r/library/python/tags/ -image: $CI_DOCKER_REGISTRY/python:3.9 +image: $CI_DOCKER_REGISTRY/python:3.10 workflow: rules: @@ -87,7 +87,7 @@ pytest: script: - pip install coverage - coverage run --source=./ophyd_devices -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests - - coverage report + - coverage report - coverage xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: @@ -97,22 +97,16 @@ pytest: coverage_format: cobertura path: coverage.xml -tests-3.10: +tests-3.11: stage: AdditionalTests - image: $CI_DOCKER_REGISTRY/python:3.10 + image: $CI_DOCKER_REGISTRY/python:3.11 needs: ["pytest"] script: - pytest -v --random-order ./tests allow_failure: true -tests-3.11: - extends: "tests-3.10" - stage: AdditionalTests - image: $CI_DOCKER_REGISTRY/python:3.11 - allow_failure: true - tests-3.12: - extends: "tests-3.10" + extends: "tests-3.11" stage: AdditionalTests image: $CI_DOCKER_REGISTRY/python:3.12 allow_failure: true @@ -146,10 +140,10 @@ semver: - export REPOSITORY_USERNAME=__token__ - export REPOSITORY_PASSWORD=$CI_PYPI_TOKEN - > - semantic-release publish -v DEBUG - -D version_variable=./setup.py:__version__ - -D hvcs=gitlab - + semantic-release publish -v DEBUG + -D version_variable=./setup.py:__version__ + -D hvcs=gitlab + allow_failure: false rules: - if: '$CI_COMMIT_REF_NAME == "master"' diff --git a/.pylintrc b/.pylintrc index 0561755..4f63d82 100644 --- a/.pylintrc +++ b/.pylintrc @@ -52,7 +52,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.9 +py-version=3.10 # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. diff --git a/setup.cfg b/setup.cfg index 29b76f3..d43162b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = package_dir = = . packages = find: -python_requires = >=3.9 +python_requires = >=3.10 [options.packages.find] where = . From 02f950de8d6b2eba142495f71b2f470133f13187 Mon Sep 17 00:00:00 2001 From: Holler Mirko Date: Tue, 5 Mar 2024 17:04:29 +0100 Subject: [PATCH 64/77] added show all to sample storage --- .../epics/devices/flomni_sample_storage.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ophyd_devices/epics/devices/flomni_sample_storage.py b/ophyd_devices/epics/devices/flomni_sample_storage.py index e9f1b7f..18c48fa 100644 --- a/ophyd_devices/epics/devices/flomni_sample_storage.py +++ b/ophyd_devices/epics/devices/flomni_sample_storage.py @@ -4,6 +4,7 @@ from ophyd import Component as Cpt from ophyd import Device from ophyd import DynamicDeviceComponent as Dcpt from ophyd import EpicsSignal +from prettytable import PrettyTable class FlomniSampleStorageError(Exception): @@ -18,6 +19,7 @@ class FlomniSampleStorage(Device): "unset_sample_slot", "set_sample_in_gripper", "unset_sample_in_gripper", + "show_all", ] SUB_VALUE = "value" _default_sub = SUB_VALUE @@ -80,3 +82,36 @@ class FlomniSampleStorage(Device): def is_sample_in_gripper(self) -> bool: val = self.sample_in_gripper.get() return bool(val) + + def get_sample_name(self, slot_nr) -> str: + val = getattr(self.sample_names, f"sample{slot_nr}").get() + return str(val) + + def show_all(self): + t = PrettyTable() + t.title = "flOMNI sample storage" + field_names = [""] + field_names.extend(str(ax) for ax in range(1,11)) + for ct in range(0,2): + t.field_names = field_names + row = ["Container " + str(ct)] + row.extend( + "used" if self.is_sample_slot_used(slot_nr) else "free" + for slot_nr in range((ct*10)+1,(ct*10)+11) + ) + t.add_row(row) + print(t) + print("\n\nFollowing samples are currently loaded:\n") + for ct in range(1,21): + if self.is_sample_slot_used(ct): + print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}") + if self.sample_in_gripper.get(): + print(f"\nGripper: {self.sample_in_gripper_name.get()}\n") + else: + print(f"\nGripper: no sample\n") + + if self.is_sample_slot_used(0): + print(f"flOMNI stage: {self.get_sample_name(0)}\n") + else: + print(f"flOMNI stage: no sample\n") + From 4ff7c4b8d9ed99dd63bdf665b266bcef320f0721 Mon Sep 17 00:00:00 2001 From: Holler Mirko Date: Wed, 6 Mar 2024 15:04:16 +0100 Subject: [PATCH 65/77] fixed formatting --- .../epics/devices/flomni_sample_storage.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ophyd_devices/epics/devices/flomni_sample_storage.py b/ophyd_devices/epics/devices/flomni_sample_storage.py index 18c48fa..bf154fa 100644 --- a/ophyd_devices/epics/devices/flomni_sample_storage.py +++ b/ophyd_devices/epics/devices/flomni_sample_storage.py @@ -82,36 +82,35 @@ class FlomniSampleStorage(Device): def is_sample_in_gripper(self) -> bool: val = self.sample_in_gripper.get() return bool(val) - + def get_sample_name(self, slot_nr) -> str: val = getattr(self.sample_names, f"sample{slot_nr}").get() return str(val) - + def show_all(self): t = PrettyTable() t.title = "flOMNI sample storage" field_names = [""] - field_names.extend(str(ax) for ax in range(1,11)) - for ct in range(0,2): + field_names.extend(str(ax) for ax in range(1, 11)) + for ct in range(0, 2): t.field_names = field_names row = ["Container " + str(ct)] row.extend( - "used" if self.is_sample_slot_used(slot_nr) else "free" - for slot_nr in range((ct*10)+1,(ct*10)+11) - ) - t.add_row(row) + "used" if self.is_sample_slot_used(slot_nr) else "free" + for slot_nr in range((ct * 10) + 1, (ct * 10) + 11) + ) + t.add_row(row) print(t) print("\n\nFollowing samples are currently loaded:\n") - for ct in range(1,21): + for ct in range(1, 21): if self.is_sample_slot_used(ct): print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}") if self.sample_in_gripper.get(): - print(f"\nGripper: {self.sample_in_gripper_name.get()}\n") + print(f"\n Gripper: {self.sample_in_gripper_name.get()}\n") else: - print(f"\nGripper: no sample\n") + print(f"\n Gripper: no sample\n") if self.is_sample_slot_used(0): - print(f"flOMNI stage: {self.get_sample_name(0)}\n") + print(f" flOMNI stage: {self.get_sample_name(0)}\n") else: - print(f"flOMNI stage: no sample\n") - + print(f" flOMNI stage: no sample\n") From a9f58d2b5686370a07766ed72903f82f5e2d9cb1 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 8 Mar 2024 12:11:28 +0100 Subject: [PATCH 66/77] fix(smaract): fixed axes_referenced --- ophyd_devices/smaract/smaract_controller.py | 4 +- tests/test_smaract.py | 96 +++++++++------------ 2 files changed, 44 insertions(+), 56 deletions(-) diff --git a/ophyd_devices/smaract/smaract_controller.py b/ophyd_devices/smaract/smaract_controller.py index 3a65e79..ddf801f 100644 --- a/ophyd_devices/smaract/smaract_controller.py +++ b/ophyd_devices/smaract/smaract_controller.py @@ -188,7 +188,9 @@ class SmaractController(Controller): return bool(int(return_val.split(",")[1])) def all_axes_referenced(self) -> bool: - return all(self.axis_is_referenced(ax) for ax in self._axis if ax is not None) + return all( + self.axis_is_referenced(ax.axis_Id_numeric) for ax in self._axis if ax is not None + ) @retry_once @axis_checked diff --git a/tests/test_smaract.py b/tests/test_smaract.py index 776b5e4..a04a425 100644 --- a/tests/test_smaract.py +++ b/tests/test_smaract.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest from utils import SocketMock @@ -7,6 +9,27 @@ from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, Smar from ophyd_devices.smaract.smaract_ophyd import SmaractMotor +@pytest.fixture +def controller(): + SmaractController._reset_controller() + controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) + controller.on() + controller.sock.flush_buffer() + yield controller + + +@pytest.fixture +def lsmarA(): + SmaractController._reset_controller() + motor_a = SmaractMotor( + "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock + ) + motor_a.controller.on() + motor_a.controller.sock.flush_buffer() + motor_a.stage() + yield motor_a + + @pytest.mark.parametrize( "axis,position,get_message,return_msg", [ @@ -16,11 +39,7 @@ from ophyd_devices.smaract.smaract_ophyd import SmaractMotor (0, -50.23, b":GP0\n", b":P0,-50230000\n"), ], ) -def test_get_position(axis, position, get_message, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_get_position(controller, axis, position, get_message, return_msg): controller.sock.buffer_recv = return_msg val = controller.get_position(axis) assert val == position @@ -36,11 +55,7 @@ def test_get_position(axis, position, get_message, return_msg): (200, False, b":GPPK0\n", b":PPK0,0\n", ValueError), ], ) -def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, exception): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_axis_is_referenced(controller, axis, is_referenced, get_message, return_msg, exception): controller.sock.buffer_recv = return_msg if exception is not None: with pytest.raises(exception): @@ -60,11 +75,7 @@ def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, except (b":E,-1", SmaractCommunicationError, True), ], ) -def test_socket_put_and_receive_raises_exception(return_msg, exception, raised): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_socket_put_and_receive_raises_exception(controller, return_msg, exception, raised): controller.sock.buffer_recv = return_msg with pytest.raises(exception): controller.socket_put_and_receive(b"test", raise_if_not_status=True) @@ -82,11 +93,7 @@ def test_socket_put_and_receive_raises_exception(return_msg, exception, raised): @pytest.mark.parametrize( "mode,get_message,return_msg", [(0, b":GCM\n", b":CM0\n"), (1, b":GCM\n", b":CM1\n")] ) -def test_communication_mode(mode, get_message, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_communication_mode(controller, mode, get_message, return_msg): controller.sock.buffer_recv = return_msg val = controller.get_communication_mode() assert controller.sock.buffer_put[0] == get_message @@ -108,11 +115,7 @@ def test_communication_mode(mode, get_message, return_msg): (0, [b":GS0\n", b":GS0\n"], [b":E0,0\n", b":S0,9"]), ], ) -def test_axis_is_moving(is_moving, get_message, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_axis_is_moving(controller, is_moving, get_message, return_msg): controller.sock.buffer_recv = return_msg val = controller.is_axis_moving(0) assert val == is_moving @@ -129,11 +132,7 @@ def test_axis_is_moving(is_moving, get_message, return_msg): (6, 1, b":GST1\n", b":ST1,6\n"), ], ) -def test_get_sensor_definition(sensor_id, axis, get_msg, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_get_sensor_definition(controller, sensor_id, axis, get_msg, return_msg): controller.sock.buffer_recv = return_msg sensor = controller.get_sensor_type(axis) assert sensor.type_code == sensor_id @@ -147,11 +146,7 @@ def test_get_sensor_definition(sensor_id, axis, get_msg, return_msg): (20.23, 1, b":SCLS1,20230000\n", b":E-1,0"), ], ) -def test_set_move_speed(move_speed, axis, get_msg, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_set_move_speed(controller, move_speed, axis, get_msg, return_msg): controller.sock.buffer_recv = return_msg controller.set_closed_loop_move_speed(axis, move_speed) assert controller.sock.buffer_put[0] == get_msg @@ -165,11 +160,7 @@ def test_set_move_speed(move_speed, axis, get_msg, return_msg): (20.23, 1, None, b":MPA1,20230000,1000\n", b":E0,0"), ], ) -def test_move_axis_to_absolute_position(pos, axis, hold_time, get_msg, return_msg): - SmaractController._reset_controller() - controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123) - controller.on() - controller.sock.flush_buffer() +def test_move_axis_to_absolute_position(controller, pos, axis, hold_time, get_msg, return_msg): controller.sock.buffer_recv = return_msg if hold_time is not None: controller.move_axis_to_absolute_position(axis, pos, hold_time=hold_time) @@ -203,29 +194,24 @@ def test_move_axis_to_absolute_position(pos, axis, hold_time, get_msg, return_ms ), ], ) -def test_move_axis(pos, get_msg, return_msg): - SmaractController._reset_controller() - lsmarA = SmaractMotor( - "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock - ) - lsmarA.controller.on() +def test_move_axis(lsmarA, pos, get_msg, return_msg): controller = lsmarA.controller - controller.sock.flush_buffer() controller.sock.buffer_recv = return_msg lsmarA.move(pos) assert controller.sock.buffer_put == get_msg @pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])]) -def test_stop_axis(num_axes, get_msg, return_msg): - SmaractController._reset_controller() - lsmarA = SmaractMotor( - "A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock - ) - lsmarA.stage() +def test_stop_axis(lsmarA, num_axes, get_msg, return_msg): controller = lsmarA.controller - controller.on() - controller.sock.flush_buffer() controller.sock.buffer_recv = return_msg controller.stop_all_axes() assert controller.sock.buffer_put == get_msg + + +def test_all_axes_referenced(lsmarA): + controller = lsmarA.controller + with mock.patch.object(controller, "axis_is_referenced", return_value=True) as mock_is_ref: + val = controller.all_axes_referenced() + assert val + mock_is_ref.assert_called_once_with(0) From 4fbba7393adbb01ebf80667b205a1dbaab9bb15c Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 8 Mar 2024 12:13:23 +0100 Subject: [PATCH 67/77] fix(smaract): added user access for axis_is_referenced and all_axes_referenced --- ophyd_devices/smaract/smaract_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ophyd_devices/smaract/smaract_controller.py b/ophyd_devices/smaract/smaract_controller.py index ddf801f..02d0b9f 100644 --- a/ophyd_devices/smaract/smaract_controller.py +++ b/ophyd_devices/smaract/smaract_controller.py @@ -78,6 +78,8 @@ class SmaractController(Controller): "move_open_loop_steps", "find_reference_mark", "describe", + "axis_is_referenced", + "all_axes_referenced", ] def __init__( From b0400c999653ccb3ae06f621fb67c7877e813094 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 8 Mar 2024 13:52:33 +0000 Subject: [PATCH 68/77] 0.25.2 Automatically generated by python-semantic-release --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9b0e0..b03b821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## v0.25.2 (2024-03-08) + +### Fix + +* **smaract:** Added user access for axis_is_referenced and all_axes_referenced ([`4fbba73`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/4fbba7393adbb01ebf80667b205a1dbaab9bb15c)) +* **smaract:** Fixed axes_referenced ([`a9f58d2`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/a9f58d2b5686370a07766ed72903f82f5e2d9cb1)) + ## v0.25.1 (2024-03-05) ### Fix diff --git a/setup.py b/setup.py index c4f8286..2af01fd 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.25.1" +__version__ = "0.25.2" if __name__ == "__main__": setup( From 238ecb5ff84b55f028b75df32fccdc3685609d69 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 8 Mar 2024 16:15:13 +0100 Subject: [PATCH 69/77] fix: fix type conversion for SimCamera uniform noise --- ophyd_devices/sim/sim_data.py | 1 + tests/test_simulation.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/sim/sim_data.py b/ophyd_devices/sim/sim_data.py index 932d2ca..187498f 100644 --- a/ophyd_devices/sim/sim_data.py +++ b/ophyd_devices/sim/sim_data.py @@ -691,6 +691,7 @@ class SimulatedDataCamera(SimulatedDataBase): return v if noise == NoiseType.UNIFORM: v += np.random.uniform(-noise_multiplier, noise_multiplier, v.shape) + v[v <= 0] = 0 return v if noise == NoiseType.NONE: return v diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 004551e..407afa4 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -68,12 +68,23 @@ def test_camera__init__(camera): assert isinstance(camera, BECDevice) -def test_camera_readback(camera): +@pytest.mark.parametrize("amplitude, noise_multiplier", [(0, 1), (100, 10), (1000, 50)]) +def test_camera_readback(camera, amplitude, noise_multiplier): """Test the readback method of SimMonitor.""" for model_name in camera.sim.sim_get_models(): camera.sim.sim_select_model(model_name) - assert camera.image.read()[camera.image.name]["value"].shape == camera.SHAPE - assert isinstance(camera.image.read()[camera.image.name]["value"][0, 0], camera.BIT_DEPTH) + camera.sim.sim_params = {"noise_multiplier": noise_multiplier} + camera.sim.sim_params = {"amplitude": amplitude} + camera.sim.sim_params = {"noise": "poisson"} + assert camera.image.get().shape == camera.SHAPE + assert isinstance(camera.image.get()[0, 0], camera.BIT_DEPTH) + camera.sim.sim_params = {"noise": "uniform"} + camera.sim.sim_params = {"hot_pixel_coords": []} + camera.sim.sim_params = {"hot_pixel_values": []} + camera.sim.sim_params = {"hot_pixel_types": []} + assert camera.image.get().shape == camera.SHAPE + assert isinstance(camera.image.get()[0, 0], camera.BIT_DEPTH) + assert (camera.image.get() <= (amplitude + noise_multiplier + 1)).all() def test_positioner__init__(positioner): From 676c63bfb3f2c24a9d1f88cf36889c7c57fe2779 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 8 Mar 2024 15:49:50 +0000 Subject: [PATCH 70/77] 0.25.3 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b03b821..c2cea74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.25.3 (2024-03-08) + +### Fix + +* Fix type conversion for SimCamera uniform noise ([`238ecb5`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/238ecb5ff84b55f028b75df32fccdc3685609d69)) + ## v0.25.2 (2024-03-08) ### Fix diff --git a/setup.py b/setup.py index 2af01fd..f092502 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.25.2" +__version__ = "0.25.3" if __name__ == "__main__": setup( From d9f09b0d866f97a859c9b437474928e7a9e8c1b6 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 8 Mar 2024 13:39:34 +0100 Subject: [PATCH 71/77] feat: added computed signal --- ophyd_devices/__init__.py | 5 +- ophyd_devices/utils/dynamic_pseudo.py | 122 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 ophyd_devices/utils/dynamic_pseudo.py diff --git a/ophyd_devices/__init__.py b/ophyd_devices/__init__.py index 8205c20..4fc1976 100644 --- a/ophyd_devices/__init__.py +++ b/ophyd_devices/__init__.py @@ -7,18 +7,19 @@ from .galil.sgalil_ophyd import SGalilMotor from .npoint.npoint import NPointAxis from .rt_lamni import RtFlomniMotor, RtLamniMotor from .sim.sim import SimCamera -from .sim.sim import SimMonitor from .sim.sim import SimFlyer from .sim.sim import SimFlyer as SynFlyer +from .sim.sim import SimMonitor from .sim.sim import SimMonitor as SynAxisMonitor from .sim.sim import SimMonitor as SynGaussBEC from .sim.sim import SimPositioner from .sim.sim import SimPositioner as SynAxisOPAAS from .sim.sim import SynDeviceOPAAS +from .sim.sim_frameworks import DeviceProxy, H5ImageReplayProxy, SlitProxy from .sim.sim_signals import ReadOnlySignal -from .sim.sim_frameworks import DeviceProxy, SlitProxy, H5ImageReplayProxy from .sim.sim_signals import ReadOnlySignal as SynSignalRO from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages from .smaract.smaract_ophyd import SmaractMotor from .utils.bec_device_base import BECDeviceBase +from .utils.dynamic_pseudo import ComputedSignal from .utils.static_device_test import launch diff --git a/ophyd_devices/utils/dynamic_pseudo.py b/ophyd_devices/utils/dynamic_pseudo.py new file mode 100644 index 0000000..097ce74 --- /dev/null +++ b/ophyd_devices/utils/dynamic_pseudo.py @@ -0,0 +1,122 @@ +""" +This module provides a class for creating a pseudo signal that is computed from other signals. +""" + +from functools import reduce + +from ophyd import SignalRO +from ophyd.ophydobj import Kind + + +def rgetattr(obj, attr, *args): + """See https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects""" + + def _getattr(obj, attr): + return getattr(obj, attr, *args) + + return reduce(_getattr, [obj] + attr.split(".")) + + +class ComputedSignal(SignalRO): + """ + A read-only signal that is computed from other signals. The compute method should be a string + representation of a function that takes the input signals as arguments and returns the computed + value. The input signals should be provided as a list of strings that represent the path to the + signal in the device manager. + """ + + def __init__( + self, + *, + name, + value=0, + timestamp=None, + device_manager=None, + parent=None, + labels=None, + kind=Kind.hinted, + tolerance=None, + rtolerance=None, + metadata=None, + cl=None, + attr_name="" + ): + 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._device_manager = device_manager + self._input_signals = [] + self._signal_subs = [] + self._compute_method = None + + def _signal_callback(self, *args, **kwargs): + self._run_subs(sub_type=self.SUB_VALUE, old_value=None, value=self.get()) + + @property + def compute_method(self): + """ + Set the compute method for the pseudo signal + + Args: + compute_method (str): The compute method to be used. This should be a string + representation of a function that takes the input signals as arguments + and returns the computed value. + + """ + return self._compute_method + + @compute_method.setter + def compute_method(self, method: str): + if not method.startswith("def"): + raise ValueError("The compute method should be a string representation of a function") + + # get the function name + function_name = method.split("(")[0].split(" ")[1] + # pylint: disable=exec-used + exec(method) + self._compute_method = locals()[function_name] + + @property + def input_signals(self): + """ + Set the input signals for the pseudo signal + + Args: + *input_vars: The input signals to be used for the computation + + """ + return self._input_signals + + @input_signals.setter + def input_signals(self, input_vars): + if self._signal_subs: + for signal, sub_id in self._signal_subs: + signal.unsubscribe(sub_id) + signals = [] + for signal in input_vars: + if isinstance(signal, str): + target = signal.replace("_", ".") + parts = target.split(".") + target = ".".join([parts[0], "obj"] + parts[1:]) + obj = rgetattr(self._device_manager.devices, target) + sub_id = obj.subscribe(self._signal_callback) + self._signal_subs.append((obj, sub_id)) + signals.append(obj) + else: + signals.append(signal) + self._input_signals = signals + + def get(self): + if self._compute_method: + return self._compute_method(*self._input_signals) + return None From c76e1a0b4e012271b4e4baaa2ff8210f59a984b9 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 8 Mar 2024 15:29:51 +0100 Subject: [PATCH 72/77] test: added tests for dynamic_pseudo --- ophyd_devices/utils/dynamic_pseudo.py | 8 ++++--- tests/test_dynamic_pseudo.py | 30 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/test_dynamic_pseudo.py diff --git a/ophyd_devices/utils/dynamic_pseudo.py b/ophyd_devices/utils/dynamic_pseudo.py index 097ce74..1dc8839 100644 --- a/ophyd_devices/utils/dynamic_pseudo.py +++ b/ophyd_devices/utils/dynamic_pseudo.py @@ -3,6 +3,7 @@ This module provides a class for creating a pseudo signal that is computed from """ from functools import reduce +from typing import Callable from ophyd import SignalRO from ophyd.ophydobj import Kind @@ -63,7 +64,7 @@ class ComputedSignal(SignalRO): self._run_subs(sub_type=self.SUB_VALUE, old_value=None, value=self.get()) @property - def compute_method(self): + def compute_method(self) -> Callable | None: """ Set the compute method for the pseudo signal @@ -117,6 +118,7 @@ class ComputedSignal(SignalRO): self._input_signals = signals def get(self): - if self._compute_method: - return self._compute_method(*self._input_signals) + if self.compute_method: + # pylint: disable=not-callable + return self.compute_method(*self.input_signals) return None diff --git a/tests/test_dynamic_pseudo.py b/tests/test_dynamic_pseudo.py new file mode 100644 index 0000000..03b2a01 --- /dev/null +++ b/tests/test_dynamic_pseudo.py @@ -0,0 +1,30 @@ +from unittest import mock + +import pytest + +from ophyd_devices.utils.dynamic_pseudo import ComputedSignal +from tests.utils import DMMock + + +@pytest.fixture +def device_manager_with_devices(): + dm = DMMock() + dm.add_device("a") + dm.add_device("b") + device_mock = mock.MagicMock() + device_mock.obj.readback.get.return_value = 20 + dm.devices["a"] = device_mock + dm.devices["b"] = device_mock + + return dm + + +def test_computed_signal(device_manager_with_devices): + signal = ComputedSignal(name="test", device_manager=device_manager_with_devices) + assert signal.get() is None + + signal.compute_method = "def test(a, b): return a.get() + b.get()" + signal.input_signals = ["a_readback", "b_readback"] + assert signal.get() == 40 + + assert callable(signal.compute_method) From c68c3c1b54ecff2c51417168ee3e91b4056831fc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 8 Mar 2024 17:13:15 +0100 Subject: [PATCH 73/77] docs: improved doc strings for computed signal --- ophyd_devices/utils/dynamic_pseudo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ophyd_devices/utils/dynamic_pseudo.py b/ophyd_devices/utils/dynamic_pseudo.py index 1dc8839..d4157d0 100644 --- a/ophyd_devices/utils/dynamic_pseudo.py +++ b/ophyd_devices/utils/dynamic_pseudo.py @@ -73,6 +73,9 @@ class ComputedSignal(SignalRO): representation of a function that takes the input signals as arguments and returns the computed value. + Example: + >>> signal.compute_method = "def test(a, b): return a.get() + b.get()" + """ return self._compute_method @@ -95,6 +98,9 @@ class ComputedSignal(SignalRO): Args: *input_vars: The input signals to be used for the computation + Example: + >>> signal.input_signals = ["samx_readback", "samx_readback"] + """ return self._input_signals From 63de4f5a229ac33a181a49b07f8ca4e6cdf05621 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 8 Mar 2024 16:37:11 +0000 Subject: [PATCH 74/77] 0.26.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2cea74..a1535e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ +## v0.26.0 (2024-03-08) + +### Feature + +* Added computed signal ([`d9f09b0`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/d9f09b0d866f97a859c9b437474928e7a9e8c1b6)) + +### Documentation + +* Improved doc strings for computed signal ([`c68c3c1`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/c68c3c1b54ecff2c51417168ee3e91b4056831fc)) + ## v0.25.3 (2024-03-08) ### Fix diff --git a/setup.py b/setup.py index f092502..de16566 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.25.3" +__version__ = "0.26.0" if __name__ == "__main__": setup( From 33e4458c59cce44e93d9f3bae44ce41028688471 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 9 Mar 2024 15:27:20 +0100 Subject: [PATCH 75/77] fix: fixed dynamic pseudo --- ophyd_devices/utils/dynamic_pseudo.py | 20 +++++++++++++++----- tests/test_dynamic_pseudo.py | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ophyd_devices/utils/dynamic_pseudo.py b/ophyd_devices/utils/dynamic_pseudo.py index d4157d0..2392dbe 100644 --- a/ophyd_devices/utils/dynamic_pseudo.py +++ b/ophyd_devices/utils/dynamic_pseudo.py @@ -5,9 +5,12 @@ This module provides a class for creating a pseudo signal that is computed from from functools import reduce from typing import Callable +from bec_lib import bec_logger from ophyd import SignalRO from ophyd.ophydobj import Kind +logger = bec_logger.logger + def rgetattr(obj, attr, *args): """See https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects""" @@ -40,7 +43,7 @@ class ComputedSignal(SignalRO): rtolerance=None, metadata=None, cl=None, - attr_name="" + attr_name="", ): super().__init__( name=name, @@ -59,6 +62,7 @@ class ComputedSignal(SignalRO): self._input_signals = [] self._signal_subs = [] self._compute_method = None + self._compute_method_str = None def _signal_callback(self, *args, **kwargs): self._run_subs(sub_type=self.SUB_VALUE, old_value=None, value=self.get()) @@ -77,18 +81,22 @@ class ComputedSignal(SignalRO): >>> signal.compute_method = "def test(a, b): return a.get() + b.get()" """ - return self._compute_method + return self._compute_method_str @compute_method.setter def compute_method(self, method: str): + logger.info(f"Updating compute method for {self.name}.") + method = method.strip() if not method.startswith("def"): raise ValueError("The compute method should be a string representation of a function") # get the function name function_name = method.split("(")[0].split(" ")[1] + method = method.replace(function_name, "user_compute_method") + self._compute_method_str = method # pylint: disable=exec-used exec(method) - self._compute_method = locals()[function_name] + self._compute_method = locals()["user_compute_method"] @property def input_signals(self): @@ -124,7 +132,9 @@ class ComputedSignal(SignalRO): self._input_signals = signals def get(self): - if self.compute_method: + if self._compute_method: # pylint: disable=not-callable - return self.compute_method(*self.input_signals) + if self.input_signals: + return self._compute_method(*self.input_signals) + return self._compute_method() return None diff --git a/tests/test_dynamic_pseudo.py b/tests/test_dynamic_pseudo.py index 03b2a01..dab9f6f 100644 --- a/tests/test_dynamic_pseudo.py +++ b/tests/test_dynamic_pseudo.py @@ -27,4 +27,6 @@ def test_computed_signal(device_manager_with_devices): signal.input_signals = ["a_readback", "b_readback"] assert signal.get() == 40 - assert callable(signal.compute_method) + # pylint: disable=protected-access + assert callable(signal._compute_method) + assert signal._compute_method_str == "def user_compute_method(a, b): return a.get() + b.get()" From 999b294657e4ef58f6e9897f5d41c32cd1776c3d Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sun, 10 Mar 2024 09:36:49 +0000 Subject: [PATCH 76/77] 0.26.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1535e3..2e683a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.26.1 (2024-03-10) + +### Fix + +* Fixed dynamic pseudo ([`33e4458`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/33e4458c59cce44e93d9f3bae44ce41028688471)) + ## v0.26.0 (2024-03-08) ### Feature diff --git a/setup.py b/setup.py index de16566..9d24c9c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.26.0" +__version__ = "0.26.1" if __name__ == "__main__": setup( From e5f8a4e772c1ce77e4549b25e1ce097961e1306e Mon Sep 17 00:00:00 2001 From: Holler Mirko Date: Mon, 11 Mar 2024 14:16:07 +0100 Subject: [PATCH 77/77] various_fixes_and_printouts --- ophyd_devices/rt_lamni/rt_flomni_ophyd.py | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py index abb5b4e..de6d936 100644 --- a/ophyd_devices/rt_lamni/rt_flomni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_flomni_ophyd.py @@ -33,12 +33,14 @@ class RtFlomniController(RtController): "feedback_disable", "feedback_enable_without_reset", "feedback_enable_with_reset", + "feedback_is_running", "add_pos_to_scan", "clear_trajectory_generator", "show_cyclic_error_compensation", "laser_tracker_on", "laser_tracker_off", "laser_tracker_show_all", + "show_signal_strength_interferometer", ] def __init__( @@ -103,6 +105,7 @@ class RtFlomniController(RtController): self.move_to_zero() if not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: + print("Please wait, slew rate limiters not on target.") logger.info("Please wait, slew rate limiters not on target.") while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1: time.sleep(0.05) @@ -121,6 +124,9 @@ class RtFlomniController(RtController): fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]") fsamx_in = fsamx.user_parameter.get("in") if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3): + print( + "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." + ) raise RtError( "Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically." ) @@ -135,6 +141,7 @@ class RtFlomniController(RtController): time.sleep(0.4) if not self.feedback_is_running(): + print("Feedback is not running; likely an error in the interferometer.") raise RtError("Feedback is not running; likely an error in the interferometer.") time.sleep(1.5) @@ -148,6 +155,7 @@ class RtFlomniController(RtController): self.set_device_enabled("fsamy", False) self.set_device_enabled("foptx", False) self.set_device_enabled("fopty", False) + print("rt feedback is now enabled.") def move_samx_to_scan_region(self, fovx: float, cenx: float): if self.rt_pid_voltage is None: @@ -196,6 +204,7 @@ class RtFlomniController(RtController): time.sleep(0.01) if not self.feedback_is_running(): + print("Feedback is not running; likely an error in the interferometer.") raise RtError("Feedback is not running; likely an error in the interferometer.") self.set_device_enabled("fsamx", False) @@ -215,6 +224,7 @@ class RtFlomniController(RtController): fsamx = self.get_device_manager().devices.fsamx fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]") + print("rt feedback is now disalbed.") def get_pid_x(self) -> float: voltage = float(self.socket_put_and_receive("g").strip()) @@ -228,10 +238,12 @@ class RtFlomniController(RtController): logger.info("Cyclic Error Compensation: y-axis is initialized") else: logger.info("Cyclic Error Compensation: y-axis is NOT initialized") + print("Cyclic Error Compensation: y-axis is NOT initialized") if cec1 == 32: logger.info("Cyclic Error Compensation: x-axis is initialized") else: - logger.info("Cyclic Error Compensation: y-axis is NOT initialized") + logger.info("Cyclic Error Compensation: x-axis is NOT initialized") + print("Cyclic Error Compensation: x-axis is NOT initialized") def set_rotation_angle(self, val: float) -> None: self.socket_put(f"a{val/180*np.pi}") @@ -241,6 +253,7 @@ class RtFlomniController(RtController): if not self.tracker_info["enabled_z"] or not self.tracker_info["enabled_y"]: logger.info("Enabling the laser tracker. Please wait...") + print("Enabling the laser tracker. Please wait...") tracker_intensity = self.tracker_info["tracker_intensity"] if ( @@ -248,6 +261,7 @@ class RtFlomniController(RtController): or tracker_intensity < self.tracker_info["threshold_intensity_z"] ): logger.info(self.tracker_info) + print("The tracker cannot be enabled because the beam intensity it low.") raise RtError("The tracker cannot be enabled because the beam intensity it low.") self.move_to_zero() @@ -263,9 +277,12 @@ class RtFlomniController(RtController): self.laser_tracker_wait_on_target() logger.info("Laser tracker running!") + print("Laser tracker running!") def laser_tracker_off(self): self.socket_put("T0") + logger.info("Disabled the laser tracker") + print("Disabled the laser tracker") def laser_tracker_show_all(self): self.laser_update_tracker_info() @@ -321,6 +338,7 @@ class RtFlomniController(RtController): time.sleep(0.5) count += 1 if count > max_repeat: + print("Failed to reach laser target position.") raise RtError("Failed to reach laser target position.") def slew_rate_limiters_on_target(self) -> bool: @@ -530,6 +548,10 @@ class RtFlomniSetpointSignal(RtSetpointSignal): """ if not self.parent.controller.feedback_is_running(): + print( + "The interferometer feedback is not running. Either it is turned off or and" + " interferometer error occured." + ) raise RtError( "The interferometer feedback is not running. Either it is turned off or and" " interferometer error occured."