Merging master

This commit is contained in:
mohacsi_i 2024-03-12 10:43:23 +01:00
commit c323354fea
52 changed files with 4749 additions and 1408 deletions

View File

@ -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:
@ -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
@ -121,6 +115,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"'

View File

@ -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.

View File

@ -2,6 +2,137 @@
<!--next-version-placeholder-->
## 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
* 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
* Fix type conversion for SimCamera uniform noise ([`238ecb5`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/238ecb5ff84b55f028b75df32fccdc3685609d69))
## 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
* 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
* 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
* Sim_monitor negative readback fixed ([`91e587b`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/91e587b09271a436e7405c44dda60ea4b536865b))
## 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
* 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
* Replaced outdated enable_set by read_only ([`f91d0c4`](https://gitlab.psi.ch/bec/ophyd_devices/-/commit/f91d0c482d194e5f69c7206d0f6ad0971f84b0e1))
## 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
* 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
* **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
* **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
* 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
* 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

View File

@ -1,16 +1,25 @@
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 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_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.bec_device_base import BECDeviceBase
from .utils.dynamic_pseudo import ComputedSignal
from .utils.static_device_test import launch

View File

@ -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()

View File

@ -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

View File

@ -1,7 +1,20 @@
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 .delay_generator_csaxs import DelayGeneratorcSAXS
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 .psi_detector_base import CustomDetectorMixin, PSIDetectorBase
from .slits import SlitH, SlitV
from .specMotors import (
Bpm4i,
EnergyKev,
@ -16,19 +29,7 @@ from .specMotors import (
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 .aerotech.AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX
# from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin
from .SpmBase import SpmBase
from .XbpmBase import XbpmBase, XbpmCsaxsOp

View File

@ -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()

View File

@ -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()

View File

@ -0,0 +1,116 @@
import time
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):
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",
"show_all",
]
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)
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"\n Gripper: {self.sample_in_gripper_name.get()}\n")
else:
print(f"\n Gripper: 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")

View File

@ -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
),

View File

@ -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()

View File

@ -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
@ -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()

View File

@ -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
@ -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()

View File

@ -1,4 +1,3 @@
import functools
import threading
import time
@ -7,28 +6,42 @@ 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
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",
"lights_off",
"lights_on",
]
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 +53,79 @@ 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
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
@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):
@ -63,11 +142,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced):
class FlomniGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(
GalilReadbackSignal,
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")
@ -114,6 +189,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,
@ -202,18 +278,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")
@ -238,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:
@ -252,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:

View File

@ -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
@ -107,11 +119,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")
@ -157,7 +165,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,
@ -246,18 +254,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")
@ -316,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()

View File

@ -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
@ -363,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
@ -379,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")
@ -514,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")
@ -587,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)

View File

@ -1 +1,2 @@
from .rt_lamni_ophyd import RtLamniMotor, RtLamniController
from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
from .rt_lamni_ophyd import RtLamniController, RtLamniMotor

View File

@ -0,0 +1,768 @@
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
from prettytable import PrettyTable
from ophyd_devices.rt_lamni.rt_ophyd import (
BECConfigError,
RtCommunicationError,
RtController,
RtError,
RtReadbackSignal,
RtSetpointSignal,
RtSignalRO,
retry_once,
)
from ophyd_devices.utils.controller import threadlocked
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
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",
"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__(
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:
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)
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):
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."
)
if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01):
fsamx.read_only = False
fsamx.obj.move(fsamx_in, wait=True)
fsamx.read_only = True
time.sleep(1)
self.socket_put("l1")
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)
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)
print("rt feedback is now enabled.")
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.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.read_only = True
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():
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)
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]")
print("rt feedback is now disalbed.")
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")
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: 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}")
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...")
print("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)
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()
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!")
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()
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):
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()
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:
print("Failed to reach laser target position.")
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().connector.set(
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().connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=0, metadata=self.readout_metadata
).dumps(),
)
logger.info(
"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().connector.set_and_publish(
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():
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."
)
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()

View File

@ -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
@ -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(
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(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=0, metadata=self.readout_metadata
).dumps(),
),
)
logger.info(
@ -432,12 +432,11 @@ class RtLamniController(Controller):
)
def publish_device_data(self, signals, pointID):
self.get_device_manager().producer.send(
self.get_device_manager().connector.set_and_publish(
MessageEndpoints.device_read("rt_lamni"),
messages.DeviceMessage(
signals=signals,
metadata={"pointID": pointID, **self.readout_metadata},
).dumps(),
signals=signals, metadata={"pointID": pointID, **self.readout_metadata}
),
)
def feedback_status_angle_lamni(self) -> bool:
@ -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()

View File

@ -0,0 +1,818 @@
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().connector.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().connector.set(
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().connector.set(
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().connector.set_and_publish(
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].read_only = not 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()

View File

@ -2,9 +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 SlitProxy

View File

@ -1,20 +1,26 @@
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 Device, DeviceStatus, Kind
from ophyd import Device, DeviceStatus
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_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor
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.sim.sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal
from ophyd_devices.sim.sim_data import (
SimulatedDataCamera,
SimulatedDataMonitor,
SimulatedPositioner,
)
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
from ophyd_devices.sim.sim_test_devices import DummyController
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
logger = bec_logger.logger
@ -27,162 +33,82 @@ 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"]
USER_ACCESS = ["sim", "registered_proxies"]
sim_cls = SimulatedDataMonitor
BIT_DEPTH = np.uint32
readback = Cpt(ComputedReadOnlySignal, value=0, kind=Kind.hinted)
readback = Cpt(ReadOnlySignal, value=BIT_DEPTH(0), kind=Kind.hinted, compute_readback=True)
SUB_READBACK = "readback"
_default_sub = SUB_READBACK
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)
self._registered_proxies = {}
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.
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
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]
@property
def registered_proxies(self) -> None:
"""Dictionary of registered signal_names and proxies."""
return self._registered_proxies
class SimCamera(Device):
USER_ACCESS = ["sim"]
"""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) : 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", "registered_proxies"]
sim_cls = SimulatedDataCamera
SHAPE = (100, 100)
BIT_DEPTH = np.uint16
SUB_MONITOR = "monitor"
_default_sub = SUB_MONITOR
@ -192,37 +118,43 @@ class SimCamera(Device):
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,
ReadOnlySignal,
name="image",
value=np.empty(SHAPE, dtype=np.uint16),
value=np.empty(SHAPE, dtype=BIT_DEPTH),
compute_readback=True,
kind=Kind.omitted,
)
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
self._registered_proxies = {}
self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs)
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
self._stopped = False
self.file_name = ""
self.metadata = {}
self._staged = False
self.scaninfo = None
self._update_scaninfo()
def trigger(self):
@property
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.
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 +162,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,40 +174,36 @@ 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().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.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()
def _send_data_to_bec(self) -> None:
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_name}
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
self.device_manager.producer.set_and_publish(
MessageEndpoints.device_read(self.name), msg.dumps()
)
def unstage(self) -> list[object]:
"""Unstage the device
@ -284,293 +211,42 @@ class SimCamera(Device):
"""
if self._stopped is True or not self._staged:
return super().unstage()
self._send_data_to_bec()
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"]
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 SynController(OphydObject):
def on(self):
pass
def off(self):
pass
class SynFlyerLamNI(Device, PositionerBase):
def __init__(
self,
*,
name,
readback_func=None,
value=0,
delay=0,
speed=1,
update_frequency=2,
precision=3,
parent=None,
labels=None,
kind=None,
device_manager=None,
**kwargs,
):
if readback_func is None:
def readback_func(x):
return x
sentinel = object()
loop = kwargs.pop("loop", sentinel)
if loop is not sentinel:
warnings.warn(
f"{self.__class__} no longer takes a loop as input. "
"Your input will be ignored and may raise in the future",
stacklevel=2,
)
self.sim_state = {}
self._readback_func = readback_func
self.delay = delay
self.precision = precision
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_manager = device_manager
# initialize values
self.sim_state["readback"] = readback_func(value)
self.sim_state["readback_ts"] = ttime.time()
super().__init__(name=name, parent=parent, labels=labels, kind=kind, **kwargs)
self.controller = SynController(name="SynController")
def kickoff(self, metadata, num_pos, positions, exp_time: float = 0):
positions = np.asarray(positions)
def produce_data(device, metadata):
buffer_time = 0.2
elapsed_time = 0
bundle = 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).
>>> 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)
@ -578,60 +254,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)
# TODO add short description to these two lines and explain what this does
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.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, 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."""
@ -647,6 +324,11 @@ class SimPositioner(Device, PositionerBase):
"""Return the high limit of the simulated device."""
return self.limits[1]
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.
@ -692,42 +374,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
@ -737,7 +418,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()
@ -747,8 +428,123 @@ class SimPositioner(Device, PositionerBase):
return "mm"
class SynDynamicComponents(Device):
messages = Dcpt({f"message{i}": (SynSignal, None, {"name": f"msg{i}"}) for i in range(1, 6)})
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,
*,
precision: int = 3,
parent=None,
kind=None,
device_manager=None,
# TODO remove after refactoring config
speed: float = 100,
delay: int = 1,
update_frequency: int = 100,
**kwargs,
):
self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs)
self.precision = precision
self.device_manager = device_manager
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 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()
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},
)
)
ttime.sleep(exp_time)
elapsed_time += exp_time
if elapsed_time > buffer_time:
elapsed_time = 0
device.device_manager.connector.set_and_publish(
MessageEndpoints.device_read(device.name), bundle
)
bundle = messages.BundleMessage()
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.set_and_publish(
MessageEndpoints.device_read(device.name), bundle
)
device.device_manager.connector.set(
MessageEndpoints.device_status(device.name),
messages.DeviceStatusMessage(
device=device.name, status=0, metadata={"pointID": num_pos, **metadata}
),
)
print("done")
flyer = threading.Thread(target=produce_data, args=(self, metadata))
flyer.start()
class SynDeviceSubOPAAS(Device):
@ -761,5 +557,10 @@ class SynDeviceOPAAS(Device):
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__":
gauss = SynGaussBEC(name="gauss")
cam = SimCamera(name="cam")
cam.image.read()

View File

@ -1,8 +1,15 @@
from abc import ABC, abstractmethod
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
@ -13,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):
@ -28,160 +35,383 @@ class NoiseType(str, enum.Enum):
POISSON = "poisson"
class SimulatedDataBase:
class HotPixelType(str, enum.Enum):
"""Type of hot pixel to add to simulated data."""
CONSTANT = "constant"
FLUCTUATING = "fluctuating"
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(lambda: {})
self._all_params = defaultdict(lambda: {})
self.device_manager = device_manager
self._simulation_type = None
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 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 execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any:
"""
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.
Execute either the provided method or reroutes the method execution
to a device proxy in case it is registered in self.parentregistered_proxies.
"""
return self._active_params
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
def set_sim_params(self, params: dict) -> None:
"""Change the current set of parameters for the active simulation type.
if method is not None:
return method(*args, **kwargs)
raise SimulatedDataException(f"Method {method} is not available for {self.parent.name}")
def sim_select_model(self, model: str) -> None:
"""
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.<device>.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 = self.parent.init_sim_params
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.bit_depth = self.parent.BIT_DEPTH
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 [
"ComplexConstantModel",
"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
@ -190,171 +420,170 @@ class SimulatedDataMonitor(SimulatedDataBase):
Args:
signal_name (str): Name of the signal to update.
"""
if self.get_sim_type() == SimulationType.CONSTANT:
value = self._compute_constant()
elif self.get_sim_type() == SimulationType.GAUSSIAN:
value = self._compute_gaussian()
if compute_readback:
method = self._compute
value = self.execute_simulation_method(method=method, signal_name=signal_name)
value = self.bit_depth(np.max(value, 0))
self.update_sim_state(signal_name, value)
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:
# 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
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.
If computation fails, it returns 0.
Returns: float
def _compute(self, *args, **kwargs) -> int:
"""
Compute the return value for given motor position and active model.
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)
)
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"]
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"]
else:
motor_pos = 0
method = self._model
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: 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 (int): Value to add noise to.
Returns:
int: Value with added noise.
"""
if noise == NoiseType.POISSON:
v = np.random.poisson(v)
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
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
return v
class SimulatedDataCamera(SimulatedDataBase):
"""Simulated class to compute data for a 2D camera."""
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_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.bit_depth = self.parent.BIT_DEPTH
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 _init_default(self) -> None:
"""Initialize the default model for a simulated camera
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.
Use the default model "Gaussian".
"""
self._all_params = {
SimulationType.CONSTANT: {
"amp": 100,
"noise": NoiseType.POISSON,
"noise_multiplier": 0.1,
},
SimulationType.GAUSSIAN: {
"amp": 100,
"cen_off": np.array([0, 0]),
"cov": np.array([[10, 5], [5, 10]]),
"noise": NoiseType.NONE,
"noise_multiplier": 0.1,
},
}
# 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)
self.sim_select_model(SimulationType2D.GAUSSIAN)
def _compute_sim_state(self, signal_name: str) -> None:
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.
signal_name (str) : Name of the signal to update.
compute_readback (bool) : Flag whether to compute readback based on function hosted in SimulatedData
"""
if self.get_sim_type() == SimulationType.CONSTANT:
value = self._compute_constant()
elif self.get_sim_type() == SimulationType.GAUSSIAN:
value = self._compute_gaussian()
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()
value = self.bit_depth(value)
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
def _compute_multivariate_gaussian(
self, pos: np.ndarray, cen_off: np.ndarray, cov: np.ndarray
) -> np.ndarray:
"""Computes and returns the multivariate Gaussian distribution.
Args:
pos (np.ndarray): Position of the gaussian.
cen_off (np.ndarray): Offset from cener of image for the gaussian.
cov (np.ndarray): Covariance matrix of the gaussian.
def _compute_empty_image(self) -> np.ndarray:
"""Computes return value for sim_type = "empty_image".
Returns:
np.ndarray: Multivariate Gaussian distribution.
float: 0
"""
try:
shape = self.parent.image_shape.get()
return np.zeros(shape)
except SimulatedDataException as exc:
raise SimulatedDataException(
f"Could not compute empty image for {self.parent.name} with {exc} raised. Deactivate eiger to continue."
) from exc
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)
# 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)
return np.exp(-fac / 2) / N
def _compute_constant(self) -> np.ndarray:
"""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.float32)
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 exc
def _compute_gaussian(self) -> float:
"""Computes return value for sim_type = "gauss".
@ -367,41 +596,121 @@ 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]),
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
)
pos = np.empty((*X.shape, 2))
pos[:, :, 0] = X
pos[:, :, 1] = Y
v = self._compute_multivariate_gaussian(
pos=pos, cen_off=params["cen_off"], cov=params["cov"]
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"],
)
# 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
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
def _compute_multivariate_gaussian(
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.
Args:
pos (np.ndarray): Position of the gaussian.
cen_off (np.ndarray): Offset from center of image for the gaussian.
cov (np.ndarray): Covariance matrix of the gaussian.
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)
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
def _prepare_params_gauss(
self, amp: float, cov: np.ndarray, offset: np.ndarray, shape: tuple
) -> tuple:
"""Prepare the positions for the gaussian.
Args:
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.
"""
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
return pos, offset, cov, amp
def _add_noise(self, v: np.ndarray, noise: NoiseType, noise_multiplier: float) -> 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:
v += np.random.uniform(-noise_multiplier, noise_multiplier, v.shape)
v[v <= 0] = 0
return v
if noise == NoiseType.NONE:
return v
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 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[coord[0], coord[1]] = value
elif hot_pixel_type == HotPixelType.FLUCTUATING:
maximum = np.max(v) if np.max(v) != 0 else 1
if v[coord[0], coord[1]] / maximum > 0.5:
v[coord[0], coord[1]] = value
return v

View File

@ -0,0 +1,355 @@
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, 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):
"""
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: baseline
deviceClass: SlitProxy
deviceConfig:
eiger:
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]
motor_dir: [0, 1] # x:0 , y:1, z:2 coordinates
enabled: true
readOnly: false
"""
USER_ACCESS = ["enabled", "lookup", "help"]
def __init__(
self,
name,
*args,
device_manager=None,
**kwargs,
):
self._gaussian_blur_sigma = 5
super().__init__(name, *args, device_manager=device_manager, **kwargs)
def help(self) -> None:
"""Print documentation for the SlitLookup device."""
print(self.__doc__)
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.
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.sim_params
shape = device_obj.image_shape.get()
params.update(
{
"noise": NoiseType.POISSON,
"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(
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(
device_pos=device_pos,
ref_motors=self.config[device_name]["ref_motors"],
width=self.config[device_name]["slit_width"],
direction=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, 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:
"""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],
direction: list[int],
):
mask = np.ones_like(device_pos)
for ii, motor_name in enumerate(ref_motors):
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[..., 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)
class H5ImageReplayProxy(DeviceProxy):
"""This Proxy class can be used to replay images from an h5 file.
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
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._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 replay data for a single device. The config has information about multiple devices {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:
"""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]
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 dataset, 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, also closes the HDF5 dataset"""
if self.h5_file:
self.stop()
self._staged = Staged.no
return [self]
def _load_image(self):
"""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
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
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)

View File

@ -1,28 +1,44 @@
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
from ophyd_devices.utils.bec_device_base import BECDeviceBase
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.
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)
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,22 +50,27 @@ 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.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):
"""Get the current position of the simulated device.
@ -58,6 +79,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 +105,57 @@ 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.
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)
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 +165,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.")
@ -220,18 +189,138 @@ class ComputedReadOnlySignal(Signal):
res[self.name]["precision"] = self.precision
return res
@property
def timestamp(self):
"""Timestamp of the readback value"""
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,
}
}
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())
def read_configuration(self):
"""Read method"""
return self.read()

View File

@ -0,0 +1,152 @@
import threading
import time as ttime
import numpy as np
from bec_lib import MessageEndpoints, messages
from ophyd import Device, OphydObject, PositionerBase
class DummyControllerDevice(Device):
USER_ACCESS = ["controller"]
class DummyController:
USER_ACCESS = [
"some_var",
"some_var_property",
"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 __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
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},
)
)
ttime.sleep(exp_time)
elapsed_time += exp_time
if elapsed_time > buffer_time:
elapsed_time = 0
device.device_manager.connector.set_and_publish(
MessageEndpoints.device_read(device.name), bundle
)
bundle = messages.BundleMessage()
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(
MessageEndpoints.device_status(device.name),
messages.DeviceStatusMessage(
device=device.name, status=0, metadata={"pointID": num_pos, **metadata}
),
)
print("done")
flyer = threading.Thread(target=produce_data, args=(self, metadata))
flyer.start()

View File

@ -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
)

50
ophyd_devices/sim/test.py Normal file
View File

@ -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

View File

@ -3,14 +3,13 @@ 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 (
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")
@ -73,7 +72,15 @@ 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",
"axis_is_referenced",
"all_axes_referenced",
]
def __init__(
self,
@ -110,14 +117,22 @@ 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 = 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}")
@ -175,7 +190,9 @@ 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.axis_Id_numeric) for ax in self._axis if ax is not None
)
@retry_once
@axis_checked
@ -234,19 +251,18 @@ 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}",
raise_if_not_status=True,
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True
)
@retry_once
@ -374,6 +390,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:
@ -411,15 +436,8 @@ 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",
]
for ax in range(self._Smaract_axis_per_controller):
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:
t.add_row(
@ -428,7 +446,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"),
]
)
@ -483,7 +501,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}'"
)

View File

@ -0,0 +1,161 @@
from typing import Protocol, runtime_checkable
from ophyd import Kind
@runtime_checkable
class BECDevice(Protocol):
"""Protocol for BEC devices with zero functionality."""
name: str
_destroyed: bool
@property
def kind(self) -> Kind:
"""kind property"""
@kind.setter
def kind(self, value: Kind):
"""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"""
@property
def connected(self) -> bool:
"""connected property.
Check if signals are connected
Returns:
bool: True if connected, False otherwise
"""
@connected.setter
def connected(self, value: bool):
"""connected setter"""
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')
"""
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')
"""
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" : ..}, ...}
"""
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" : ..}, ...}
"""
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, *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) -> Kind:
"""Kind property, stems from ophyd."""
return self._kind
@kind.setter
def kind(self, value: Kind):
"""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:
"""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

View File

@ -1,4 +1,4 @@
import os
import getpass
from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger
@ -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
@ -102,9 +102,13 @@ class BecScaninfoMixin:
def get_username(self) -> str:
"""Get username"""
if not self.sim_mode:
return self.device_manager.producer.get(MessageEndpoints.account()).decode()
return os.getlogin()
if self.sim_mode:
return getpass.getuser()
msg = self.device_manager.connector.get(MessageEndpoints.account())
if msg:
return msg
return getpass.getuser()
def load_scan_metadata(self) -> None:
"""Load scan metadata

View File

@ -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 Device, 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:
@ -52,7 +50,7 @@ class DeviceMock:
return self
class ProducerMock:
class ConnectorMock:
def __init__(self, store_data=True) -> None:
self.message_sent = []
self._get_buffer = {}
@ -122,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 = []
@ -142,47 +140,18 @@ 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)
# 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,
@ -220,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
@ -253,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):
@ -285,3 +244,28 @@ class ConfigSignal(Signal):
"shape": data_shape(val),
}
}
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):
return False
def wait_for_connection(self, all_signals=False, timeout=2):
raise RuntimeError("Connection error")
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")

View File

@ -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

View File

@ -0,0 +1,140 @@
"""
This module provides a class for creating a pseudo signal that is computed from other signals.
"""
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"""
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
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())
@property
def compute_method(self) -> Callable | None:
"""
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.
Example:
>>> signal.compute_method = "def test(a, b): return a.get() + b.get()"
"""
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()["user_compute_method"]
@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
Example:
>>> signal.input_signals = ["samx_readback", "samx_readback"]
"""
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:
# pylint: disable=not-callable
if self.input_signals:
return self._compute_method(*self.input_signals)
return self._compute_method()
return None

View File

@ -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}")

View File

@ -7,6 +7,8 @@ import ophyd
import yaml
from bec_lib.scibec_validator import SciBecValidator
from ophyd_devices.utils.bec_device_base import BECDevice
try:
from bec_plugins import devices as plugin_devices
except ImportError:
@ -176,7 +178,7 @@ class StaticDeviceTest:
Returns:
"""
assert isinstance(obj, BECDevice)
assert isinstance(obj.name, str)
assert isinstance(obj.read(), dict)
assert isinstance(obj.read_configuration(), dict)

View File

@ -15,7 +15,7 @@ classifiers =
package_dir =
= .
packages = find:
python_requires = >=3.9
python_requires = >=3.10
[options.packages.find]
where = .

View File

@ -1,6 +1,6 @@
from setuptools import setup
__version__ = "0.19.3"
__version__ = "0.26.1"
if __name__ == "__main__":
setup(
@ -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"]},

View File

@ -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()

View File

@ -0,0 +1,32 @@
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
# 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()"

View File

@ -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):

View File

@ -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(

60
tests/test_fupr_ophyd.py Normal file
View File

@ -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")

View File

@ -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

172
tests/test_galil_flomni.py Normal file
View File

@ -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()

View File

@ -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(

View File

@ -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(

89
tests/test_rt_flomni.py Normal file
View File

@ -0,0 +1,89 @@
from unittest import mock
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()
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()
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

132
tests/test_simulation.py Normal file
View File

@ -0,0 +1,132 @@
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)
@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):
"""Test the __init__ method of SimMonitor."""
assert isinstance(camera, SimCamera)
assert isinstance(camera, BECDevice)
@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)
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):
"""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")
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)

View File

@ -1,29 +1,45 @@
from unittest import mock
import pytest
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.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",
[
(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):
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
@ -33,17 +49,13 @@ 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):
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):
@ -57,17 +69,13 @@ 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),
],
)
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)
@ -79,21 +87,13 @@ 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()
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
@ -103,23 +103,19 @@ 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):
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
@ -131,16 +127,12 @@ 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):
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
@ -154,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
@ -172,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)
@ -191,67 +175,43 @@ 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,
)
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()
@pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])])
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)

View File

@ -1,8 +1,8 @@
from bec_lib.devicemanager import DeviceContainer
from bec_lib.tests.utils import ProducerMock
from unittest import mock
from bec_lib.devicemanager import DeviceContainer
from bec_lib.tests.utils import ConnectorMock
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:
@ -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"""