Merging master
This commit is contained in:
commit
c323354fea
@ -1,7 +1,7 @@
|
|||||||
# This file is a template, and might need editing before it works on your project.
|
# 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:
|
# Official language image. Look for the different tagged releases at:
|
||||||
# https://hub.docker.com/r/library/python/tags/
|
# https://hub.docker.com/r/library/python/tags/
|
||||||
image: $CI_DOCKER_REGISTRY/python:3.9
|
image: $CI_DOCKER_REGISTRY/python:3.10
|
||||||
|
|
||||||
workflow:
|
workflow:
|
||||||
rules:
|
rules:
|
||||||
@ -87,7 +87,7 @@ pytest:
|
|||||||
script:
|
script:
|
||||||
- pip install coverage
|
- pip install coverage
|
||||||
- coverage run --source=./ophyd_devices -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
|
- coverage run --source=./ophyd_devices -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
|
||||||
- coverage report
|
- coverage report
|
||||||
- coverage xml
|
- coverage xml
|
||||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||||
artifacts:
|
artifacts:
|
||||||
@ -97,22 +97,16 @@ pytest:
|
|||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
|
|
||||||
tests-3.10:
|
tests-3.11:
|
||||||
stage: AdditionalTests
|
stage: AdditionalTests
|
||||||
image: $CI_DOCKER_REGISTRY/python:3.10
|
image: $CI_DOCKER_REGISTRY/python:3.11
|
||||||
needs: ["pytest"]
|
needs: ["pytest"]
|
||||||
script:
|
script:
|
||||||
- pytest -v --random-order ./tests
|
- pytest -v --random-order ./tests
|
||||||
allow_failure: true
|
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:
|
tests-3.12:
|
||||||
extends: "tests-3.10"
|
extends: "tests-3.11"
|
||||||
stage: AdditionalTests
|
stage: AdditionalTests
|
||||||
image: $CI_DOCKER_REGISTRY/python:3.12
|
image: $CI_DOCKER_REGISTRY/python:3.12
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
@ -121,6 +115,8 @@ trigger_bec:
|
|||||||
trigger:
|
trigger:
|
||||||
project: bec/bec
|
project: bec/bec
|
||||||
strategy: depend
|
strategy: depend
|
||||||
|
variables:
|
||||||
|
OPHYD_DEVICES_BRANCH: $CI_COMMIT_REF_NAME
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
|
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
|
||||||
|
|
||||||
@ -144,10 +140,10 @@ semver:
|
|||||||
- export REPOSITORY_USERNAME=__token__
|
- export REPOSITORY_USERNAME=__token__
|
||||||
- export REPOSITORY_PASSWORD=$CI_PYPI_TOKEN
|
- export REPOSITORY_PASSWORD=$CI_PYPI_TOKEN
|
||||||
- >
|
- >
|
||||||
semantic-release publish -v DEBUG
|
semantic-release publish -v DEBUG
|
||||||
-D version_variable=./setup.py:__version__
|
-D version_variable=./setup.py:__version__
|
||||||
-D hvcs=gitlab
|
-D hvcs=gitlab
|
||||||
|
|
||||||
allow_failure: false
|
allow_failure: false
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||||
|
@ -52,7 +52,7 @@ persistent=yes
|
|||||||
|
|
||||||
# Minimum Python version to use for version dependent checks. Will default to
|
# Minimum Python version to use for version dependent checks. Will default to
|
||||||
# the version used to run pylint.
|
# 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
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
# user-friendly hints instead of false-positive error messages.
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
131
CHANGELOG.md
131
CHANGELOG.md
@ -2,6 +2,137 @@
|
|||||||
|
|
||||||
<!--next-version-placeholder-->
|
<!--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)
|
## v0.19.3 (2024-02-10)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
from .eiger1p5m_csaxs.eiger1p5m import Eiger1p5MDetector
|
from .eiger1p5m_csaxs.eiger1p5m import Eiger1p5MDetector
|
||||||
from .epics import *
|
from .epics import *
|
||||||
|
from .galil.fgalil_ophyd import FlomniGalilMotor
|
||||||
|
from .galil.fupr_ophyd import FuprGalilMotor
|
||||||
from .galil.galil_ophyd import GalilMotor
|
from .galil.galil_ophyd import GalilMotor
|
||||||
from .galil.sgalil_ophyd import SGalilMotor
|
from .galil.sgalil_ophyd import SGalilMotor
|
||||||
from .npoint.npoint import NPointAxis
|
from .npoint.npoint import NPointAxis
|
||||||
from .rt_lamni import RtLamniMotor
|
from .rt_lamni import RtFlomniMotor, RtLamniMotor
|
||||||
from .sim.sim import SimPositioner, SimMonitor, SimCamera
|
from .sim.sim import SimCamera
|
||||||
from .sim.sim import SimPositioner as SynAxisOPAAS
|
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 SynAxisMonitor
|
||||||
from .sim.sim import SimMonitor as SynGaussBEC
|
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
|
||||||
from .sim.sim_signals import ReadOnlySignal as SynSignalRO
|
from .sim.sim_signals import ReadOnlySignal as SynSignalRO
|
||||||
from .sim.sim import SynDeviceOPAAS, SynFlyer
|
|
||||||
from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages
|
from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages
|
||||||
from .smaract.smaract_ophyd import SmaractMotor
|
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
|
from .utils.static_device_test import launch
|
||||||
|
@ -81,8 +81,7 @@ class Eiger1p5MDetector(Device):
|
|||||||
self.username = "e20588" # TODO get from config
|
self.username = "e20588" # TODO get from config
|
||||||
|
|
||||||
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
|
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
|
||||||
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
return self.device_manager.connector.get(MessageEndpoints.scan_status())
|
||||||
return messages.ScanStatusMessage.loads(msg)
|
|
||||||
|
|
||||||
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
|
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
|
||||||
if leading_zeros is None:
|
if leading_zeros is None:
|
||||||
@ -159,9 +158,7 @@ class Eiger1p5MDetector(Device):
|
|||||||
self.detector_control.put("stop")
|
self.detector_control.put("stop")
|
||||||
signals = {"config": self.read(), "data": self.file_name}
|
signals = {"config": self.read(), "data": self.file_name}
|
||||||
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
|
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
|
||||||
self.device_manager.producer.set_and_publish(
|
self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg)
|
||||||
MessageEndpoints.device_read(self.name), msg.dumps()
|
|
||||||
)
|
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
return super().unstage()
|
return super().unstage()
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from ophyd.quadem import QuadEM
|
|||||||
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
|
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
|
||||||
|
|
||||||
from .devices.delay_generator_csaxs import DelayGeneratorcSAXS
|
from .devices.delay_generator_csaxs import DelayGeneratorcSAXS
|
||||||
|
from .devices.flomni_sample_storage import FlomniSampleStorage
|
||||||
from .devices.InsertionDevice import InsertionDevice
|
from .devices.InsertionDevice import InsertionDevice
|
||||||
from .devices.slits import SlitH, SlitV
|
from .devices.slits import SlitH, SlitV
|
||||||
from .devices.specMotors import (
|
from .devices.specMotors import (
|
||||||
@ -20,7 +21,7 @@ from .devices.specMotors import (
|
|||||||
PmMonoBender,
|
PmMonoBender,
|
||||||
)
|
)
|
||||||
from .devices.SpmBase import SpmBase
|
from .devices.SpmBase import SpmBase
|
||||||
from .devices.XbpmBase import XbpmBase, XbpmCsaxsOp
|
|
||||||
|
|
||||||
# X07MA specific devices
|
# X07MA specific devices
|
||||||
from .devices.X07MADevices import *
|
from .devices.X07MADevices import *
|
||||||
|
from .devices.XbpmBase import XbpmBase, XbpmCsaxsOp
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
from .slits import SlitH, SlitV
|
# Standard ophyd classes
|
||||||
from .XbpmBase import XbpmBase, XbpmCsaxsOp
|
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
|
||||||
from .SpmBase import SpmBase
|
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 .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 (
|
from .specMotors import (
|
||||||
Bpm4i,
|
Bpm4i,
|
||||||
EnergyKev,
|
EnergyKev,
|
||||||
@ -16,19 +29,7 @@ from .specMotors import (
|
|||||||
PmMonoBender,
|
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 .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
|
||||||
|
@ -301,20 +301,20 @@ class Eiger9MSetup(CustomDetectorMixin):
|
|||||||
done (bool): True if scan is finished
|
done (bool): True if scan is finished
|
||||||
successful (bool): True if scan was successful
|
successful (bool): True if scan was successful
|
||||||
"""
|
"""
|
||||||
pipe = self.parent.producer.pipeline()
|
pipe = self.parent.connector.pipeline()
|
||||||
if successful is None:
|
if successful is None:
|
||||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||||
else:
|
else:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=self.parent.filepath, done=done, successful=successful
|
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),
|
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
|
||||||
msg.dumps(),
|
msg,
|
||||||
pipe=pipe,
|
pipe=pipe,
|
||||||
)
|
)
|
||||||
self.parent.producer.set_and_publish(
|
self.parent.connector.set_and_publish(
|
||||||
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
|
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||||
)
|
)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
|
@ -244,20 +244,20 @@ class FalconSetup(CustomDetectorMixin):
|
|||||||
done (bool): True if scan is finished
|
done (bool): True if scan is finished
|
||||||
successful (bool): True if scan was successful
|
successful (bool): True if scan was successful
|
||||||
"""
|
"""
|
||||||
pipe = self.parent.producer.pipeline()
|
pipe = self.parent.connector.pipeline()
|
||||||
if successful is None:
|
if successful is None:
|
||||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||||
else:
|
else:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=self.parent.filepath, done=done, successful=successful
|
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),
|
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
|
||||||
msg.dumps(),
|
msg,
|
||||||
pipe=pipe,
|
pipe=pipe,
|
||||||
)
|
)
|
||||||
self.parent.producer.set_and_publish(
|
self.parent.connector.set_and_publish(
|
||||||
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
|
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||||
)
|
)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
|
116
ophyd_devices/epics/devices/flomni_sample_storage.py
Normal file
116
ophyd_devices/epics/devices/flomni_sample_storage.py
Normal 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")
|
@ -136,8 +136,8 @@ class MCSSetup(CustomDetectorMixin):
|
|||||||
msg = messages.DeviceMessage(
|
msg = messages.DeviceMessage(
|
||||||
signals=dict(self.mca_data),
|
signals=dict(self.mca_data),
|
||||||
metadata=self.parent.scaninfo.scan_msg.metadata,
|
metadata=self.parent.scaninfo.scan_msg.metadata,
|
||||||
).dumps()
|
)
|
||||||
self.parent.producer.xadd(
|
self.parent.connector.xadd(
|
||||||
topic=MessageEndpoints.device_async_readback(
|
topic=MessageEndpoints.device_async_readback(
|
||||||
scanID=self.parent.scaninfo.scanID, device=self.parent.name
|
scanID=self.parent.scaninfo.scanID, device=self.parent.name
|
||||||
),
|
),
|
||||||
|
@ -331,7 +331,7 @@ class PilatusSetup(CustomDetectorMixin):
|
|||||||
done (bool): True if scan is finished
|
done (bool): True if scan is finished
|
||||||
successful (bool): True if scan was successful
|
successful (bool): True if scan was successful
|
||||||
"""
|
"""
|
||||||
pipe = self.parent.producer.pipeline()
|
pipe = self.parent.connector.pipeline()
|
||||||
if successful is None:
|
if successful is None:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=self.parent.filepath,
|
file_path=self.parent.filepath,
|
||||||
@ -345,13 +345,13 @@ class PilatusSetup(CustomDetectorMixin):
|
|||||||
successful=successful,
|
successful=successful,
|
||||||
metadata={"input_path": self.parent.filepath_raw},
|
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),
|
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
|
||||||
msg.dumps(),
|
msg,
|
||||||
pipe=pipe,
|
pipe=pipe,
|
||||||
)
|
)
|
||||||
self.parent.producer.set_and_publish(
|
self.parent.connector.set_and_publish(
|
||||||
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
|
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||||
)
|
)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from ophyd.pseudopos import (
|
|||||||
)
|
)
|
||||||
from ophyd.device import Staged
|
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 ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
@ -391,7 +391,7 @@ class PSIDelayGeneratorBase(Device):
|
|||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
else:
|
else:
|
||||||
self.device_manager = bec_utils.DMMock()
|
self.device_manager = bec_utils.DMMock()
|
||||||
self.producer = self.device_manager.producer
|
self.connector = self.device_manager.connector
|
||||||
self._update_scaninfo()
|
self._update_scaninfo()
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from bec_lib.device import DeviceStatus
|
|||||||
from bec_lib.file_utils import FileWriterMixin
|
from bec_lib.file_utils import FileWriterMixin
|
||||||
from ophyd import Device
|
from ophyd import Device
|
||||||
from ophyd.device import Staged
|
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 ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ class PSIDetectorBase(Device):
|
|||||||
self.device_manager = bec_utils.DMMock()
|
self.device_manager = bec_utils.DMMock()
|
||||||
base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
|
base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
|
||||||
self.service_cfg = {"base_path": os.path.expanduser(base_path)}
|
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_scaninfo()
|
||||||
self._update_filewriter()
|
self._update_filewriter()
|
||||||
self._init()
|
self._init()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import functools
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -7,28 +6,42 @@ from bec_lib import bec_logger
|
|||||||
from ophyd import Component as Cpt
|
from ophyd import Component as Cpt
|
||||||
from ophyd import Device, PositionerBase, Signal
|
from ophyd import Device, PositionerBase, Signal
|
||||||
from ophyd.status import wait as status_wait
|
from ophyd.status import wait as status_wait
|
||||||
from ophyd.utils import LimitError, ReadOnlyError
|
from ophyd.utils import LimitError
|
||||||
from prettytable import PrettyTable
|
|
||||||
|
|
||||||
from ophyd_devices.galil.galil_ophyd import (
|
from ophyd_devices.galil.galil_ophyd import (
|
||||||
BECConfigError,
|
BECConfigError,
|
||||||
GalilAxesReferenced,
|
GalilAxesReferenced,
|
||||||
GalilCommunicationError,
|
|
||||||
GalilController,
|
GalilController,
|
||||||
GalilError,
|
GalilError,
|
||||||
GalilMotorIsMoving,
|
GalilMotorIsMoving,
|
||||||
GalilMotorResolution,
|
GalilMotorResolution,
|
||||||
GalilReadbackSignal,
|
|
||||||
GalilSetpointSignal,
|
GalilSetpointSignal,
|
||||||
|
GalilSignalRO,
|
||||||
retry_once,
|
retry_once,
|
||||||
)
|
)
|
||||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
from ophyd_devices.utils.controller import threadlocked
|
||||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class FlomniGalilController(GalilController):
|
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:
|
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||||
if axis_Id is None and axis_Id_numeric is not None:
|
if axis_Id is None and axis_Id_numeric is not None:
|
||||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
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
|
# TODO: check if all axes are referenced in all controllers
|
||||||
return super().all_axes_referenced()
|
return super().all_axes_referenced()
|
||||||
|
|
||||||
|
def fosaz_light_curtain_is_triggered(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check the light curtain status for fosaz
|
||||||
|
|
||||||
class FlomniGalilReadbackSignal(GalilReadbackSignal):
|
Returns:
|
||||||
pass
|
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):
|
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):
|
class FlomniGalilMotorResolution(GalilMotorResolution):
|
||||||
@ -63,11 +142,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced):
|
|||||||
|
|
||||||
class FlomniGalilMotor(Device, PositionerBase):
|
class FlomniGalilMotor(Device, PositionerBase):
|
||||||
USER_ACCESS = ["controller"]
|
USER_ACCESS = ["controller"]
|
||||||
readback = Cpt(
|
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||||
GalilReadbackSignal,
|
|
||||||
signal_name="readback",
|
|
||||||
kind="hinted",
|
|
||||||
)
|
|
||||||
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
|
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
|
||||||
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
|
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
|
||||||
motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
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."
|
"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")
|
||||||
|
self.pid_x_correction = 0
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
prefix,
|
prefix,
|
||||||
@ -202,18 +278,10 @@ class FlomniGalilMotor(Device, PositionerBase):
|
|||||||
while self.motor_is_moving.get():
|
while self.motor_is_moving.get():
|
||||||
logger.info("motor is moving")
|
logger.info("motor is moving")
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_READBACK,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
success = np.isclose(
|
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||||
val[self.name]["value"],
|
|
||||||
position,
|
|
||||||
atol=self.tolerance,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
print(" stop")
|
print(" stop")
|
||||||
@ -238,7 +306,7 @@ class FlomniGalilMotor(Device, PositionerBase):
|
|||||||
def axis_Id(self, val):
|
def axis_Id(self, val):
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
if len(val) != 1:
|
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_alpha = val
|
||||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||||
else:
|
else:
|
||||||
@ -252,7 +320,7 @@ class FlomniGalilMotor(Device, PositionerBase):
|
|||||||
def axis_Id_numeric(self, val):
|
def axis_Id_numeric(self, val):
|
||||||
if isinstance(val, int):
|
if isinstance(val, int):
|
||||||
if val > 26:
|
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_alpha = val
|
||||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||||
else:
|
else:
|
||||||
|
@ -62,6 +62,18 @@ class FuprGalilReadbackSignal(GalilReadbackSignal):
|
|||||||
step_mm = self.parent.motor_resolution.get()
|
step_mm = self.parent.motor_resolution.get()
|
||||||
return current_pos / step_mm
|
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):
|
class FuprGalilSetpointSignal(GalilSetpointSignal):
|
||||||
@retry_once
|
@retry_once
|
||||||
@ -107,11 +119,7 @@ class FuprGalilAxesReferenced(GalilAxesReferenced):
|
|||||||
class FuprGalilMotor(Device, PositionerBase):
|
class FuprGalilMotor(Device, PositionerBase):
|
||||||
USER_ACCESS = ["controller"]
|
USER_ACCESS = ["controller"]
|
||||||
MOTOR_RESOLUTION = 25600
|
MOTOR_RESOLUTION = 25600
|
||||||
readback = Cpt(
|
readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||||
GalilReadbackSignal,
|
|
||||||
signal_name="readback",
|
|
||||||
kind="hinted",
|
|
||||||
)
|
|
||||||
user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint")
|
user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint")
|
||||||
motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config")
|
motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config")
|
||||||
motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||||
@ -157,7 +165,7 @@ class FuprGalilMotor(Device, PositionerBase):
|
|||||||
raise BECConfigError(
|
raise BECConfigError(
|
||||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
"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__(
|
super().__init__(
|
||||||
prefix,
|
prefix,
|
||||||
@ -246,18 +254,10 @@ class FuprGalilMotor(Device, PositionerBase):
|
|||||||
while self.motor_is_moving.get():
|
while self.motor_is_moving.get():
|
||||||
logger.info("motor is moving")
|
logger.info("motor is moving")
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_READBACK,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
success = np.isclose(
|
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||||
val[self.name]["value"],
|
|
||||||
position,
|
|
||||||
atol=self.tolerance,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
print(" stop")
|
print(" stop")
|
||||||
@ -316,31 +316,3 @@ class FuprGalilMotor(Device, PositionerBase):
|
|||||||
def stop(self, *, success=False):
|
def stop(self, *, success=False):
|
||||||
self.controller.stop_all_axes()
|
self.controller.stop_all_axes()
|
||||||
return super().stop(success=success)
|
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()
|
|
||||||
|
@ -51,6 +51,11 @@ class GalilController(Controller):
|
|||||||
"socket_put_and_receive",
|
"socket_put_and_receive",
|
||||||
"socket_put_confirmed",
|
"socket_put_confirmed",
|
||||||
"lgalil_is_air_off_and_orchestra_enabled",
|
"lgalil_is_air_off_and_orchestra_enabled",
|
||||||
|
"drive_axis_to_limit",
|
||||||
|
"find_reference",
|
||||||
|
"get_motor_limit_switch",
|
||||||
|
"is_motor_on",
|
||||||
|
"all_axes_referenced",
|
||||||
]
|
]
|
||||||
|
|
||||||
@threadlocked
|
@threadlocked
|
||||||
@ -142,10 +147,11 @@ class GalilController(Controller):
|
|||||||
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
||||||
self.socket_put_confirmed(f"ndir={direction_flag}")
|
self.socket_put_confirmed(f"ndir={direction_flag}")
|
||||||
self.socket_put_confirmed("XQ#NEWPAR")
|
self.socket_put_confirmed("XQ#NEWPAR")
|
||||||
|
time.sleep(0.005)
|
||||||
self.socket_put_confirmed("XQ#FES")
|
self.socket_put_confirmed("XQ#FES")
|
||||||
time.sleep(0.1)
|
time.sleep(0.01)
|
||||||
while self.is_axis_moving(None, axis_Id_numeric):
|
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)
|
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||||
# check if we actually hit the limit
|
# check if we actually hit the limit
|
||||||
@ -363,11 +369,7 @@ class GalilMotorIsMoving(GalilSignalRO):
|
|||||||
def get(self):
|
def get(self):
|
||||||
val = super().get()
|
val = super().get()
|
||||||
if val is not None:
|
if val is not None:
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_VALUE,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
@ -379,11 +381,7 @@ class GalilAxesReferenced(GalilSignalRO):
|
|||||||
|
|
||||||
class GalilMotor(Device, PositionerBase):
|
class GalilMotor(Device, PositionerBase):
|
||||||
USER_ACCESS = ["controller"]
|
USER_ACCESS = ["controller"]
|
||||||
readback = Cpt(
|
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||||
GalilReadbackSignal,
|
|
||||||
signal_name="readback",
|
|
||||||
kind="hinted",
|
|
||||||
)
|
|
||||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||||
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
|
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
|
||||||
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
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():
|
while self.motor_is_moving.get():
|
||||||
logger.info("motor is moving")
|
logger.info("motor is moving")
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_READBACK,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
success = np.isclose(
|
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||||
val[self.name]["value"],
|
|
||||||
position,
|
|
||||||
atol=self.tolerance,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
print(" stop")
|
print(" stop")
|
||||||
@ -587,6 +577,7 @@ class GalilMotor(Device, PositionerBase):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# pytest: skip-file
|
||||||
mock = False
|
mock = False
|
||||||
if not mock:
|
if not mock:
|
||||||
leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
||||||
|
@ -1 +1,2 @@
|
|||||||
from .rt_lamni_ophyd import RtLamniMotor, RtLamniController
|
from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
|
||||||
|
from .rt_lamni_ophyd import RtLamniController, RtLamniMotor
|
||||||
|
768
ophyd_devices/rt_lamni/rt_flomni_ophyd.py
Normal file
768
ophyd_devices/rt_lamni/rt_flomni_ophyd.py
Normal 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()
|
@ -3,7 +3,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import numpy as np
|
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 Component as Cpt
|
||||||
from ophyd import Device, PositionerBase, Signal
|
from ophyd import Device, PositionerBase, Signal
|
||||||
from ophyd.status import wait as status_wait
|
from ophyd.status import wait as status_wait
|
||||||
@ -308,9 +308,9 @@ class RtLamniController(Controller):
|
|||||||
|
|
||||||
def _update_flyer_device_info(self):
|
def _update_flyer_device_info(self):
|
||||||
flyer_info = self._get_flyer_device_info()
|
flyer_info = self._get_flyer_device_info()
|
||||||
self.get_device_manager().producer.set(
|
self.get_device_manager().connector.set(
|
||||||
MessageEndpoints.device_info("rt_scan"),
|
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:
|
def _get_flyer_device_info(self) -> dict:
|
||||||
@ -385,11 +385,11 @@ class RtLamniController(Controller):
|
|||||||
|
|
||||||
# if not (mode==2 or mode==3):
|
# if not (mode==2 or mode==3):
|
||||||
# error
|
# error
|
||||||
self.get_device_manager().producer.set_and_publish(
|
self.get_device_manager().connector.set(
|
||||||
MessageEndpoints.device_status("rt_scan"),
|
MessageEndpoints.device_status("rt_scan"),
|
||||||
messages.DeviceStatusMessage(
|
messages.DeviceStatusMessage(
|
||||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||||
).dumps(),
|
),
|
||||||
)
|
)
|
||||||
# while scan is running
|
# while scan is running
|
||||||
while mode > 0:
|
while mode > 0:
|
||||||
@ -420,11 +420,11 @@ class RtLamniController(Controller):
|
|||||||
signals = self._get_signals_from_table(return_table)
|
signals = self._get_signals_from_table(return_table)
|
||||||
self.publish_device_data(signals=signals, pointID=int(return_table[0]))
|
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"),
|
MessageEndpoints.device_status("rt_scan"),
|
||||||
messages.DeviceStatusMessage(
|
messages.DeviceStatusMessage(
|
||||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||||
).dumps(),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -432,12 +432,11 @@ class RtLamniController(Controller):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def publish_device_data(self, signals, pointID):
|
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"),
|
MessageEndpoints.device_read("rt_lamni"),
|
||||||
messages.DeviceMessage(
|
messages.DeviceMessage(
|
||||||
signals=signals,
|
signals=signals, metadata={"pointID": pointID, **self.readout_metadata}
|
||||||
metadata={"pointID": pointID, **self.readout_metadata},
|
),
|
||||||
).dumps(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def feedback_status_angle_lamni(self) -> bool:
|
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."
|
f"Device {device_name} is not configured and cannot be enabled/disabled."
|
||||||
)
|
)
|
||||||
return
|
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):
|
class RtLamniSignalBase(SocketSignal):
|
||||||
@ -623,11 +622,7 @@ class RtLamniMotorIsMoving(RtLamniSignalRO):
|
|||||||
def get(self):
|
def get(self):
|
||||||
val = super().get()
|
val = super().get()
|
||||||
if val is not None:
|
if val is not None:
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_VALUE,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
@ -642,11 +637,7 @@ class RtLamniFeedbackRunning(RtLamniSignalRO):
|
|||||||
|
|
||||||
class RtLamniMotor(Device, PositionerBase):
|
class RtLamniMotor(Device, PositionerBase):
|
||||||
USER_ACCESS = ["controller"]
|
USER_ACCESS = ["controller"]
|
||||||
readback = Cpt(
|
readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted")
|
||||||
RtLamniReadbackSignal,
|
|
||||||
signal_name="readback",
|
|
||||||
kind="hinted",
|
|
||||||
)
|
|
||||||
user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint")
|
user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint")
|
||||||
|
|
||||||
motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
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():
|
while self.motor_is_moving.get():
|
||||||
print("motor is moving")
|
print("motor is moving")
|
||||||
val = self.readback.read()
|
val = self.readback.read()
|
||||||
self._run_subs(
|
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||||
sub_type=self.SUB_READBACK,
|
|
||||||
value=val,
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
print("Move finished")
|
print("Move finished")
|
||||||
self._done_moving()
|
self._done_moving()
|
||||||
|
818
ophyd_devices/rt_lamni/rt_ophyd.py
Normal file
818
ophyd_devices/rt_lamni/rt_ophyd.py
Normal 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()
|
@ -2,9 +2,11 @@ from .sim import (
|
|||||||
SimPositioner,
|
SimPositioner,
|
||||||
SimMonitor,
|
SimMonitor,
|
||||||
SimCamera,
|
SimCamera,
|
||||||
SynDynamicComponents,
|
SimFlyer,
|
||||||
SynFlyer,
|
SimFlyer as SynFlyer,
|
||||||
)
|
)
|
||||||
from .sim_xtreme import SynXtremeOtf
|
from .sim_xtreme import SynXtremeOtf
|
||||||
|
|
||||||
from .sim_signals import SetableSignal, ReadOnlySignal, ComputedReadOnlySignal
|
from .sim_signals import SetableSignal, ReadOnlySignal
|
||||||
|
|
||||||
|
from .sim_frameworks import SlitProxy
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
from collections import defaultdict
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time as ttime
|
import time as ttime
|
||||||
import warnings
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||||
from ophyd import Component as Cpt
|
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 DynamicDeviceComponent as Dcpt
|
||||||
from ophyd import OphydObject, PositionerBase, Signal
|
from ophyd import Kind, PositionerBase
|
||||||
from ophyd.sim import EnumSignal, SynSignal
|
from ophyd.flyers import FlyerInterface
|
||||||
from ophyd.utils import LimitError, ReadOnlyError
|
from ophyd.sim import SynSignal
|
||||||
from ophyd_devices.sim.sim_data import SimulatedDataBase, SimulatedDataCamera, SimulatedDataMonitor
|
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
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -27,162 +33,82 @@ class SimMonitor(Device):
|
|||||||
"""
|
"""
|
||||||
A simulated device mimic any 1D Axis (position, temperature, beam).
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
name : string, keyword only
|
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
||||||
value : object, optional
|
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||||
The initial value. Default is 0.
|
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||||
delay : number, optional
|
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||||
Simulates how long it takes the device to "move". Default is 0 seconds.
|
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||||
precision : integer, optional
|
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
USER_ACCESS = ["sim"]
|
USER_ACCESS = ["sim", "registered_proxies"]
|
||||||
|
|
||||||
sim_cls = SimulatedDataMonitor
|
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"
|
SUB_READBACK = "readback"
|
||||||
_default_sub = SUB_READBACK
|
_default_sub = SUB_READBACK
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
name,
|
name,
|
||||||
value=0,
|
*,
|
||||||
delay=0,
|
precision: int = 3,
|
||||||
precision=3,
|
|
||||||
tolerance: float = 0.5,
|
|
||||||
sim_init: dict = None,
|
sim_init: dict = None,
|
||||||
parent=None,
|
parent=None,
|
||||||
labels=None,
|
|
||||||
kind=None,
|
kind=None,
|
||||||
device_manager=None,
|
device_manager=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.delay = delay
|
|
||||||
self.precision = precision
|
self.precision = precision
|
||||||
self.tolerance = tolerance
|
|
||||||
self.init_sim_params = sim_init
|
self.init_sim_params = sim_init
|
||||||
self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs)
|
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.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
||||||
self.readback.name = self.name
|
self.readback.name = self.name
|
||||||
|
|
||||||
|
@property
|
||||||
class SynGaussBEC(Device):
|
def registered_proxies(self) -> None:
|
||||||
"""
|
"""Dictionary of registered signal_names and proxies."""
|
||||||
Evaluate a point on a Gaussian based on the value of a motor.
|
return self._registered_proxies
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
class SimCamera(Device):
|
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
|
sim_cls = SimulatedDataCamera
|
||||||
SHAPE = (100, 100)
|
SHAPE = (100, 100)
|
||||||
|
BIT_DEPTH = np.uint16
|
||||||
|
|
||||||
SUB_MONITOR = "monitor"
|
SUB_MONITOR = "monitor"
|
||||||
_default_sub = SUB_MONITOR
|
_default_sub = SUB_MONITOR
|
||||||
@ -192,37 +118,43 @@ class SimCamera(Device):
|
|||||||
file_pattern = Cpt(SetableSignal, name="file_pattern", value="", kind=Kind.config)
|
file_pattern = Cpt(SetableSignal, name="file_pattern", value="", kind=Kind.config)
|
||||||
frames = Cpt(SetableSignal, name="frames", value=1, kind=Kind.config)
|
frames = Cpt(SetableSignal, name="frames", value=1, kind=Kind.config)
|
||||||
burst = Cpt(SetableSignal, name="burst", 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_shape = Cpt(SetableSignal, name="image_shape", value=SHAPE, kind=Kind.config)
|
||||||
image = Cpt(
|
image = Cpt(
|
||||||
ComputedReadOnlySignal,
|
ReadOnlySignal,
|
||||||
name="image",
|
name="image",
|
||||||
value=np.empty(SHAPE, dtype=np.uint16),
|
value=np.empty(SHAPE, dtype=BIT_DEPTH),
|
||||||
|
compute_readback=True,
|
||||||
kind=Kind.omitted,
|
kind=Kind.omitted,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs
|
||||||
*,
|
|
||||||
name,
|
|
||||||
kind=None,
|
|
||||||
parent=None,
|
|
||||||
sim_init: dict = None,
|
|
||||||
device_manager=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
):
|
||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self.init_sim_params = sim_init
|
self.init_sim_params = sim_init
|
||||||
|
self._registered_proxies = {}
|
||||||
self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs)
|
self.sim = self.sim_cls(parent=self, device_manager=device_manager, **kwargs)
|
||||||
|
|
||||||
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
|
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self.file_name = ""
|
self._staged = False
|
||||||
self.metadata = {}
|
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)
|
status = DeviceStatus(self)
|
||||||
|
|
||||||
self.subscribe(status._finished, event_type=self.SUB_ACQ_DONE, run=False)
|
self.subscribe(status._finished, event_type=self.SUB_ACQ_DONE, run=False)
|
||||||
@ -230,7 +162,6 @@ class SimCamera(Device):
|
|||||||
def acquire():
|
def acquire():
|
||||||
try:
|
try:
|
||||||
for _ in range(self.burst.get()):
|
for _ in range(self.burst.get()):
|
||||||
# Send data for each trigger
|
|
||||||
self._run_subs(sub_type=self.SUB_MONITOR, value=self.image.get())
|
self._run_subs(sub_type=self.SUB_MONITOR, value=self.image.get())
|
||||||
if self._stopped:
|
if self._stopped:
|
||||||
raise DeviceStop
|
raise DeviceStop
|
||||||
@ -243,40 +174,36 @@ class SimCamera(Device):
|
|||||||
threading.Thread(target=acquire, daemon=True).start()
|
threading.Thread(target=acquire, daemon=True).start()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def stage(self) -> list[object]:
|
def _update_scaninfo(self) -> None:
|
||||||
"""Stage the camera
|
"""Update scaninfo from BecScaninfoMixing
|
||||||
|
This depends on device manager and operation/sim_mode
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
self.scaninfo = BecScaninfoMixin(self.device_manager)
|
||||||
scan_msg = messages.ScanStatusMessage.loads(msg)
|
|
||||||
self.metadata = {
|
def stage(self) -> list[object]:
|
||||||
"scanID": scan_msg.content["scanID"],
|
"""Stage the camera for upcoming scan
|
||||||
"RID": scan_msg.content["info"]["RID"],
|
|
||||||
"queueID": scan_msg.content["info"]["queueID"],
|
This method is called from BEC in preparation of a scan.
|
||||||
}
|
It receives metadata about the scan from BEC,
|
||||||
scan_number = scan_msg.content["info"]["scan_number"]
|
compiles it and prepares the camera for the scan.
|
||||||
self.frames.set(scan_msg.content["info"]["num_points"])
|
|
||||||
self.file_name = os.path.join(
|
FYI: No data is written to disk in the simulation, but upon each trigger it
|
||||||
self.file_path.get(), self.file_pattern.get().format(scan_number)
|
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
|
self._stopped = False
|
||||||
return super().stage()
|
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]:
|
def unstage(self) -> list[object]:
|
||||||
"""Unstage the device
|
"""Unstage the device
|
||||||
|
|
||||||
@ -284,293 +211,42 @@ class SimCamera(Device):
|
|||||||
"""
|
"""
|
||||||
if self._stopped is True or not self._staged:
|
if self._stopped is True or not self._staged:
|
||||||
return super().unstage()
|
return super().unstage()
|
||||||
self._send_data_to_bec()
|
|
||||||
|
|
||||||
return super().unstage()
|
return super().unstage()
|
||||||
|
|
||||||
def stop(self, *, success=False):
|
def stop(self, *, success=False):
|
||||||
|
"""Stop the device"""
|
||||||
self._stopped = True
|
self._stopped = True
|
||||||
super().stop(success=success)
|
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):
|
class SimPositioner(Device, PositionerBase):
|
||||||
"""
|
"""
|
||||||
A simulated device mimicing any 1D Axis device (position, temperature, rotation).
|
A simulated device mimicing any 1D Axis device (position, temperature, rotation).
|
||||||
|
|
||||||
|
>>> motor = SimPositioner(name="motor")
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
name : string, keyword only
|
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.\
|
||||||
readback_func : callable, optional
|
Optional parameters:
|
||||||
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
|
delay (int) : If 0, execution of move will be instant. If 1, exectution will depend on motor velocity. Default is 1.
|
||||||
offset.
|
update_frequency (int) : Frequency in Hz of the update of the simulated state during a move. Default is 2 Hz.
|
||||||
Expected signature: ``f(x) -> value``.
|
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||||
value : object, optional
|
tolerance (float) : Tolerance of the positioner to accept reaching target positions. Default is 0.5.
|
||||||
The initial value. Default is 0.
|
limits (tuple) : Tuple of the low and high limits of the positioner. Overrides low/high_limit_travel is specified. Default is None.
|
||||||
delay : number, optional
|
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||||
Simulates how long it takes the device to "move". Default is 0 seconds.
|
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||||
precision : integer, optional
|
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
||||||
Digits of precision. Default is 3.
|
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Specify which attributes are accessible via BEC client
|
# 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
|
# Define the signals as class attributes
|
||||||
readback = Cpt(ReadOnlySignal, name="readback", value=0, kind=Kind.hinted)
|
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)
|
motor_is_moving = Cpt(SetableSignal, value=0, kind=Kind.normal)
|
||||||
|
|
||||||
# Config signals
|
# 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)
|
acceleration = Cpt(SetableSignal, value=1, kind=Kind.config)
|
||||||
|
|
||||||
# Ommitted signals
|
# Ommitted signals
|
||||||
high_limit_travel = Cpt(SetableSignal, value=0, kind=Kind.omitted)
|
high_limit_travel = Cpt(SetableSignal, value=0, kind=Kind.omitted)
|
||||||
low_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"
|
SUB_READBACK = "readback"
|
||||||
_default_sub = SUB_READBACK
|
_default_sub = SUB_READBACK
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
name,
|
name,
|
||||||
readback_func=None,
|
*,
|
||||||
value=0,
|
delay: int = 1,
|
||||||
delay=1,
|
|
||||||
speed=1,
|
|
||||||
update_frequency=2,
|
update_frequency=2,
|
||||||
precision=3,
|
precision=3,
|
||||||
parent=None,
|
|
||||||
labels=None,
|
|
||||||
kind=None,
|
|
||||||
limits=None,
|
|
||||||
tolerance: float = 0.5,
|
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,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# Whether motions should be instantaneous or depend on motor velocity
|
|
||||||
self.delay = delay
|
self.delay = delay
|
||||||
|
self.device_manager = device_manager
|
||||||
self.precision = precision
|
self.precision = precision
|
||||||
self.tolerance = tolerance
|
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.update_frequency = update_frequency
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self.dummy_controller = DummyController()
|
self.dummy_controller = DummyController()
|
||||||
|
|
||||||
# initialize inner dictionary with simulated state
|
|
||||||
self.sim = self.sim_cls(parent=self, **kwargs)
|
self.sim = self.sim_cls(parent=self, **kwargs)
|
||||||
|
|
||||||
super().__init__(name=name, labels=labels, kind=kind, **kwargs)
|
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
|
||||||
# Rename self.readback.name to self.name, also in self.sim_state
|
|
||||||
self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
||||||
self.readback.name = self.name
|
self.readback.name = self.name
|
||||||
# Init limits from deviceConfig
|
|
||||||
if limits is not None:
|
if limits is not None:
|
||||||
assert len(limits) == 2
|
assert len(limits) == 2
|
||||||
self.low_limit_travel.put(limits[0])
|
self.low_limit_travel.put(limits[0])
|
||||||
self.high_limit_travel.put(limits[1])
|
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
|
@property
|
||||||
def limits(self):
|
def limits(self):
|
||||||
"""Return the limits of the simulated device."""
|
"""Return the limits of the simulated device."""
|
||||||
@ -647,6 +324,11 @@ class SimPositioner(Device, PositionerBase):
|
|||||||
"""Return the high limit of the simulated device."""
|
"""Return the high limit of the simulated device."""
|
||||||
return self.limits[1]
|
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):
|
def check_value(self, value: any):
|
||||||
"""
|
"""
|
||||||
Check that requested position is within existing limits.
|
Check that requested position is within existing limits.
|
||||||
@ -692,42 +374,41 @@ class SimPositioner(Device, PositionerBase):
|
|||||||
|
|
||||||
st = DeviceStatus(device=self)
|
st = DeviceStatus(device=self)
|
||||||
if self.delay:
|
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():
|
def move_and_finish():
|
||||||
"""Move the simulated device and finish the motion."""
|
"""Move the simulated device and finish the motion."""
|
||||||
success = True
|
success = True
|
||||||
try:
|
try:
|
||||||
# Compute final position with some jitter
|
|
||||||
move_val = self._get_sim_state(
|
move_val = self._get_sim_state(
|
||||||
self.setpoint.name
|
self.setpoint.name
|
||||||
) + self.tolerance * np.random.uniform(-1, 1)
|
) + 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(
|
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)):
|
for ii in np.linspace(old_setpoint, move_val, int(updates)):
|
||||||
ttime.sleep(1 / self.update_frequency)
|
ttime.sleep(1 / self.update_frequency)
|
||||||
update_state(ii)
|
update_state(ii)
|
||||||
# Update the state of the simulated device to the final position
|
|
||||||
update_state(move_val)
|
update_state(move_val)
|
||||||
self._set_sim_state(self.motor_is_moving, 0)
|
self._set_sim_state(self.motor_is_moving, 0)
|
||||||
except DeviceStop:
|
except DeviceStop:
|
||||||
success = False
|
success = False
|
||||||
finally:
|
finally:
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
# Call function from positioner base to indicate that motion finished with success
|
|
||||||
self._done_moving(success=success)
|
self._done_moving(success=success)
|
||||||
# Set status to finished
|
self._set_sim_state(self.motor_is_moving.name, 0)
|
||||||
st.set_finished()
|
st.set_finished()
|
||||||
|
|
||||||
# Start motion in Thread
|
|
||||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If self.delay is 0, we move the simulated device instantaneously
|
|
||||||
update_state(value)
|
update_state(value)
|
||||||
self._done_moving()
|
self._done_moving()
|
||||||
|
self._set_sim_state(self.motor_is_moving.name, 0)
|
||||||
st.set_finished()
|
st.set_finished()
|
||||||
return st
|
return st
|
||||||
|
|
||||||
@ -737,7 +418,7 @@ class SimPositioner(Device, PositionerBase):
|
|||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self):
|
def position(self) -> float:
|
||||||
"""Return the current position of the simulated device."""
|
"""Return the current position of the simulated device."""
|
||||||
return self.readback.get()
|
return self.readback.get()
|
||||||
|
|
||||||
@ -747,8 +428,123 @@ class SimPositioner(Device, PositionerBase):
|
|||||||
return "mm"
|
return "mm"
|
||||||
|
|
||||||
|
|
||||||
class SynDynamicComponents(Device):
|
class SimFlyer(Device, PositionerBase, FlyerInterface):
|
||||||
messages = Dcpt({f"message{i}": (SynSignal, None, {"name": f"msg{i}"}) for i in range(1, 6)})
|
"""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):
|
class SynDeviceSubOPAAS(Device):
|
||||||
@ -761,5 +557,10 @@ class SynDeviceOPAAS(Device):
|
|||||||
z = Cpt(SynDeviceSubOPAAS, name="z")
|
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__":
|
if __name__ == "__main__":
|
||||||
gauss = SynGaussBEC(name="gauss")
|
cam = SimCamera(name="cam")
|
||||||
|
cam.image.read()
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
from abc import ABC, abstractmethod
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from prettytable import PrettyTable
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import inspect
|
||||||
import time as ttime
|
import time as ttime
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from lmfit import models, Model
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
|
|
||||||
@ -13,11 +20,11 @@ class SimulatedDataException(Exception):
|
|||||||
"""Exception raised when there is an issue with the simulated data."""
|
"""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."""
|
"""Type of simulation to steer simulated data."""
|
||||||
|
|
||||||
CONSTANT = "constant"
|
CONSTANT = "constant"
|
||||||
GAUSSIAN = "gauss"
|
GAUSSIAN = "gaussian"
|
||||||
|
|
||||||
|
|
||||||
class NoiseType(str, enum.Enum):
|
class NoiseType(str, enum.Enum):
|
||||||
@ -28,160 +35,383 @@ class NoiseType(str, enum.Enum):
|
|||||||
POISSON = "poisson"
|
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 = [
|
USER_ACCESS = [
|
||||||
"get_sim_params",
|
"sim_params",
|
||||||
"set_sim_params",
|
"sim_select_model",
|
||||||
"get_sim_type",
|
"sim_get_models",
|
||||||
"set_sim_type",
|
"sim_show_all",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None:
|
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.parent = parent
|
||||||
self.sim_state = defaultdict(lambda: {})
|
|
||||||
self._all_params = defaultdict(lambda: {})
|
|
||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self._simulation_type = None
|
self.sim_state = defaultdict(dict)
|
||||||
self.init_paramaters(**kwargs)
|
self.registered_proxies = getattr(self.parent, "registered_proxies", {})
|
||||||
self._active_params = self._all_params.get(self._simulation_type, None)
|
self._model = {}
|
||||||
|
self._model_params = None
|
||||||
|
self._params = {}
|
||||||
|
|
||||||
def init_paramaters(self, **kwargs):
|
def execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any:
|
||||||
"""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()
|
|
||||||
"""
|
"""
|
||||||
|
Execute either the provided method or reroutes the method execution
|
||||||
def get_sim_params(self) -> dict:
|
to a device proxy in case it is registered in self.parentregistered_proxies.
|
||||||
"""Return the currently parameters for the active simulation type in sim_type.
|
|
||||||
|
|
||||||
These parameters can be changed with set_sim_params.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Parameters of the currently active simulation in sim_type.
|
|
||||||
"""
|
"""
|
||||||
return self._active_params
|
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:
|
if method is not None:
|
||||||
"""Change the current set of parameters for the active simulation type.
|
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:
|
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():
|
model_cls = self.get_model_cls(model)
|
||||||
try:
|
self._model = model_cls() if callable(model_cls) else model_cls
|
||||||
if k == "noise":
|
self._params = self.get_params_for_model_cls()
|
||||||
self._active_params[k] = NoiseType(v)
|
self._params.update(self._get_additional_params())
|
||||||
else:
|
print(self._get_table_active_simulation())
|
||||||
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
|
|
||||||
|
|
||||||
def get_sim_type(self) -> SimulationType:
|
@property
|
||||||
"""Return the simulation type of the simulation.
|
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:
|
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:
|
@sim_params.setter
|
||||||
"""Set the simulation type of the simulation."""
|
def sim_params(self, params: dict):
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
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:
|
def update_sim_state(self, signal_name: str, value: any) -> None:
|
||||||
"""Update the simulated state of the device.
|
"""Update the simulated state of the device.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
signal_name (str): Name of the signal to update.
|
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]["value"] = value
|
||||||
self.sim_state[signal_name]["timestamp"] = ttime.time()
|
self.sim_state[signal_name]["timestamp"] = ttime.time()
|
||||||
|
|
||||||
def _update_init_params(self, sim_type_default: SimulationType) -> None:
|
@abstractmethod
|
||||||
"""Update the initial parameters of the simulated data with input from deviceConfig.
|
def _get_additional_params(self) -> dict:
|
||||||
|
"""Initialize the default parameters for the noise."""
|
||||||
|
|
||||||
Args:
|
@abstractmethod
|
||||||
sim_type_default (SimulationType): Default simulation type to use if not specified in deviceConfig.
|
def get_model_cls(self, model: str) -> any:
|
||||||
"""
|
"""
|
||||||
init_params = self.parent.init_sim_params
|
Method to get the class for the active simulation model_cls
|
||||||
for sim_type in self._all_params.values():
|
"""
|
||||||
for sim_type_config_element in sim_type:
|
|
||||||
if init_params:
|
@abstractmethod
|
||||||
if sim_type_config_element in init_params:
|
def get_params_for_model_cls(self) -> dict:
|
||||||
sim_type[sim_type_config_element] = init_params[sim_type_config_element]
|
"""
|
||||||
# Set simulation type to default if not specified in deviceConfig
|
Method to get the parameters for the active simulation model.
|
||||||
sim_type_select = (
|
"""
|
||||||
init_params.get("sim_type", sim_type_default) if init_params else sim_type_default
|
|
||||||
|
@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):
|
class SimulatedDataMonitor(SimulatedDataBase):
|
||||||
"""Simulated data for a monitor."""
|
"""Simulated data class for a monitor."""
|
||||||
|
|
||||||
def init_paramaters(self, **kwargs):
|
def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None:
|
||||||
"""Initialize the parameters for the simulated data
|
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
|
def _get_additional_params(self) -> None:
|
||||||
SimulationType.CONSTANT and SimulationType.GAUSSIAN.
|
params = DEFAULT_PARAMS_NOISE.copy()
|
||||||
New simulation types can be added by adding a new key to self._all_params,
|
params.update(DEFAULT_PARAMS_MOTOR.copy())
|
||||||
together with the required parameters for that simulation type. Please
|
return params
|
||||||
also complement the docstring of this method with the new simulation type.
|
|
||||||
|
|
||||||
For SimulationType.CONSTANT:
|
def _init_default(self) -> None:
|
||||||
Amp is the amplitude of the constant value.
|
"""Initialize the default parameters for the simulated data."""
|
||||||
Noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'.
|
self.sim_select_model("ConstantModel")
|
||||||
Noise multiplier is the multiplier of the noise, only relevant for uniform noise.
|
|
||||||
|
|
||||||
For SimulationType.GAUSSIAN:
|
def get_model_cls(self, model: str) -> any:
|
||||||
ref_motor is the motor that is used as reference to compute the gaussian.
|
"""Get the class for the active simulation model."""
|
||||||
amp is the amplitude of the gaussian.
|
if model not in self._model_lookup:
|
||||||
cen is the center of the gaussian.
|
raise SimulatedDataException(f"Model {model} not found in {self._model_lookup.keys()}.")
|
||||||
sig is the sigma of the gaussian.
|
return self._model_lookup[model]
|
||||||
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_all_sim_models(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
self._all_params = {
|
Method to get all names from the available simulation models from the lmfit.models pool.
|
||||||
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)
|
|
||||||
|
|
||||||
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.
|
"""Update the simulated state of the device.
|
||||||
|
|
||||||
It will update the value in self.sim_state with the value computed by
|
It will update the value in self.sim_state with the value computed by
|
||||||
@ -190,171 +420,170 @@ class SimulatedDataMonitor(SimulatedDataBase):
|
|||||||
Args:
|
Args:
|
||||||
signal_name (str): Name of the signal to update.
|
signal_name (str): Name of the signal to update.
|
||||||
"""
|
"""
|
||||||
if self.get_sim_type() == SimulationType.CONSTANT:
|
if compute_readback:
|
||||||
value = self._compute_constant()
|
method = self._compute
|
||||||
elif self.get_sim_type() == SimulationType.GAUSSIAN:
|
value = self.execute_simulation_method(method=method, signal_name=signal_name)
|
||||||
value = self._compute_gaussian()
|
value = self.bit_depth(np.max(value, 0))
|
||||||
|
self.update_sim_state(signal_name, value)
|
||||||
|
|
||||||
self.update_sim_state(signal_name, value)
|
def _compute(self, *args, **kwargs) -> int:
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
|
Compute the return value for given motor position and active model.
|
||||||
|
|
||||||
params = self._active_params
|
Returns:
|
||||||
try:
|
float: Value computed by the active model.
|
||||||
motor_pos = self.device_manager.devices[params["ref_motor"]].obj.read()[
|
"""
|
||||||
params["ref_motor"]
|
mot_name = self.sim_params["ref_motor"]
|
||||||
]["value"]
|
if self.device_manager and mot_name in self.device_manager.devices:
|
||||||
v = params["amp"] * np.exp(
|
motor_pos = self.device_manager.devices[mot_name].obj.read()[mot_name]["value"]
|
||||||
-((motor_pos - params["cen"]) ** 2) / (2 * params["sig"] ** 2)
|
else:
|
||||||
)
|
motor_pos = 0
|
||||||
if params["noise"] == NoiseType.POISSON:
|
method = self._model
|
||||||
v = np.random.poisson(np.round(v), 1)[0]
|
value = int(method.eval(params=self._model_params, x=motor_pos))
|
||||||
elif params["noise"] == NoiseType.UNIFORM:
|
return self._add_noise(value, self.sim_params["noise"], self.sim_params["noise_multiplier"])
|
||||||
v += np.random.uniform(-1, 1) * 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
|
return v
|
||||||
except SimulatedDataException as exc:
|
elif noise == NoiseType.UNIFORM:
|
||||||
# TODO Propagate msg to client!
|
noise = np.ceil(np.random.uniform(0, 1) * noise_multiplier).astype(int)
|
||||||
logger.warning(
|
v += noise * (np.random.randint(0, 2) * 2 - 1)
|
||||||
f"Could not compute gaussian for {params['ref_motor']} with {exc} raised."
|
return v if v > 0 else 0
|
||||||
"Returning 0 instead."
|
return v
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class SimulatedDataCamera(SimulatedDataBase):
|
class SimulatedDataCamera(SimulatedDataBase):
|
||||||
"""Simulated class to compute data for a 2D camera."""
|
"""Simulated class to compute data for a 2D camera."""
|
||||||
|
|
||||||
def init_paramaters(self, **kwargs):
|
def __init__(self, *args, parent=None, device_manager=None, **kwargs) -> None:
|
||||||
"""Initialize the parameters for the simulated data
|
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
|
def _init_default(self) -> None:
|
||||||
SimulationType.CONSTANT and SimulationType.GAUSSIAN.
|
"""Initialize the default model for a simulated camera
|
||||||
New simulation types can be added by adding a new key to self._all_params,
|
|
||||||
together with the required parameters for that simulation type. Please
|
|
||||||
also complement the docstring of this method with the new simulation type.
|
|
||||||
|
|
||||||
For SimulationType.CONSTANT:
|
Use the default model "Gaussian".
|
||||||
Amp is the amplitude of the constant value.
|
|
||||||
Noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'.
|
|
||||||
Noise multiplier is the multiplier of the noise, only relevant for uniform noise.
|
|
||||||
|
|
||||||
For SimulationType.GAUSSIAN:
|
|
||||||
amp is the amplitude of the gaussian.
|
|
||||||
cen_off is the pixel offset from the center of the gaussian from the center of the image.
|
|
||||||
It is passed as a numpy array.
|
|
||||||
cov is the 2D covariance matrix used to specify the shape of the gaussian.
|
|
||||||
It is a 2x2 matrix and will be passed as a numpy array.
|
|
||||||
noise is the type of noise to add to the signal. Available options are 'poisson', 'uniform' or 'none'.
|
|
||||||
noise multiplier is the multiplier of the noise, only relevant for uniform noise.
|
|
||||||
"""
|
"""
|
||||||
self._all_params = {
|
self.sim_select_model(SimulationType2D.GAUSSIAN)
|
||||||
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)
|
|
||||||
|
|
||||||
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.
|
"""Update the simulated state of the device.
|
||||||
|
|
||||||
It will update the value in self.sim_state with the value computed by
|
It will update the value in self.sim_state with the value computed by
|
||||||
the chosen simulation type.
|
the chosen simulation type.
|
||||||
|
|
||||||
Args:
|
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:
|
if compute_readback:
|
||||||
value = self._compute_constant()
|
if self._model == SimulationType2D.CONSTANT:
|
||||||
elif self.get_sim_type() == SimulationType.GAUSSIAN:
|
method = "_compute_constant"
|
||||||
value = self._compute_gaussian()
|
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)
|
self.update_sim_state(signal_name, value)
|
||||||
|
|
||||||
def _compute_constant(self) -> float:
|
def _compute_empty_image(self) -> np.ndarray:
|
||||||
"""Compute a return value for sim_type = Constant."""
|
"""Computes return value for sim_type = "empty_image".
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
Returns:
|
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]
|
def _compute_constant(self) -> np.ndarray:
|
||||||
cov_det = np.linalg.det(cov)
|
"""Compute a return value for SimulationType2D constant."""
|
||||||
cov_inv = np.linalg.inv(cov)
|
try:
|
||||||
N = np.sqrt((2 * np.pi) ** dim * cov_det)
|
shape = self.parent.image_shape.get()
|
||||||
# This einsum call calculates (x-mu)T.Sigma-1.(x-mu) in a vectorized
|
v = self.sim_params.get("amplitude") * np.ones(shape, dtype=np.float32)
|
||||||
# way across all the input variables.
|
v = self._add_noise(v, self.sim_params["noise"], self.sim_params["noise_multiplier"])
|
||||||
fac = np.einsum("...k,kl,...l->...", pos - cen_off, cov_inv, pos - cen_off)
|
return self._add_hot_pixel(
|
||||||
|
v,
|
||||||
return np.exp(-fac / 2) / N
|
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:
|
def _compute_gaussian(self) -> float:
|
||||||
"""Computes return value for sim_type = "gauss".
|
"""Computes return value for sim_type = "gauss".
|
||||||
@ -367,41 +596,121 @@ class SimulatedDataCamera(SimulatedDataBase):
|
|||||||
Returns: float
|
Returns: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = self._active_params
|
|
||||||
shape = self.sim_state[self.parent.image_shape.name]["value"]
|
|
||||||
try:
|
try:
|
||||||
X, Y = np.meshgrid(
|
amp = self.sim_params.get("amplitude")
|
||||||
np.linspace(-shape[0] / 2, shape[0] / 2, shape[0]),
|
cov = self.sim_params.get("covariance")
|
||||||
np.linspace(-shape[1] / 2, shape[1] / 2, shape[1]),
|
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(
|
v = self._compute_multivariate_gaussian(pos=pos, cen_off=offset, cov=cov, amp=amp)
|
||||||
pos=pos, cen_off=params["cen_off"], cov=params["cov"]
|
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:
|
except SimulatedDataException as exc:
|
||||||
# TODO Propagate msg to client!
|
raise SimulatedDataException(
|
||||||
logger.warning(
|
f"Could not compute gaussian for {self.parent.name} with {exc} raised. Deactivate eiger to continue."
|
||||||
f"Could not compute gaussian for {params['ref_motor']} with {exc} raised."
|
) from exc
|
||||||
"Returning 0 instead."
|
|
||||||
)
|
def _compute_multivariate_gaussian(
|
||||||
return 0
|
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
|
||||||
|
355
ophyd_devices/sim/sim_frameworks.py
Normal file
355
ophyd_devices/sim/sim_frameworks.py
Normal 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)
|
@ -1,28 +1,44 @@
|
|||||||
import time as ttime
|
import time
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
import numpy as np
|
|
||||||
from ophyd import Signal, Kind
|
from ophyd import Signal, Kind
|
||||||
from ophyd.utils import ReadOnlyError
|
from ophyd.utils import ReadOnlyError
|
||||||
|
|
||||||
|
from ophyd_devices.utils.bec_device_base import BECDeviceBase
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
# Readout precision for Setable/Readonly/ComputedReadonly signals
|
# Readout precision for Setable/ReadOnlySignal signals
|
||||||
PRECISION = 3
|
PRECISION = 3
|
||||||
|
|
||||||
|
|
||||||
class SetableSignal(Signal):
|
class SetableSignal(Signal):
|
||||||
"""Setable signal for simulated devices.
|
"""Setable signal for simulated devices.
|
||||||
|
|
||||||
It will return the value of the readback signal based on the position
|
The signal will store the value in sim_state of the SimulatedData class of the parent device.
|
||||||
created in the sim_state dictionary 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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
|
||||||
name: str,
|
name: str,
|
||||||
value: any = None,
|
*args,
|
||||||
|
value: any = 0,
|
||||||
kind: int = Kind.normal,
|
kind: int = Kind.normal,
|
||||||
precision: float = PRECISION,
|
precision: float = PRECISION,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -34,22 +50,27 @@ class SetableSignal(Signal):
|
|||||||
)
|
)
|
||||||
self._value = value
|
self._value = value
|
||||||
self.precision = precision
|
self.precision = precision
|
||||||
# Init the sim_state, if self.parent.sim available, use it, else use self.parent
|
self.sim = getattr(self.parent, "sim", None)
|
||||||
self.sim = getattr(self.parent, "sim", self.parent)
|
|
||||||
self._update_sim_state(value)
|
self._update_sim_state(value)
|
||||||
|
|
||||||
def _update_sim_state(self, value: any) -> None:
|
def _update_sim_state(self, value: any) -> None:
|
||||||
"""Update the readback value."""
|
"""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:
|
def _get_value(self) -> any:
|
||||||
"""Update the timestamp of the readback value."""
|
"""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:
|
def _get_timestamp(self) -> any:
|
||||||
"""Update the timestamp of the readback value."""
|
"""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):
|
def get(self):
|
||||||
"""Get the current position of the simulated device.
|
"""Get the current position of the simulated device.
|
||||||
|
|
||||||
@ -58,6 +79,7 @@ class SetableSignal(Signal):
|
|||||||
self._value = self._get_value()
|
self._value = self._get_value()
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
def put(self, value):
|
def put(self, value):
|
||||||
"""Put the value to the simulated device.
|
"""Put the value to the simulated device.
|
||||||
|
|
||||||
@ -83,111 +105,57 @@ class SetableSignal(Signal):
|
|||||||
|
|
||||||
|
|
||||||
class ReadOnlySignal(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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
|
||||||
name: str,
|
name: str,
|
||||||
|
*args,
|
||||||
|
parent=None,
|
||||||
value: any = 0,
|
value: any = 0,
|
||||||
kind: int = Kind.normal,
|
kind: int = Kind.normal,
|
||||||
precision: float = PRECISION,
|
precision: float = PRECISION,
|
||||||
|
compute_readback: bool = False,
|
||||||
**kwargs,
|
**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(
|
self._metadata.update(
|
||||||
connected=True,
|
connected=True,
|
||||||
write_access=False,
|
write_access=False,
|
||||||
)
|
)
|
||||||
self.precision = precision
|
|
||||||
self._value = value
|
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.sim = getattr(self.parent, "sim", None)
|
||||||
self._init_sim_state()
|
if self.sim:
|
||||||
|
self._init_sim_state()
|
||||||
|
|
||||||
def _init_sim_state(self) -> None:
|
def _init_sim_state(self) -> None:
|
||||||
"""Init the readback value and timestamp in sim_state"""
|
"""Create the initial sim_state in the SimulatedData class of the parent device."""
|
||||||
if self.sim:
|
self.sim.update_sim_state(self.name, self._value)
|
||||||
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()
|
|
||||||
|
|
||||||
def _update_sim_state(self) -> None:
|
def _update_sim_state(self) -> None:
|
||||||
"""Update the readback value.
|
"""Update the readback value."""
|
||||||
|
self.sim.compute_sim_state(signal_name=self.name, compute_readback=self.compute_readback)
|
||||||
Call _compute_sim_state in parent device which updates the sim_state.
|
|
||||||
"""
|
|
||||||
self.sim._compute_sim_state(self.name)
|
|
||||||
|
|
||||||
def _get_value(self) -> any:
|
def _get_value(self) -> any:
|
||||||
"""Update the timestamp of the readback value."""
|
"""Update the timestamp of the readback value."""
|
||||||
@ -197,15 +165,16 @@ class ComputedReadOnlySignal(Signal):
|
|||||||
"""Update the timestamp of the readback value."""
|
"""Update the timestamp of the readback value."""
|
||||||
return self.sim.sim_state[self.name]["timestamp"]
|
return self.sim.sim_state[self.name]["timestamp"]
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
def get(self):
|
def get(self):
|
||||||
"""Get the current position of the simulated device.
|
"""Get the current position of the simulated device."""
|
||||||
|
if self.sim:
|
||||||
Core function for signal.
|
self._update_sim_state()
|
||||||
"""
|
self._value = self._get_value()
|
||||||
self._update_sim_state()
|
return self._value
|
||||||
self._value = self._get_value()
|
return np.random.rand()
|
||||||
return self._value
|
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
def put(self, value) -> None:
|
def put(self, value) -> None:
|
||||||
"""Put method, should raise ReadOnlyError since the signal is readonly."""
|
"""Put method, should raise ReadOnlyError since the signal is readonly."""
|
||||||
raise ReadOnlyError(f"The signal {self.name} is readonly.")
|
raise ReadOnlyError(f"The signal {self.name} is readonly.")
|
||||||
@ -220,18 +189,138 @@ class ComputedReadOnlySignal(Signal):
|
|||||||
res[self.name]["precision"] = self.precision
|
res[self.name]["precision"] = self.precision
|
||||||
return res
|
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
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
"""Timestamp of the readback value"""
|
"""Timestamp of the readback value"""
|
||||||
return self._get_timestamp()
|
return self._get_timestamp()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
"""Read method"""
|
||||||
|
return {
|
||||||
|
self.name: {
|
||||||
|
"value": self.get(),
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def read_configuration(self):
|
||||||
from ophyd_devices.sim import SimPositioner
|
"""Read method"""
|
||||||
|
return self.read()
|
||||||
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())
|
|
||||||
|
152
ophyd_devices/sim/sim_test_devices.py
Normal file
152
ophyd_devices/sim/sim_test_devices.py
Normal 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()
|
@ -357,8 +357,8 @@ class SynXtremeOtfReplay(FlyerInterface, Device):
|
|||||||
}
|
}
|
||||||
msg = messages.DeviceMessage(
|
msg = messages.DeviceMessage(
|
||||||
signals=signals, metadata=self._device_manager.devices.otf.metadata
|
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
|
MessageEndpoints.device_readback("signals"), msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
50
ophyd_devices/sim/test.py
Normal file
50
ophyd_devices/sim/test.py
Normal 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
|
@ -3,14 +3,13 @@ import functools
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from prettytable import PrettyTable
|
||||||
from typeguard import typechecked
|
from typeguard import typechecked
|
||||||
|
|
||||||
from ophyd_devices.smaract.smaract_errors import (
|
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
|
||||||
SmaractCommunicationError,
|
|
||||||
SmaractErrorCode,
|
|
||||||
)
|
|
||||||
from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked
|
from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked
|
||||||
|
|
||||||
logger = logging.getLogger("smaract_controller")
|
logger = logging.getLogger("smaract_controller")
|
||||||
@ -73,7 +72,15 @@ class SmaractSensors:
|
|||||||
class SmaractController(Controller):
|
class SmaractController(Controller):
|
||||||
_axes_per_controller = 6
|
_axes_per_controller = 6
|
||||||
_initialized = False
|
_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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -110,14 +117,22 @@ class SmaractController(Controller):
|
|||||||
|
|
||||||
@threadlocked
|
@threadlocked
|
||||||
def socket_put_and_receive(
|
def socket_put_and_receive(
|
||||||
self,
|
self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False
|
||||||
val: str,
|
|
||||||
remove_trailing_chars=True,
|
|
||||||
check_for_errors=True,
|
|
||||||
raise_if_not_status=False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
self.socket_put(val)
|
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:
|
if remove_trailing_chars:
|
||||||
return_val = self._remove_trailing_characters(return_val)
|
return_val = self._remove_trailing_characters(return_val)
|
||||||
logger.debug(f"Sending {val}; Returned {return_val}")
|
logger.debug(f"Sending {val}; Returned {return_val}")
|
||||||
@ -175,7 +190,9 @@ class SmaractController(Controller):
|
|||||||
return bool(int(return_val.split(",")[1]))
|
return bool(int(return_val.split(",")[1]))
|
||||||
|
|
||||||
def all_axes_referenced(self) -> bool:
|
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
|
@retry_once
|
||||||
@axis_checked
|
@axis_checked
|
||||||
@ -234,19 +251,18 @@ class SmaractController(Controller):
|
|||||||
@axis_checked
|
@axis_checked
|
||||||
@typechecked
|
@typechecked
|
||||||
def move_open_loop_steps(
|
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:
|
) -> None:
|
||||||
"""Move open loop steps
|
"""Move open loop steps. It performs a burst of steps with the given parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
axis_Id_numeric (int): Axis number.
|
axis_Id_numeric (int): Axis number.
|
||||||
steps (float): Relative position to move to in mm.
|
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.
|
||||||
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.
|
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(
|
self.socket_put_and_receive(
|
||||||
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}",
|
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True
|
||||||
raise_if_not_status=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry_once
|
@retry_once
|
||||||
@ -374,6 +390,15 @@ class SmaractController(Controller):
|
|||||||
if self._message_starts_with(return_val, f":ST{axis_Id_numeric}"):
|
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},")))
|
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
|
@retry_once
|
||||||
@axis_checked
|
@axis_checked
|
||||||
def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None:
|
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:
|
def describe(self) -> None:
|
||||||
t = PrettyTable()
|
t = PrettyTable()
|
||||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||||
t.field_names = [
|
t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"]
|
||||||
"Axis",
|
for ax in range(self._axes_per_controller):
|
||||||
"Name",
|
|
||||||
"Connected",
|
|
||||||
"Referenced",
|
|
||||||
"Closed Loop Speed",
|
|
||||||
"Position",
|
|
||||||
]
|
|
||||||
for ax in range(self._Smaract_axis_per_controller):
|
|
||||||
axis = self._axis[ax]
|
axis = self._axis[ax]
|
||||||
if axis is not None:
|
if axis is not None:
|
||||||
t.add_row(
|
t.add_row(
|
||||||
@ -428,7 +446,7 @@ class SmaractController(Controller):
|
|||||||
axis.name,
|
axis.name,
|
||||||
axis.connected,
|
axis.connected,
|
||||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
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"),
|
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:
|
def _message_starts_with(self, msg: str, leading_chars: str) -> bool:
|
||||||
if msg.startswith(leading_chars):
|
if msg.startswith(leading_chars):
|
||||||
return True
|
return True
|
||||||
else:
|
raise SmaractCommunicationError(
|
||||||
raise SmaractCommunicationError(
|
f"Expected to receive a return message starting with {leading_chars} but instead"
|
||||||
f"Expected to receive a return message starting with {leading_chars} but instead received '{msg}'"
|
f" received '{msg}'"
|
||||||
)
|
)
|
||||||
|
161
ophyd_devices/utils/bec_device_base.py
Normal file
161
ophyd_devices/utils/bec_device_base.py
Normal 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
|
@ -1,4 +1,4 @@
|
|||||||
import os
|
import getpass
|
||||||
|
|
||||||
from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger
|
from bec_lib import DeviceManagerBase, messages, MessageEndpoints, bec_logger
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ class BecScaninfoMixin:
|
|||||||
messages.ScanStatusMessage: messages.ScanStatusMessage object
|
messages.ScanStatusMessage: messages.ScanStatusMessage object
|
||||||
"""
|
"""
|
||||||
if not self.sim_mode:
|
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):
|
if not isinstance(msg, messages.ScanStatusMessage):
|
||||||
return None
|
return None
|
||||||
return msg
|
return msg
|
||||||
@ -102,9 +102,13 @@ class BecScaninfoMixin:
|
|||||||
|
|
||||||
def get_username(self) -> str:
|
def get_username(self) -> str:
|
||||||
"""Get username"""
|
"""Get username"""
|
||||||
if not self.sim_mode:
|
if self.sim_mode:
|
||||||
return self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
return getpass.getuser()
|
||||||
return os.getlogin()
|
|
||||||
|
msg = self.device_manager.connector.get(MessageEndpoints.account())
|
||||||
|
if msg:
|
||||||
|
return msg
|
||||||
|
return getpass.getuser()
|
||||||
|
|
||||||
def load_scan_metadata(self) -> None:
|
def load_scan_metadata(self) -> None:
|
||||||
"""Load scan metadata
|
"""Load scan metadata
|
@ -2,12 +2,10 @@ import time
|
|||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_lib.devicemanager import DeviceContainer
|
from bec_lib.devicemanager import DeviceContainer
|
||||||
|
from ophyd import Device, Kind, Signal
|
||||||
from ophyd import Signal, Kind
|
|
||||||
|
|
||||||
from ophyd_devices.utils.socket import data_shape, data_type
|
from ophyd_devices.utils.socket import data_shape, data_type
|
||||||
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
DEFAULT_EPICSSIGNAL_VALUE = object()
|
DEFAULT_EPICSSIGNAL_VALUE = object()
|
||||||
|
|
||||||
@ -18,7 +16,7 @@ class DeviceMock:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.read_buffer = value
|
self.read_buffer = value
|
||||||
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
||||||
self._enabled_set = True
|
self._read_only = False
|
||||||
self._enabled = True
|
self._enabled = True
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
@ -28,12 +26,12 @@ class DeviceMock:
|
|||||||
return self.read_buffer
|
return self.read_buffer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled_set(self) -> bool:
|
def read_only(self) -> bool:
|
||||||
return self._enabled_set
|
return self._read_only
|
||||||
|
|
||||||
@enabled_set.setter
|
@read_only.setter
|
||||||
def enabled_set(self, val: bool):
|
def read_only(self, val: bool):
|
||||||
self._enabled_set = val
|
self._read_only = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
@ -52,7 +50,7 @@ class DeviceMock:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class ProducerMock:
|
class ConnectorMock:
|
||||||
def __init__(self, store_data=True) -> None:
|
def __init__(self, store_data=True) -> None:
|
||||||
self.message_sent = []
|
self.message_sent = []
|
||||||
self._get_buffer = {}
|
self._get_buffer = {}
|
||||||
@ -122,17 +120,17 @@ class ProducerMock:
|
|||||||
|
|
||||||
class PipelineMock:
|
class PipelineMock:
|
||||||
_pipe_buffer = []
|
_pipe_buffer = []
|
||||||
_producer = None
|
_connector = None
|
||||||
|
|
||||||
def __init__(self, producer) -> None:
|
def __init__(self, connector) -> None:
|
||||||
self._producer = producer
|
self._connector = connector
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
if not self._producer.store_data:
|
if not self._connector.store_data:
|
||||||
self._pipe_buffer = []
|
self._pipe_buffer = []
|
||||||
return []
|
return []
|
||||||
res = [
|
res = [
|
||||||
getattr(self._producer, method)(*args, **kwargs)
|
getattr(self._connector, method)(*args, **kwargs)
|
||||||
for method, args, kwargs in self._pipe_buffer
|
for method, args, kwargs in self._pipe_buffer
|
||||||
]
|
]
|
||||||
self._pipe_buffer = []
|
self._pipe_buffer = []
|
||||||
@ -142,47 +140,18 @@ class PipelineMock:
|
|||||||
class DMMock:
|
class DMMock:
|
||||||
"""Mock for DeviceManager
|
"""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):
|
def __init__(self):
|
||||||
self.devices = DeviceContainer()
|
self.devices = DeviceContainer()
|
||||||
self.producer = ProducerMock()
|
self.connector = ConnectorMock()
|
||||||
|
|
||||||
def add_device(self, name: str, value: float = 0.0):
|
def add_device(self, name: str, value: float = 0.0):
|
||||||
self.devices[name] = DeviceMock(name, value)
|
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):
|
class ConfigSignal(Signal):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -220,14 +189,7 @@ class ConfigSignal(Signal):
|
|||||||
self._readback = getattr(self.parent, self.storage_name)[self.name]
|
self._readback = getattr(self.parent, self.storage_name)[self.name]
|
||||||
return self._readback
|
return self._readback
|
||||||
|
|
||||||
def put(
|
def put(self, value, connection_timeout=1, callback=None, timeout=1, **kwargs):
|
||||||
self,
|
|
||||||
value,
|
|
||||||
connection_timeout=1,
|
|
||||||
callback=None,
|
|
||||||
timeout=1,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Using channel access, set the write PV to `value`.
|
"""Using channel access, set the write PV to `value`.
|
||||||
|
|
||||||
Keyword arguments are passed on to callbacks
|
Keyword arguments are passed on to callbacks
|
||||||
@ -253,10 +215,7 @@ class ConfigSignal(Signal):
|
|||||||
getattr(self.parent, self.storage_name)[self.name] = value
|
getattr(self.parent, self.storage_name)[self.name] = value
|
||||||
super().put(value, timestamp=timestamp, force=True)
|
super().put(value, timestamp=timestamp, force=True)
|
||||||
self._run_subs(
|
self._run_subs(
|
||||||
sub_type=self.SUB_VALUE,
|
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=timestamp
|
||||||
old_value=old_value,
|
|
||||||
value=value,
|
|
||||||
timestamp=timestamp,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
@ -285,3 +244,28 @@ class ConfigSignal(Signal):
|
|||||||
"shape": data_shape(val),
|
"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")
|
||||||
|
@ -57,10 +57,6 @@ class Controller(OphydObject):
|
|||||||
labels=None,
|
labels=None,
|
||||||
kind=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:
|
if not self._initialized:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name, attr_name=attr_name, parent=parent, labels=labels, kind=kind
|
name=name, attr_name=attr_name, parent=parent, labels=labels, kind=kind
|
||||||
@ -69,6 +65,10 @@ class Controller(OphydObject):
|
|||||||
self._axis = []
|
self._axis = []
|
||||||
self._initialize()
|
self._initialize()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
self.sock = None
|
||||||
|
self._socket_cls = socket_cls
|
||||||
|
self._socket_host = socket_host
|
||||||
|
self._socket_port = socket_port
|
||||||
|
|
||||||
def _initialize(self):
|
def _initialize(self):
|
||||||
self._connected = False
|
self._connected = False
|
||||||
@ -133,7 +133,7 @@ class Controller(OphydObject):
|
|||||||
|
|
||||||
def off(self) -> None:
|
def off(self) -> None:
|
||||||
"""Close the socket connection to the controller"""
|
"""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.sock.close()
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
140
ophyd_devices/utils/dynamic_pseudo.py
Normal file
140
ophyd_devices/utils/dynamic_pseudo.py
Normal 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
|
@ -177,18 +177,29 @@ class SocketSignal(abc.ABC, Signal):
|
|||||||
class SocketIO:
|
class SocketIO:
|
||||||
"""SocketIO helper class for TCP IP connections"""
|
"""SocketIO helper class for TCP IP connections"""
|
||||||
|
|
||||||
def __init__(self, host, port):
|
def __init__(self, host, port, max_retry=10):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
|
self.max_retry = max_retry
|
||||||
self._initialize_socket()
|
self._initialize_socket()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
print(f"connecting to {self.host} port {self.port}")
|
print(f"connecting to {self.host} port {self.port}")
|
||||||
# self.sock.create_connection((host, port))
|
# self.sock.create_connection((host, port))
|
||||||
if self.sock is None:
|
retry_count = 0
|
||||||
self._initialize_socket()
|
while True:
|
||||||
self.sock.connect((self.host, self.port))
|
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):
|
def _put(self, msg_bytes):
|
||||||
logger.debug(f"put message: {msg_bytes}")
|
logger.debug(f"put message: {msg_bytes}")
|
||||||
|
@ -7,6 +7,8 @@ import ophyd
|
|||||||
import yaml
|
import yaml
|
||||||
from bec_lib.scibec_validator import SciBecValidator
|
from bec_lib.scibec_validator import SciBecValidator
|
||||||
|
|
||||||
|
from ophyd_devices.utils.bec_device_base import BECDevice
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from bec_plugins import devices as plugin_devices
|
from bec_plugins import devices as plugin_devices
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -176,7 +178,7 @@ class StaticDeviceTest:
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
assert isinstance(obj, BECDevice)
|
||||||
assert isinstance(obj.name, str)
|
assert isinstance(obj.name, str)
|
||||||
assert isinstance(obj.read(), dict)
|
assert isinstance(obj.read(), dict)
|
||||||
assert isinstance(obj.read_configuration(), dict)
|
assert isinstance(obj.read_configuration(), dict)
|
||||||
|
@ -15,7 +15,7 @@ classifiers =
|
|||||||
package_dir =
|
package_dir =
|
||||||
= .
|
= .
|
||||||
packages = find:
|
packages = find:
|
||||||
python_requires = >=3.9
|
python_requires = >=3.10
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = .
|
where = .
|
||||||
|
4
setup.py
4
setup.py
@ -1,6 +1,6 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
__version__ = "0.19.3"
|
__version__ = "0.26.1"
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup(
|
setup(
|
||||||
@ -14,6 +14,8 @@ if __name__ == "__main__":
|
|||||||
"std_daq_client",
|
"std_daq_client",
|
||||||
"pyepics",
|
"pyepics",
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"h5py",
|
||||||
|
"hdf5plugin",
|
||||||
],
|
],
|
||||||
extras_require={"dev": ["pytest", "pytest-random-order", "black", "coverage"]},
|
extras_require={"dev": ["pytest", "pytest-random-order", "black", "coverage"]},
|
||||||
package_data={"ophyd_devices.smaract": ["smaract_sensors.json"]},
|
package_data={"ophyd_devices.smaract": ["smaract_sensors.json"]},
|
||||||
|
@ -14,6 +14,7 @@ def test_controller_off():
|
|||||||
|
|
||||||
# make sure it is indempotent
|
# make sure it is indempotent
|
||||||
controller.off()
|
controller.off()
|
||||||
|
controller._reset_controller()
|
||||||
|
|
||||||
|
|
||||||
def test_controller_on():
|
def test_controller_on():
|
||||||
@ -28,3 +29,4 @@ def test_controller_on():
|
|||||||
# make sure it is indempotent
|
# make sure it is indempotent
|
||||||
controller.on()
|
controller.on()
|
||||||
socket_cls().open.assert_called_once()
|
socket_cls().open.assert_called_once()
|
||||||
|
controller._reset_controller()
|
||||||
|
32
tests/test_dynamic_pseudo.py
Normal file
32
tests/test_dynamic_pseudo.py
Normal 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()"
|
@ -27,7 +27,7 @@ def mock_det():
|
|||||||
prefix = "X12SA-ES-EIGER9M:"
|
prefix = "X12SA-ES-EIGER9M:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
), mock.patch(
|
), mock.patch(
|
||||||
@ -50,7 +50,7 @@ def test_init():
|
|||||||
prefix = "X12SA-ES-EIGER9M:"
|
prefix = "X12SA-ES-EIGER9M:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
), mock.patch(
|
), mock.patch(
|
||||||
@ -428,24 +428,24 @@ def test_publish_file_location(mock_det, scaninfo):
|
|||||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
)
|
)
|
||||||
if scaninfo["successful"] is None:
|
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:
|
else:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
).dumps()
|
)
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
||||||
msg,
|
msg,
|
||||||
pipe=mock_det.producer.pipeline.return_value,
|
pipe=mock_det.connector.pipeline.return_value,
|
||||||
),
|
),
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.file_event(mock_det.name),
|
MessageEndpoints.file_event(mock_det.name),
|
||||||
msg,
|
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):
|
def test_stop(mock_det):
|
||||||
|
@ -27,7 +27,7 @@ def mock_det():
|
|||||||
prefix = "X12SA-SITORO:"
|
prefix = "X12SA-SITORO:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
) as filemixin, mock.patch(
|
) as filemixin, mock.patch(
|
||||||
@ -215,24 +215,24 @@ def test_publish_file_location(mock_det, scaninfo):
|
|||||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
)
|
)
|
||||||
if scaninfo["successful"] is None:
|
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:
|
else:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
).dumps()
|
)
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
||||||
msg,
|
msg,
|
||||||
pipe=mock_det.producer.pipeline.return_value,
|
pipe=mock_det.connector.pipeline.return_value,
|
||||||
),
|
),
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.file_event(mock_det.name),
|
MessageEndpoints.file_event(mock_det.name),
|
||||||
msg,
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
60
tests/test_fupr_ophyd.py
Normal file
60
tests/test_fupr_ophyd.py
Normal 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")
|
@ -3,26 +3,36 @@ from unittest import mock
|
|||||||
import pytest
|
import pytest
|
||||||
from utils import SocketMock
|
from utils import SocketMock
|
||||||
|
|
||||||
from ophyd_devices.galil.galil_ophyd import GalilMotor
|
from ophyd_devices.galil.galil_ophyd import GalilController, GalilMotor
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.fixture(scope="function")
|
||||||
"pos,msg,sign",
|
def leyey():
|
||||||
[
|
GalilController._reset_controller()
|
||||||
(1, b" -12800\n\r", 1),
|
leyey_motor = GalilMotor(
|
||||||
(-1, b" -12800\n\r", -1),
|
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_axis_get(pos, msg, sign):
|
|
||||||
leyey = GalilMotor(
|
|
||||||
"H",
|
|
||||||
name="leyey",
|
|
||||||
host="mpc2680.psi.ch",
|
|
||||||
port=8081,
|
|
||||||
sign=sign,
|
|
||||||
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.flush_buffer()
|
||||||
leyey.controller.sock.buffer_recv = msg
|
leyey.controller.sock.buffer_recv = msg
|
||||||
val = leyey.read()
|
val = leyey.read()
|
||||||
@ -44,21 +54,11 @@ def test_axis_get(pos, msg, sign):
|
|||||||
b"XQ#NEWPAR\r",
|
b"XQ#NEWPAR\r",
|
||||||
b"MG_XQ0\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):
|
def test_axis_put(leyey, 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()
|
|
||||||
leyey.controller.sock.flush_buffer()
|
leyey.controller.sock.flush_buffer()
|
||||||
leyey.controller.sock.buffer_recv = socket_get_messages
|
leyey.controller.sock.buffer_recv = socket_get_messages
|
||||||
leyey.user_setpoint.put(target_pos)
|
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_XQ2\r",
|
||||||
b"MG _LRA, _LFA\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,
|
1,
|
||||||
@ -108,27 +98,15 @@ def test_axis_put(target_pos, socket_put_messages, socket_get_messages):
|
|||||||
b"MG_XQ2\r",
|
b"MG_XQ2\r",
|
||||||
b"MG _LRB, _LFB\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):
|
def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages):
|
||||||
leyey = GalilMotor("A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock)
|
leyex.controller.sock.flush_buffer()
|
||||||
leyey.controller.on()
|
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||||
leyey.controller.sock.flush_buffer()
|
leyex.controller.drive_axis_to_limit(axis_nr, direction)
|
||||||
leyey.controller.sock.buffer_recv = socket_get_messages
|
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||||
leyey.controller.drive_axis_to_limit(axis_nr, direction)
|
|
||||||
assert leyey.controller.sock.buffer_put == socket_put_messages
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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_XQ2\r",
|
||||||
b"MG axisref[0]\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,
|
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_XQ2\r",
|
||||||
b"MG axisref[1]\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):
|
def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages):
|
||||||
leyey = GalilMotor("A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock)
|
leyex.controller.sock.flush_buffer()
|
||||||
leyey.controller.on()
|
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||||
leyey.controller.sock.flush_buffer()
|
leyex.controller.find_reference(axis_nr)
|
||||||
leyey.controller.sock.buffer_recv = socket_get_messages
|
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||||
leyey.controller.find_reference(axis_nr)
|
|
||||||
assert leyey.controller.sock.buffer_put == socket_put_messages
|
|
||||||
|
172
tests/test_galil_flomni.py
Normal file
172
tests/test_galil_flomni.py
Normal 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()
|
@ -32,7 +32,7 @@ def mock_det():
|
|||||||
prefix = "X12SA-MCS:"
|
prefix = "X12SA-MCS:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
) as filemixin, mock.patch(
|
) as filemixin, mock.patch(
|
||||||
@ -53,7 +53,7 @@ def test_init():
|
|||||||
prefix = "X12SA-ES-EIGER9M:"
|
prefix = "X12SA-ES-EIGER9M:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
), mock.patch(
|
), 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()
|
mock_det.custom_prepare._send_data_to_bec()
|
||||||
device_metadata = mock_det.scaninfo.scan_msg.metadata
|
device_metadata = mock_det.scaninfo.scan_msg.metadata
|
||||||
metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()})
|
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(
|
calls = mock.call(
|
||||||
topic=MessageEndpoints.device_async_readback(
|
topic=MessageEndpoints.device_async_readback(
|
||||||
scanID=metadata["scanID"], device=mock_det.name
|
scanID=metadata["scanID"], device=mock_det.name
|
||||||
@ -193,7 +193,7 @@ def test_send_data_to_bec(mock_det, metadata, mca_data):
|
|||||||
expire=1800,
|
expire=1800,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert mock_det.producer.xadd.call_args == calls
|
assert mock_det.connector.xadd.call_args == calls
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -28,7 +28,7 @@ def mock_det():
|
|||||||
prefix = "X12SA-ES-PILATUS300K:"
|
prefix = "X12SA-ES-PILATUS300K:"
|
||||||
sim_mode = False
|
sim_mode = False
|
||||||
dm = DMMock()
|
dm = DMMock()
|
||||||
with mock.patch.object(dm, "producer"):
|
with mock.patch.object(dm, "connector"):
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
"ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
|
||||||
), mock.patch(
|
), mock.patch(
|
||||||
@ -207,27 +207,27 @@ def test_publish_file_location(mock_det, scaninfo):
|
|||||||
file_path=scaninfo["filepath"],
|
file_path=scaninfo["filepath"],
|
||||||
done=scaninfo["done"],
|
done=scaninfo["done"],
|
||||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||||
).dumps()
|
)
|
||||||
else:
|
else:
|
||||||
msg = messages.FileMessage(
|
msg = messages.FileMessage(
|
||||||
file_path=scaninfo["filepath"],
|
file_path=scaninfo["filepath"],
|
||||||
done=scaninfo["done"],
|
done=scaninfo["done"],
|
||||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||||
successful=scaninfo["successful"],
|
successful=scaninfo["successful"],
|
||||||
).dumps()
|
)
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
||||||
msg,
|
msg,
|
||||||
pipe=mock_det.producer.pipeline.return_value,
|
pipe=mock_det.connector.pipeline.return_value,
|
||||||
),
|
),
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.file_event(mock_det.name),
|
MessageEndpoints.file_event(mock_det.name),
|
||||||
msg,
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
89
tests/test_rt_flomni.py
Normal file
89
tests/test_rt_flomni.py
Normal 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
132
tests/test_simulation.py
Normal 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)
|
@ -1,29 +1,45 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from utils import SocketMock
|
from utils import SocketMock
|
||||||
|
|
||||||
from ophyd_devices.smaract import SmaractController
|
from ophyd_devices.smaract import SmaractController
|
||||||
from ophyd_devices.smaract.smaract_controller import SmaractCommunicationMode
|
from ophyd_devices.smaract.smaract_controller import SmaractCommunicationMode
|
||||||
from ophyd_devices.smaract.smaract_errors import (
|
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
|
||||||
SmaractCommunicationError,
|
|
||||||
SmaractErrorCode,
|
|
||||||
)
|
|
||||||
from ophyd_devices.smaract.smaract_ophyd import SmaractMotor
|
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(
|
@pytest.mark.parametrize(
|
||||||
"axis,position,get_message,return_msg",
|
"axis,position,get_message,return_msg",
|
||||||
[
|
[
|
||||||
(0, 50, b":GP0\n", b":P0,50000000"),
|
(0, 50, b":GP0\n", b":P0,50000000\n"),
|
||||||
(1, 0, b":GP1\n", b":P1,0"),
|
(1, 0, b":GP1\n", b":P1,0\n"),
|
||||||
(0, -50, b":GP0\n", b":P0,-50000000"),
|
(0, -50, b":GP0\n", b":P0,-50000000\n"),
|
||||||
(0, -50.23, b":GP0\n", b":P0,-50230000"),
|
(0, -50.23, b":GP0\n", b":P0,-50230000\n"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_position(axis, position, get_message, return_msg):
|
def test_get_position(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
val = controller.get_position(axis)
|
val = controller.get_position(axis)
|
||||||
assert val == position
|
assert val == position
|
||||||
@ -33,17 +49,13 @@ def test_get_position(axis, position, get_message, return_msg):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"axis,is_referenced,get_message,return_msg,exception",
|
"axis,is_referenced,get_message,return_msg,exception",
|
||||||
[
|
[
|
||||||
(0, True, b":GPPK0\n", b":PPK0,1", None),
|
(0, True, b":GPPK0\n", b":PPK0,1\n", None),
|
||||||
(1, True, b":GPPK1\n", b":PPK1,1", None),
|
(1, True, b":GPPK1\n", b":PPK1,1\n", None),
|
||||||
(0, False, b":GPPK0\n", b":PPK0,0", None),
|
(0, False, b":GPPK0\n", b":PPK0,0\n", None),
|
||||||
(200, False, b":GPPK0\n", b":PPK0,0", ValueError),
|
(200, False, b":GPPK0\n", b":PPK0,0\n", ValueError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, exception):
|
def test_axis_is_referenced(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
if exception is not None:
|
if exception is not None:
|
||||||
with pytest.raises(exception):
|
with pytest.raises(exception):
|
||||||
@ -57,17 +69,13 @@ def test_axis_is_referenced(axis, is_referenced, get_message, return_msg, except
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"return_msg,exception,raised",
|
"return_msg,exception,raised",
|
||||||
[
|
[
|
||||||
(b"false", SmaractCommunicationError, False),
|
(b"false\n", SmaractCommunicationError, False),
|
||||||
(b":E0,1", SmaractErrorCode, True),
|
(b":E0,1", SmaractErrorCode, True),
|
||||||
(b":E,1", SmaractCommunicationError, True),
|
(b":E,1", SmaractCommunicationError, True),
|
||||||
(b":E,-1", SmaractCommunicationError, True),
|
(b":E,-1", SmaractCommunicationError, True),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_socket_put_and_receive_raises_exception(return_msg, exception, raised):
|
def test_socket_put_and_receive_raises_exception(controller, return_msg, exception, raised):
|
||||||
SmaractController._reset_controller()
|
|
||||||
controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123)
|
|
||||||
controller.on()
|
|
||||||
controller.sock.flush_buffer()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
with pytest.raises(exception):
|
with pytest.raises(exception):
|
||||||
controller.socket_put_and_receive(b"test", raise_if_not_status=True)
|
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):
|
with pytest.raises(exception):
|
||||||
controller.socket_put_and_receive(b"test")
|
controller.socket_put_and_receive(b"test")
|
||||||
else:
|
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(
|
@pytest.mark.parametrize(
|
||||||
"mode,get_message,return_msg",
|
"mode,get_message,return_msg", [(0, b":GCM\n", b":CM0\n"), (1, b":GCM\n", b":CM1\n")]
|
||||||
[
|
|
||||||
(0, b":GCM\n", b":CM0"),
|
|
||||||
(1, b":GCM\n", b":CM1"),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
def test_communication_mode(mode, get_message, return_msg):
|
def test_communication_mode(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
val = controller.get_communication_mode()
|
val = controller.get_communication_mode()
|
||||||
assert controller.sock.buffer_put[0] == get_message
|
assert controller.sock.buffer_put[0] == get_message
|
||||||
@ -103,23 +103,19 @@ def test_communication_mode(mode, get_message, return_msg):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_moving,get_message,return_msg",
|
"is_moving,get_message,return_msg",
|
||||||
[
|
[
|
||||||
(0, b":GS0\n", b":S0,0"),
|
(0, b":GS0\n", b":S0,0\n"),
|
||||||
(1, b":GS0\n", b":S0,1"),
|
(1, b":GS0\n", b":S0,1\n"),
|
||||||
(1, b":GS0\n", b":S0,2"),
|
(1, b":GS0\n", b":S0,2\n"),
|
||||||
(0, b":GS0\n", b":S0,3"),
|
(0, b":GS0\n", b":S0,3\n"),
|
||||||
(1, b":GS0\n", b":S0,4"),
|
(1, b":GS0\n", b":S0,4\n"),
|
||||||
(0, b":GS0\n", b":S0,5"),
|
(0, b":GS0\n", b":S0,5\n"),
|
||||||
(0, b":GS0\n", b":S0,6"),
|
(0, b":GS0\n", b":S0,6\n"),
|
||||||
(1, b":GS0\n", b":S0,7"),
|
(1, b":GS0\n", b":S0,7\n"),
|
||||||
(0, b":GS0\n", b":S0,9"),
|
(0, b":GS0\n", b":S0,9\n"),
|
||||||
(0, [b":GS0\n", b":GS0\n"], [b":E0,0", b":S0,9"]),
|
(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):
|
def test_axis_is_moving(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
val = controller.is_axis_moving(0)
|
val = controller.is_axis_moving(0)
|
||||||
assert val == is_moving
|
assert val == is_moving
|
||||||
@ -131,16 +127,12 @@ def test_axis_is_moving(is_moving, get_message, return_msg):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"sensor_id,axis,get_msg,return_msg",
|
"sensor_id,axis,get_msg,return_msg",
|
||||||
[
|
[
|
||||||
(1, 0, b":GST0\n", b":ST0,1"),
|
(1, 0, b":GST0\n", b":ST0,1\n"),
|
||||||
(6, 0, b":GST0\n", b":ST0,6"),
|
(6, 0, b":GST0\n", b":ST0,6\n"),
|
||||||
(6, 1, b":GST1\n", b":ST1,6"),
|
(6, 1, b":GST1\n", b":ST1,6\n"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_sensor_definition(sensor_id, axis, get_msg, return_msg):
|
def test_get_sensor_definition(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
sensor = controller.get_sensor_type(axis)
|
sensor = controller.get_sensor_type(axis)
|
||||||
assert sensor.type_code == sensor_id
|
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"),
|
(20.23, 1, b":SCLS1,20230000\n", b":E-1,0"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_set_move_speed(move_speed, axis, get_msg, return_msg):
|
def test_set_move_speed(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
controller.set_closed_loop_move_speed(axis, move_speed)
|
controller.set_closed_loop_move_speed(axis, move_speed)
|
||||||
assert controller.sock.buffer_put[0] == get_msg
|
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"),
|
(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):
|
def test_move_axis_to_absolute_position(controller, 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()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
if hold_time is not None:
|
if hold_time is not None:
|
||||||
controller.move_axis_to_absolute_position(axis, pos, hold_time=hold_time)
|
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,
|
50,
|
||||||
[b":GPPK0\n", b":MPA0,50000000,1000\n", b":GS0\n", b":GP0\n"],
|
[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,
|
0,
|
||||||
[b":GPPK0\n", b":MPA0,0,1000\n", b":GS0\n", b":GP0\n"],
|
[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,
|
20.23,
|
||||||
[b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"],
|
[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,
|
20.23,
|
||||||
[b":GPPK0\n", b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"],
|
[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):
|
def test_move_axis(lsmarA, 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()
|
|
||||||
controller = lsmarA.controller
|
controller = lsmarA.controller
|
||||||
controller.sock.flush_buffer()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
lsmarA.move(pos)
|
lsmarA.move(pos)
|
||||||
assert controller.sock.buffer_put == get_msg
|
assert controller.sock.buffer_put == get_msg
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])])
|
||||||
"num_axes,get_msg,return_msg",
|
def test_stop_axis(lsmarA, 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()
|
|
||||||
controller = lsmarA.controller
|
controller = lsmarA.controller
|
||||||
controller.on()
|
|
||||||
controller.sock.flush_buffer()
|
|
||||||
controller.sock.buffer_recv = return_msg
|
controller.sock.buffer_recv = return_msg
|
||||||
controller.stop_all_axes()
|
controller.stop_all_axes()
|
||||||
assert controller.sock.buffer_put == get_msg
|
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)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from bec_lib.devicemanager import DeviceContainer
|
|
||||||
from bec_lib.tests.utils import ProducerMock
|
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from bec_lib.devicemanager import DeviceContainer
|
||||||
|
from bec_lib.tests.utils import ConnectorMock
|
||||||
|
|
||||||
|
|
||||||
class SocketMock:
|
class SocketMock:
|
||||||
"""Socket Mock. Used for testing"""
|
"""Socket Mock. Used for testing"""
|
||||||
@ -251,7 +251,7 @@ class DeviceMock:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.read_buffer = value
|
self.read_buffer = value
|
||||||
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
||||||
self._enabled_set = True
|
self._read_only = False
|
||||||
self._enabled = True
|
self._enabled = True
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
@ -263,14 +263,14 @@ class DeviceMock:
|
|||||||
return self.read_buffer
|
return self.read_buffer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled_set(self) -> bool:
|
def read_only(self) -> bool:
|
||||||
"""enabled_set property"""
|
"""read only property"""
|
||||||
return self._enabled_set
|
return self._read_only
|
||||||
|
|
||||||
@enabled_set.setter
|
@read_only.setter
|
||||||
def enabled_set(self, val: bool):
|
def read_only(self, val: bool):
|
||||||
"""enabled_set setter"""
|
"""read only setter"""
|
||||||
self._enabled_set = val
|
self._read_only = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
@ -296,13 +296,13 @@ class DeviceMock:
|
|||||||
class DMMock:
|
class DMMock:
|
||||||
"""Mock for DeviceManager
|
"""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):
|
def __init__(self):
|
||||||
self.devices = DeviceContainer()
|
self.devices = DeviceContainer()
|
||||||
self.producer = ProducerMock()
|
self.connector = ConnectorMock()
|
||||||
|
|
||||||
def add_device(self, name: str, value: float = 0.0):
|
def add_device(self, name: str, value: float = 0.0):
|
||||||
"""Add device to the DeviceManagerMock"""
|
"""Add device to the DeviceManagerMock"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user