Compare commits

..

1 Commits

Author SHA1 Message Date
cc2e0ecaa3 refactor: converted to new plugin structure 2024-03-10 21:09:06 +01:00
133 changed files with 12523 additions and 22266 deletions

View File

@@ -1,3 +0,0 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
semantic-release changelog -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__
semantic-release version -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__

View File

@@ -1,3 +1,2 @@
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM)
git add $(git diff --cached --name-only --diff-filter=ACM)

3
.gitignore vendored
View File

@@ -8,9 +8,6 @@
**/.pytest_cache
**/*.egg*
# recovery_config files
recovery_config_*
# file writer data
**.h5

View File

@@ -1,105 +0,0 @@
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
include:
- template: Security/Secret-Detection.gitlab-ci.yml
- project: "bec/awi_utils"
file: "awi_utils/test_pkg_versions/.gitlab-ci-job-check-pkg.yml"
#commands to run in the Docker container before starting each job.
before_script:
- pip install -e .[dev]
# different stages in the pipeline
stages:
- Formatter
- test # must be called test for security/secret-detection to work
- AdditionalTests
- Deploy
pylint:
stage: Formatter
script:
- pip install pylint pylint-exit anybadge
- pip install -e .[dev]
- mkdir ./pylint
- pylint ./csaxs_bec --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $?
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
- anybadge --label=Pylint --file=pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green
- echo "Pylint score is $PYLINT_SCORE"
artifacts:
paths:
- ./pylint/
expire_in: 1 week
pylint-check:
stage: Formatter
needs: []
allow_failure: true
before_script:
- pip install pylint pylint-exit anybadge
- apt-get update
- apt-get install -y bc
script:
# Identify changed Python files
- if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse $CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
CHANGED_FILES=$(git diff --name-only $SOURCE_BRANCH_COMMIT_SHA $TARGET_BRANCH_COMMIT_SHA | grep '\.py$' || true);
else
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true);
fi
- if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi
# Run pylint only on changed files
- mkdir ./pylint
- pylint $CHANGED_FILES --output-format=text . | tee ./pylint/pylint_changed_files.log || pylint-exit $?
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log)
- echo "Pylint score is $PYLINT_SCORE"
# Fail the job if the pylint score is below 9
- if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi
artifacts:
paths:
- ./pylint/
expire_in: 1 week
secret_detection:
before_script:
- ""
config_test:
stage: test
script:
- ophyd_test --config ./csaxs_bec/device_configs/ --output ./config_tests
artifacts:
paths:
- ./config_tests
when: on_failure
expire_in: "30 days"
allow_failure: true
pytest:
stage: test
script:
- pip install coverage
- coverage run --source=./csaxs_bec -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
- coverage report
- coverage xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml

View File

@@ -6,12 +6,15 @@ You might want to run cSAXS copy scripts before in case you want to have the for
## Overview
- Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.)
- Start Epics iocs
- Start BEC, BEC server and load/modify the device config with relevant hardware
- BEC commands
1. Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.)
2. Install BEC
3. Start Epics iocs
4. Start BEC, BEC server and load/modify the device config with relevant hardware
5. BEC commands
6. Start BEC widgets (GUI for motor control, eiger live plot)
7. Troubleshooting and common problems
## Clone cSAXS BEC repository
## 1. Clone cSAXS BEC repository
Clone the current cSAXS BEC repository from GIT into the new e-account.
Create directory
@@ -21,9 +24,19 @@ cd ~/Data10/software
```
Clone repository
```bash
git clone https://gitlab.psi.ch/bec/csaxs_bec.git
git clone https://gitlab.psi.ch/bec/csaxs-bec.git
```
## Start epics iocs
## 2. Install BEC
There is a bash sript in the followin directory.
Go to the directory and run the script on pc15543 logged in as the e-account (BEC server):
```bash
ssh pc15543
cd ~/Data10/software/csaxs-bec/bin/
./setup_bec.sh
```
## 3. Start epics iocs
You can start up the iocs while the *./setup_bec.sh* script is running. Be aware though that the scripts requires you to interact with it.
@@ -81,7 +94,7 @@ iocsh -7.0.6 startup.script
```
Be aware -7.0.6 is referring to the current epics version and might change in future (SLS 2.0)
## Start BEC, BEC server and load device config
## 4. Start BEC, BEC server and load device config
Step 1 needs to have finished for continuing with these steps.
What remains now is to start the bec server. Connect to pc15543 and open a new terminal to run:
@@ -114,7 +127,7 @@ bec.config.save_current_session('~/Data10/software/current_config.yaml')
```
The second command is helpful if you adjust limits of motors, which will then be stored in the config and loaded if a reload of the configuration is needed.
## BEC commands
## 5. BEC commands
A number of commands that are useful:
@@ -134,3 +147,41 @@ scans.line_scan(dev.samx, -1, 1, dev.samy, -1, 1, steps=20, exp_time=0.5, readou
scans.sgalil_grid(start_y = , end_y = , interval_y = , start_x=, end_x=, interval_x =, exp_time=0.5, readout_time=3e-3, relative=True)
```
## 6. Start BEC widgets (GUI for motor control, eiger live plot)
To start the BEC widgets, the first step is to make the bec_widgets_venv using the start startup script.
Follow the commands below:
``` bash
cd ~/Data10/software/csaxs-bec/bin
./setup_bec_widgets.sh
```
Afterwards, activate the environment on either cons-01 comp-1/2
``` bash
cd ~/Data10/software/
source activate bec_widgets_venv/bin/activate
```
Each Plot needs their own shell with activate environment
1. Eiger Plot
``` bash
cd ~/Data10/software/bec-widgets/bec_widgets/examples/eiger_plot
python eiger_plot.py
```
2. Motor Controller
``` bash
cd ~/Data10/software/bec-widgets/bec_widgets/examples/motor_movement
python motor_example.py --config csaxs_config.yaml
```
## 7. Troubleshooting and common problems
Sometimes the data backend for the Eiger gets stuck or misses frames, this will result in an error
``` python
raise EigerTimeoutError(
ophyd_devices.epics.devices.eiger9m_csaxs.EigerTimeoutError: Reached timeout with detector state 1, std_daq state FINISHED and received frames of 100 for the file writer)
```
This happens more likely after CTRL C of a scan. To recover from this more reliably, perform the an acquisition in burst mode with 100 frames, little exposure until no error message is raised after. This can be up to 3 times from former experience.
``` bash
scans.acquire(exp_time=0.02, frames_per_trigger=100, readout_time= 3e-3)
```
Afterwards, you should be good to continue with 2D gridscans.

1
bec_plugins/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .bec_client import *

View File

@@ -0,0 +1 @@
from .plugins import *

View File

@@ -0,0 +1,245 @@
from bec_lib.devicemanager import Device
from bec_lib.scan_report import ScanReport
# pylint:disable=undefined-variable
# pylint: disable=too-many-arguments
def dscan(
motor1: Device, m1_from: float, m1_to: float, steps: int, exp_time: float, **kwargs
) -> ScanReport:
"""Relative line scan with one device.
Args:
motor1 (Device): Device that should be scanned.
m1_from (float): Start position relative to the current position.
m1_to (float): End position relative to the current position.
steps (int): Number of steps.
exp_time (float): Exposure time.
Returns:
ScanReport: Status object.
Examples:
>>> dscan(dev.motor1, -5, 5, 10, 0.1)
"""
return scans.line_scan(
motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=True, **kwargs
)
def d2scan(
motor1: Device,
m1_from: float,
m1_to: float,
motor2: Device,
m2_from: float,
m2_to: float,
steps: int,
exp_time: float,
**kwargs
) -> ScanReport:
"""Relative line scan with two devices.
Args:
motor1 (Device): First device that should be scanned.
m1_from (float): Start position of the first device relative to its current position.
m1_to (float): End position of the first device relative to its current position.
motor2 (Device): Second device that should be scanned.
m2_from (float): Start position of the second device relative to its current position.
m2_to (float): End position of the second device relative to its current position.
steps (int): Number of steps.
exp_time (float): Exposure time
Returns:
ScanReport: Status object.
Examples:
>>> d2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1)
"""
return scans.line_scan(
motor1,
m1_from,
m1_to,
motor2,
m2_from,
m2_to,
steps=steps,
exp_time=exp_time,
relative=True,
**kwargs
)
def ascan(motor1, m1_from, m1_to, steps, exp_time, **kwargs):
"""Absolute line scan with one device.
Args:
motor1 (Device): Device that should be scanned.
m1_from (float): Start position.
m1_to (float): End position.
steps (int): Number of steps.
exp_time (float): Exposure time.
Returns:
ScanReport: Status object.
Examples:
>>> ascan(dev.motor1, -5, 5, 10, 0.1)
"""
return scans.line_scan(
motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=False, **kwargs
)
def a2scan(motor1, m1_from, m1_to, motor2, m2_from, m2_to, steps, exp_time, **kwargs):
"""Absolute line scan with two devices.
Args:
motor1 (Device): First device that should be scanned.
m1_from (float): Start position of the first device.
m1_to (float): End position of the first device.
motor2 (Device): Second device that should be scanned.
m2_from (float): Start position of the second device.
m2_to (float): End position of the second device.
steps (int): Number of steps.
exp_time (float): Exposure time
Returns:
ScanReport: Status object.
Examples:
>>> a2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1)
"""
return scans.line_scan(
motor1,
m1_from,
m1_to,
motor2,
m2_from,
m2_to,
steps=steps,
exp_time=exp_time,
relative=False,
**kwargs
)
def dmesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs):
"""Relative mesh scan (grid scan) with two devices.
Args:
motor1 (Device): First device that should be scanned.
m1_from (float): Start position of the first device relative to its current position.
m1_to (float): End position of the first device relative to its current position.
m1_steps (int): Number of steps for motor1.
motor2 (Device): Second device that should be scanned.
m2_from (float): Start position of the second device relative to its current position.
m2_to (float): End position of the second device relative to its current position.
m2_steps (int): Number of steps for motor2.
exp_time (float): Exposure time
Returns:
ScanReport: Status object.
Examples:
>>> dmesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1)
"""
return scans.grid_scan(
motor1,
m1_from,
m1_to,
m1_steps,
motor2,
m2_from,
m2_to,
m2_steps,
exp_time=exp_time,
relative=True,
)
def amesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs):
"""Absolute mesh scan (grid scan) with two devices.
Args:
motor1 (Device): First device that should be scanned.
m1_from (float): Start position of the first device.
m1_to (float): End position of the first device.
m1_steps (int): Number of steps for motor1.
motor2 (Device): Second device that should be scanned.
m2_from (float): Start position of the second device.
m2_to (float): End position of the second device.
m2_steps (int): Number of steps for motor2.
exp_time (float): Exposure time
Returns:
ScanReport: Status object.
Examples:
>>> amesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1)
"""
return scans.grid_scan(
motor1,
m1_from,
m1_to,
m1_steps,
motor2,
m2_from,
m2_to,
m2_steps,
exp_time=exp_time,
relative=False,
)
def umv(*args) -> ScanReport:
"""Updated absolute move (i.e. blocking) for one or more devices.
Returns:
ScanReport: Status object.
Examples:
>>> umv(dev.samx, 1)
>>> umv(dev.samx, 1, dev.samy, 2)
"""
return scans.umv(*args, relative=False)
def umvr(*args) -> ScanReport:
"""Updated relative move (i.e. blocking) for one or more devices.
Returns:
ScanReport: Status object.
Examples:
>>> umvr(dev.samx, 1)
>>> umvr(dev.samx, 1, dev.samy, 2)
"""
return scans.umv(*args, relative=True)
def mv(*args) -> ScanReport:
"""Absolute move for one or more devices.
Returns:
ScanReport: Status object.
Examples:
>>> mv(dev.samx, 1)
>>> mv(dev.samx, 1, dev.samy, 2)
"""
return scans.mv(*args, relative=False)
def mvr(*args) -> ScanReport:
"""Relative move for one or more devices.
Returns:
ScanReport: Status object.
Examples:
>>> mvr(dev.samx, 1)
>>> mvr(dev.samx, 1, dev.samy, 2)
"""
return scans.mv(*args, relative=True)

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,2 @@
from .load_additional_correction import lamni_read_additional_correction
from .x_ray_eye_align import LamNI, XrayEyeAlign, MagLamNI, DataDrivenLamNI

View File

@@ -0,0 +1,269 @@
def bl_show_all '{
local gap
printf("beamline status at %s:\n",date())
if (!_bl_hall_temperature_ok()) {
bl_hall_temperature
printf("\n")
}
if (_bl_sls_status_unusual()) {
bl_sls_status
} else {
bl_ring_current
}
bl_chk_beam _show
printf("\n")
printf("U19 ID gap : ",gap)
gap = _id_get_gap_mm()
if (gap >= 8) {
text_bf
}
printf("%.3f mm\n",gap)
text_non_bf
if (!_id_loss_rate_ok()) {
id_loss_rate
}
bl_shutter_status
if (_bl_cvd_filter_open()) {
text_bf
printf("CVD diamond filter : open / out\n")
text_non_bf
}
if (!_bl_xbox_valve_es1_open()) {
bl_xbox_valve_es1 _show
}
if (_bl_ln2_non_standard()) {
text_bf
printf("\nNon standard liquid nitrogen cooling-warning parameters occur. Please report this to your local contact.\n")
text_non_bf
printf("The macro bl_ln2_warn can be used to control this e-mail warning feature.\n")
bl_ln2_warn "show"
printf("\n")
}
printf("\n")
bl_flight_tube_pressure
printf("\n")
bl_attended _show
_bl_check_alarm_records(1,1)
printf("\n")
bl_op_msg
}'
def _bl_hall_temperature_ok() '{
local temp_ok
local stat
temp_ok = 1
# EH T02 average temperature
stat = epics_get("ILUUL-02AV:TEMP")
if ((stat < 23.0) || (stat > 26.0)) {
temp_ok = 0
}
# EH T02 temperature at T0204 axis 16
stat = epics_get("ILUUL-0200-EB104:TEMP")
if ((stat < 23.0) || (stat > 26.0)) {
temp_ok = 0
}
# EH T02 temperature at T0205 axis 18
stat = epics_get("ILUUL-0200-EB105:TEMP")
if ((stat < 23.0) || (stat > 26.0)) {
temp_ok = 0
}
return (temp_ok)
}'
# ----------------------------------------------------------------------
def bl_hall_temperature '{
local stat
stat = epics_get("ILUUL-02AV:TEMP")
printf("hall T02 average temperature : ")
if ((stat < 23.0) || (stat > 25.0)) {
text_bf
}
printf("%.2f deg.C\n",stat)
text_non_bf
stat = epics_get("ILUUL-0200-EB104:TEMP")
printf("hall temperature at T0204 axis 16 : ")
if ((stat < 23) || (stat > 25)) {
text_bf
}
printf("%.2f deg.C\n",stat)
text_non_bf
stat = epics_get("ILUUL-0200-EB105:TEMP")
printf("hall temperature at T0205 axis 18 : ")
if ((stat < 23) || (stat > 25)) {
text_bf
}
printf("%.2f deg.C\n",stat)
text_non_bf
# stat = epics_get("ILUUL-0300-EB102:TEMP")
# printf("EH T03 temperature at T0302 axis 21: ")
# if ((stat < 23) || (stat > 25)) {
# text_bf
# }
# printf("%.2f deg.C\n",stat)
# text_non_bf
}'
def _bl_sls_status_unusual() '{
local unusual
local stat
unusual = 0
stat = epics_get("X12SA-SR-VAC:SETPOINT")
if (stat != "OK") {
unusual = 1
}
stat = epics_get("ACOAU-ACCU:OP-MODE.VAL")
if ((stat != "Light Available") && (stat != "Light-Available")) {
unusual = 1
}
stat = epics_get("ALIRF-GUN:INJ-MODE")
if (stat != "TOP-UP") {
unusual = 1
}
# current threshold
stat = epics_get("ALIRF-GUN:CUR-LOWLIM")
if (stat < 350) {
unusual = 1
}
# current deadband
stat = epics_get("ALIRF-GUN:CUR-DBAND")
if (stat > 2) {
unusual = 1
}
# orbit feedback mode
stat = epics_get("ARIDI-BPM:OFB-MODE")
if (stat != "fast") {
unusual = 1
}
# fast orbit feedback
stat = epics_get("ARIDI-BPM:FOFBSTATUS-G")
if (stat != "running") {
unusual = 1
}
return(unusual)
}'
def bl_sls_status '{
local stat
stat = epics_get("ACOAU-ACCU:OP-MODE.VAL")
printf("SLS status : ")
if ((stat != "Light Available") && (stat != "Light-Available")) {
text_bf
}
printf("%s\n",stat)
text_non_bf
stat = epics_get("ALIRF-GUN:INJ-MODE")
printf("SLS injection mode : ")
if (stat != "TOP-UP") {
text_bf
}
printf("%s\n",stat)
text_non_bf
stat = epics_get("ALIRF-GUN:CUR-LOWLIM")
printf("SLS current threshold : ")
if (stat < 350) {
text_bf
}
printf("%7.3f\n",stat)
text_non_bf
stat = epics_get("ALIRF-GUN:CUR-DBAND")
printf("SLS current deadband : ")
if (stat > 2) {
text_bf
}
printf("%7.3f\n",stat)
text_non_bf
stat = epics_get("ACORF-FILL:PAT-SELECT")
printf("SLS filling pattern : ")
printf("%s\n",stat)
bl_ring_current
stat = epics_get("ARIDI-PCT:TAU-HOUR")
printf("SLS filling life time : ")
printf("%.2f h\n",stat)
stat = epics_get("ARIDI-BPM:OFB-MODE")
printf("orbit feedback mode : ")
if (stat != "fast") {
text_bf
}
printf("%s\n",stat)
text_non_bf
stat = epics_get("ARIDI-BPM:FOFBSTATUS-G")
printf("fast orbit feedback : ")
if (stat != "running") {
text_bf
}
printf("%s\n",stat)
text_non_bf
}'
def _bl_get_ring_current() '{
return epics_get("ARIDI-PCT:CURRENT")
}'
# ----------------------------------------------------------------------
def _bl_no_ring_current() '{
# set an arbitrary current limit of 100mA as no-beam limit
if (_bl_get_ring_current() < 100) {
return 1
} else {
return 0
}
}'
# ----------------------------------------------------------------------
def bl_ring_current '{
local curr
curr = _bl_get_ring_current()
if (curr < 300) {
text_bf
}
printf("SLS ring current : %.3f mA\n",curr)
text_non_bf
}'

View File

@@ -5,7 +5,7 @@ from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose
# import builtins to avoid linter errors
dev = builtins.__dict__.get("dev")

View File

@@ -1,6 +1,7 @@
def lamni_read_additional_correction():
# "additional_correction_shift"
# [0][] x , [1][] y, [2][] angle, [3][0] number of elements
import numpy as np
with open("correction_lamni_um_S01405_.txt", "r") as f:
num_elements = f.readline()

View File

@@ -9,13 +9,12 @@ from pathlib import Path
import h5py
import numpy as np
from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen
from bec_lib import bec_logger
from bec_lib.alarm_handler import AlarmBase
from bec_lib.pdf_writer import PDFWriter
from typeguard import typechecked
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
from .lamni_optics_mixin import LamNIOpticsMixin
logger = bec_logger.logger
@@ -189,8 +188,7 @@ class XrayEyeAlign:
val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm
self.alignment_values[k] = [val_x, val_y]
print(
f"Clicked position {k}: x {self.alignment_values[k][0]}, y"
f" {self.alignment_values[k][1]}"
f"Clicked position {k}: x {self.alignment_values[k][0]}, y {self.alignment_values[k][1]}"
)
if k == 0: # received center value of FZP
@@ -215,10 +213,7 @@ class XrayEyeAlign:
elif (
k == 1
): # received sample center value at samroy 0 ie the final base shift values
msg = (
f"Base shift values from movement are x {self.shift_xy[0]}, y"
f" {self.shift_xy[1]}"
)
msg = f"Base shift values from movement are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
print(msg)
logger.info(msg)
self.shift_xy[0] += (
@@ -228,12 +223,13 @@ class XrayEyeAlign:
self.alignment_values[1][1] - self.alignment_values[0][1]
) * 1000
print(
"Base shift values from movement and clicked position are x"
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
f"Base shift values from movement and clicked position are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
)
self.scans.lamni_move_to_scan_center(
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
self.shift_xy[0] / 1000,
self.shift_xy[1] / 1000,
self.get_tomo_angle(),
).wait()
self.send_message("please wait ...")
@@ -242,7 +238,9 @@ class XrayEyeAlign:
time.sleep(1)
self.scans.lamni_move_to_scan_center(
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
self.shift_xy[0] / 1000,
self.shift_xy[1] / 1000,
self.get_tomo_angle(),
).wait()
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
@@ -259,12 +257,16 @@ class XrayEyeAlign:
self._disable_rt_feedback()
self.tomo_rotate((k - 1) * 45 - 45 / 2)
self.scans.lamni_move_to_scan_center(
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
self.shift_xy[0] / 1000,
self.shift_xy[1] / 1000,
self.get_tomo_angle(),
).wait()
self._disable_rt_feedback()
self.tomo_rotate((k - 1) * 45)
self.scans.lamni_move_to_scan_center(
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
self.shift_xy[0] / 1000,
self.shift_xy[1] / 1000,
self.get_tomo_angle(),
).wait()
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
@@ -291,7 +293,9 @@ class XrayEyeAlign:
self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx
self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy
self.scans.lamni_move_to_scan_center(
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
self.shift_xy[0] / 1000,
self.shift_xy[1] / 1000,
self.get_tomo_angle(),
).wait()
print(
f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}"
@@ -306,14 +310,12 @@ class XrayEyeAlign:
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
print(
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
f" = {fovy:.0f} microns"
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy = {fovy:.0f} microns"
)
print("Use matlab routine to fit the current alignment...")
print(
"This additional shift is applied to the base shift values\n which are x"
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
f"This additional shift is applied to the base shift values\n which are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
)
self._disable_rt_feedback()
@@ -321,8 +323,7 @@ class XrayEyeAlign:
self.tomo_rotate(0)
print(
"\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running"
" lamni.align.read_xray_eye_correction()\n"
"\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running lamni.align.read_xray_eye_correction()\n"
)
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
@@ -331,13 +332,12 @@ class XrayEyeAlign:
with open(
os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w"
) as alignment_values_file:
alignment_values_file.write("angle\thorizontal\tvertical\n")
alignment_values_file.write(f"angle\thorizontal\tvertical\n")
for k in range(2, 11):
fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000
fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000
print(
f"Writing to file new alignment: number {k}, value x {fovx_offset}, y"
f" {fovy_offset}"
f"Writing to file new alignment: number {k}, value x {fovx_offset}, y {fovy_offset}"
)
alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n")
@@ -422,8 +422,7 @@ class XrayEyeAlign:
additional_correction_shift_x = self.corr_pos_x[-1]
additional_correction_shift_y = self.corr_pos_y[-1]
print(
"Additional correction shifts:"
f" {additional_correction_shift_x} {additional_correction_shift_y}"
f"Additional correction shifts: {additional_correction_shift_x} {additional_correction_shift_y}"
)
return (additional_correction_shift_x, additional_correction_shift_y)
@@ -477,8 +476,7 @@ class XrayEyeAlign:
additional_correction_shift_x = self.corr_pos_x_2[-1]
additional_correction_shift_y = self.corr_pos_y_2[-1]
print(
"Additional correction shifts 2:"
f" {additional_correction_shift_x} {additional_correction_shift_y}"
f"Additional correction shifts 2: {additional_correction_shift_x} {additional_correction_shift_y}"
)
return (additional_correction_shift_x, additional_correction_shift_y)
@@ -527,8 +525,7 @@ class LamNI(LamNIOpticsMixin):
def get_beamline_checks_enabled(self):
print(
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:"
f" {self.check_light_available}"
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available: {self.check_light_available}"
)
@property
@@ -769,18 +766,13 @@ class LamNI(LamNIOpticsMixin):
# pylint: disable=undefined-variable
self._current_scan_list.append(bec.queue.next_scan_number)
logger.info(
f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}],"
f" step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0},"
f" stitch_overlap={1},center_x={self.align.tomo_fovx_offset},"
f" center_y={self.align.tomo_fovy_offset},"
f" shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]},"
f" shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]},"
f" fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')"
)
log_message = (
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan"
f" number {bec.queue.next_scan_number}.\n"
f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}], step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0}, stitch_overlap={1},"
f"center_x={self.align.tomo_fovx_offset}, center_y={self.align.tomo_fovy_offset}, "
f"shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]}, "
f"shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]}, "
f"fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')"
)
log_message = f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan number {bec.queue.next_scan_number}.\n"
self.write_to_spec_log(log_message)
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
corridor_size = self.corridor_size if self.corridor_size > 0 else None
@@ -822,7 +814,10 @@ class LamNI(LamNIOpticsMixin):
msgs.append("Check beam failed: Shutter is closed.")
if self.check_light_available:
machine_status = dev.sls_machine_status.read(cached=True)
if machine_status["value"] not in ["Light Available", "Light-Available"]:
if machine_status["value"] not in [
"Light Available",
"Light-Available",
]:
self._beam_is_okay = False
msgs.append("Check beam failed: Light not available.")
if self.check_fofb:
@@ -863,8 +858,7 @@ class LamNI(LamNIOpticsMixin):
try:
msg = bec.logbook.LogbookMessage()
msg.add_text(
"<p><mark class='pen-red'><strong>Beamline checks failed at"
f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
f"<p><mark class='pen-red'><strong>Beamline checks failed at {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
).add_tag(["BEC", "beam_check"])
self.client.logbook.send_logbook_message(msg)
except Exception:
@@ -881,20 +875,25 @@ class LamNI(LamNIOpticsMixin):
try:
msg = bec.logbook.LogbookMessage()
msg.add_text(
"<p><mark class='pen-red'><strong>Operation resumed at"
f" {str(datetime.datetime.now())}.</strong></mark></p>"
f"<p><mark class='pen-red'><strong>Operation resumed at {str(datetime.datetime.now())}.</strong></mark></p>"
).add_tag(["BEC", "beam_check"])
self.client.logbook.send_logbook_message(msg)
except Exception:
logger.warning("Failed to send update to SciLog.")
def add_sample_database(
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
self,
samplename,
date,
eaccount,
scan_number,
setup,
sample_additional_info,
user,
):
"""Add a sample to the omny sample database. This also retrieves the tomo id."""
subprocess.run(
"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt"
f" 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'",
f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'",
shell=True,
)
with open("/tmp/currsamplesnr.txt") as tomo_number_file:
@@ -1029,16 +1028,14 @@ class LamNI(LamNIOpticsMixin):
print(f"Counting time <ctime> = {self.tomo_countingtime} s")
print(f"Stepsize microns <step> = {self.tomo_shellstep}")
print(
f"Piezo range (max 80) <microns> = {self.lamni_piezo_range_x},"
f" {self.lamni_piezo_range_y}"
f"Piezo range (max 80) <microns> = {self.lamni_piezo_range_x}, {self.lamni_piezo_range_y}"
)
print(f"Stitching number x,y = {self.lamni_stitch_x}, {self.lamni_stitch_y}")
print(f"Stitching overlap = {self.tomo_stitch_overlap}")
print(f"Circuilar FOV diam <microns> = {self.tomo_circfov}")
print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}")
print(
"For information, fov offset is rotating and finding the ROI, manual shift moves"
" rotation center"
"For information, fov offset is rotating and finding the ROI, manual shift moves rotation center"
)
print(f" _tomo_fovx_offset <mm> = {self.align.tomo_fovx_offset}")
print(f" _tomo_fovy_offset <mm> = {self.align.tomo_fovy_offset}")
@@ -1094,7 +1091,7 @@ class LamNI(LamNIOpticsMixin):
with open(ptycho_queue_file, "w") as queue_file:
scans = " ".join([str(scan) for scan in self._current_scan_list])
queue_file.write(f"p.scan_number {scans}\n")
queue_file.write("p.check_nextscan_started 1\n")
queue_file.write(f"p.check_nextscan_started 1\n")
def write_pdf_report(self):
"""create and write the pdf report with the current LamNI settings"""
@@ -1119,25 +1116,17 @@ class LamNI(LamNIOpticsMixin):
f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n",
f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n",
f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n",
(
f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n"
),
f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n",
f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n",
(
f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n"
),
(
f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n"
),
f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n",
f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n",
f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n",
f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n",
f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range:>{padding}}\n",
f"{'Restriction to circular FOV:':<{padding}}{self.tomo_circfov:>{padding}.2f}\n",
f"{'Stitching:':<{padding}}{stitching:>{padding}}\n",
f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n",
(
f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n"
),
f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n",
]
content = "".join(content)
user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf")
@@ -1145,13 +1134,20 @@ class LamNI(LamNIOpticsMixin):
file.write(header)
file.write(content)
subprocess.run(
"xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True
"xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &",
shell=True,
)
# status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True)
msg = bec.logbook.LogbookMessage()
logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png")
msg.add_file(logo_path).add_text("".join(content).replace("\n", "</p><p>")).add_tag(
["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name]
[
"BEC",
"tomo_parameters",
f"dataset_id_{dataset_id}",
"LamNI",
self.sample_name,
]
)
self.client.logbook.send_logbook_message(msg)
@@ -1236,12 +1232,16 @@ class DataDrivenLamNI(LamNI):
def sub_tomo_scan(self):
raise NotImplementedError(
"Cannot run sub_tomo_scan with data-driven LamNI. Please use"
" lamni.tomo_scan(subtomo_start=<START_NUM>) instead."
"Cannot run sub_tomo_scan with data-driven LamNI. Please use lamni.tomo_scan(subtomo_start=<START_NUM>) instead."
)
def _at_each_angle(
self, angle=None, stepsize=None, loptz_pos=None, manual_shift_x=0, manual_shift_y=0
self,
angle=None,
stepsize=None,
loptz_pos=None,
manual_shift_x=0,
manual_shift_y=0,
):
# Do something...
# self.tomo_parameters

View File

@@ -0,0 +1,3 @@
from .cSAXS import *
# from .LamNI import *

View File

@@ -0,0 +1 @@
from .cSAXS_beamline import fshopen, fshclose, fshstatus, epics_get, epics_put

View File

@@ -1,9 +1,10 @@
import builtins
from bec_ipython_client.beamline_mixin import BeamlineShowInfo
from rich import box
from rich.table import Table
from bec_client.beamline_mixin import BeamlineShowInfo
class BeamlineInfo(BeamlineShowInfo):
def show(self):

View File

@@ -0,0 +1,76 @@
"""
Post startup script for the BEC client. This script is executed after the
IPython shell is started. It is used to load the beamline specific
information and to setup the prompts.
The script is executed in the global namespace of the IPython shell. This
means that all variables defined here are available in the shell.
If needed, bec command-line arguments can be parsed here. For example, to
parse the --session argument, add the following lines to the script:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--session", help="Session name", type=str, default="my_default_session")
args = parser.parse_args()
if args.session == "my_session":
print("Loading my_session session")
from bec_plugins.bec_client.plugins.my_session import *
else:
print("Loading default session")
from bec_plugins.bec_client.plugins.default_session import *
"""
# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module
import argparse
from bec_lib import bec_logger
logger = bec_logger.logger
logger.info("Using the cSAXS startup script.")
parser = argparse.ArgumentParser()
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
args = parser.parse_args()
if args.session == "LamNI":
print("Loading LamNI session")
from bec_plugins.bec_client.plugins.cSAXS import *
from bec_plugins.bec_client.plugins.LamNI import *
lamni = LamNI(bec)
elif args.session == "cSAXS":
print("Loading cSAXS session")
from bec_plugins.bec_client.plugins.cSAXS import *
# SETUP BEAMLINE INFO
from bec_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
from bec_plugins.bec_client.plugins.cSAXS.beamline_info import BeamlineInfo
bec._beamline_mixin._bl_info_register(BeamlineInfo)
bec._beamline_mixin._bl_info_register(SLSInfo)
bec._beamline_mixin._bl_info_register(OperatorInfo)
# SETUP PROMPTS
bec._ip.prompts.username = args.session
bec._ip.prompts.status = 1
# REGISTER BEAMLINE CHECKS
from bec_lib.bl_conditions import (
FastOrbitFeedbackCondition,
LightAvailableCondition,
ShutterCondition,
)
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
# bec.bl_checks.register(_fast_orbit_feedback_condition)
bec.bl_checks.register(_light_available_condition)
bec.bl_checks.register(_shutter_condition)

View File

@@ -0,0 +1,25 @@
"""
Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to set up the BEC client configuration. The script is
executed in the global namespace of the BEC client. This means that all
variables defined here are available in the BEC client.
To set up the BEC client configuration, use the ServiceConfig class. For example,
to set the configuration file path, add the following lines to the script:
import pathlib
from bec_lib import ServiceConfig
current_path = pathlib.Path(__file__).parent.resolve()
CONFIG_PATH = f"{current_path}/<path_to_my_config_file.yaml>"
config = ServiceConfig(CONFIG_PATH)
If this startup script defined a ServiceConfig object, the BEC client will use
it to configure itself. Otherwise, the BEC client will use the default config.
"""
# example:
# current_path = pathlib.Path(__file__).parent.resolve()
# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml"
# config = ServiceConfig(CONFIG_PATH)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
import yaml
# TODO: fix imports, those classes are located in .../bec/scibec/init_scibec/configs/config.py
# (see also lamni_config.py in bec repository)
from .config import DemoConfig, X12SAConfig
class LamNIConfig(DemoConfig, X12SAConfig):
def run(self):
# self.write_galil_motors()
# self.write_rt_motors()
# self.write_smaract_motors()
# self.write_eiger1p5m()
self.write_x12sa_status()
self.write_sls_status()
self.load_csaxs_config()
# self.write_sim_user_motors()
# self.write_sim_beamline_motors()
# self.write_sim_beamline_monitors()
def write_galil_motors(self):
lamni_galil_motors = [
("lsamx", "A", -1, 0.5, {"center": 8.768000}),
("lsamy", "B", 1, 0.5, {"center": 10.041000}),
("lsamrot", "C", 1, 0.5, {}),
("loptz", "D", -1, 0.5, {}),
("loptx", "E", 1, 0.5, {"in": -0.8380, "out": -0.699}),
("lopty", "F", 1, 0.5, {"in": 3.3540, "out": 3.53}),
("leyex", "G", -1, 0.001, {"in": 14.117000}),
("leyey", "H", -1, 0.001, {"in": 48.069000, "out": 0.5}),
]
out = {}
for m in lamni_galil_motors:
out[m[0]] = dict(
{
"status": {"enabled": True, "enabled_set": True},
"deviceClass": "GalilMotor",
"deviceConfig": {
"axis_Id": m[1],
"name": m[0],
"labels": m[0],
"host": "mpc2680.psi.ch",
"port": 8081,
"sign": m[2],
"limits": [0, 0],
"tolerance": m[3],
"device_access": True,
"device_mapping": {"rt": "rtx"},
},
"acquisitionConfig": {
"schedule": "sync",
"acquisitionGroup": "motor",
"readoutPriority": "baseline",
},
"deviceTags": ["lamni"],
}
)
if m[4]:
out[m[0]]["userParameter"] = m[4]
self.write_section(out, "LamNI Galil motors")
def write_rt_motors(self):
lamni_rt_motors = [
("rtx", "A", 1),
("rty", "B", 1),
]
out = dict()
for m in lamni_rt_motors:
out[m[0]] = dict(
{
"status": {"enabled": True, "enabled_set": True},
"deviceClass": "RtLamniMotor",
"deviceConfig": {
"axis_Id": m[1],
"name": m[0],
"labels": m[0],
"host": "mpc2680.psi.ch",
"port": 3333,
"limits": [0, 0],
"sign": m[2],
"device_access": True,
},
"acquisitionConfig": {
"schedule": "sync",
"acquisitionGroup": "motor",
"readoutPriority": "baseline",
},
"deviceTags": ["lamni"],
}
)
self.write_section(out, "LamNI RT")
def write_smaract_motors(self):
lamni_smaract_motors = [
("losax", "A", -1, {"in": -0.848000}),
("losay", "B", -1, {"in": 0.135000, "out": 3.8}),
("losaz", "C", 1, {"in": -1, "out": -3}),
("lcsx", "D", -1, {}),
("lcsy", "E", -1, {}),
]
out = dict()
for m in lamni_smaract_motors:
out[m[0]] = dict(
{
"status": {"enabled": True, "enabled_set": True},
"deviceClass": "SmaractMotor",
"deviceConfig": {
"axis_Id": m[1],
"name": m[0],
"labels": m[0],
"host": "mpc2680.psi.ch",
"port": 8085,
"limits": [0, 0],
"sign": m[2],
"tolerance": 0.05,
},
"acquisitionConfig": {
"schedule": "sync",
"acquisitionGroup": "motor",
"readoutPriority": "baseline",
},
"deviceTags": ["lamni"],
}
)
if m[3]:
out[m[0]]["userParameter"] = m[3]
self.write_section(out, "LamNI SmarAct motors")
def write_eiger1p5m(self):
out = {
"eiger1p5m": {
"description": "Eiger 1.5M in vacuum detector, in-house developed, PSI",
"status": {"enabled": True, "enabled_set": True},
"deviceClass": "Eiger1p5MDetector",
"deviceConfig": {"device_access": True, "name": "eiger1p5m"},
"acquisitionConfig": {
"schedule": "sync",
"acquisitionGroup": "detector",
"readoutPriority": "monitored",
},
"deviceTags": ["detector"],
}
}
self.write_section(out, "LamNI Eiger 1.5M in vacuum")
def load_csaxs_config(self):
CONFIG_PATH = "./init_scibec/configs/test_config_cSAXS.yaml"
content = {}
with open(CONFIG_PATH, "r") as csaxs_config_file:
content = yaml.safe_load(csaxs_config_file.read())
self.write_section(content, "Default cSAXS config")

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,9 @@ but they are executed in a specific order:
import time
import numpy as np
from bec_lib import MessageEndpoints, bec_logger
from bec_server.scan_server.errors import ScanAbortion
from bec_server.scan_server.scans import RequestBase, ScanArgType, ScanBase
from bec_lib import MessageEndpoints, bec_logger, messages
from scan_server.errors import ScanAbortion
from scan_server.scans import RequestBase, ScanArgType, ScanBase
MOVEMENT_SCALE_X = np.sin(np.radians(15)) * np.cos(np.radians(30))
MOVEMENT_SCALE_Y = np.cos(np.radians(15))
@@ -96,8 +96,8 @@ class LamNIMixin:
coarse_move_req_x = np.abs(lsamx_current - move_x)
coarse_move_req_y = np.abs(lsamy_current - move_y)
self.device_manager.devices.lsamx.read_only = False
self.device_manager.devices.lsamy.read_only = False
self.device_manager.devices.lsamx.enabled_set = True
self.device_manager.devices.lsamy.enabled_set = True
if (
np.abs(y_drift) > 150
@@ -126,14 +126,12 @@ class LamNIMixin:
x_drift2 = x_center_expect * 1000 - rtx_current
y_drift2 = y_center_expect * 1000 - rty_current
logger.info(
f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f},"
f" y={y_drift2:.3f}"
f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f}, y={y_drift2:.3f}"
)
if np.abs(x_drift2) > 5 or np.abs(y_drift2) > 5:
logger.info(
"Compensating second iteration"
f" {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}"
f"Compensating second iteration {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}"
)
move_x = (
x_stage
@@ -155,20 +153,18 @@ class LamNIMixin:
rty_current = yield from self.stubs.send_rpc_and_wait("rty", "readback.get")
logger.info(
f"New scan center interferometer after second iteration {rtx_current:.3f},"
f" {rty_current:.3f} microns"
f"New scan center interferometer after second iteration {rtx_current:.3f}, {rty_current:.3f} microns"
)
x_drift2 = x_center_expect * 1000 - rtx_current
y_drift2 = y_center_expect * 1000 - rty_current
logger.info(
f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f},"
f" y={y_drift2:.3f}"
f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f}, y={y_drift2:.3f}"
)
else:
logger.info("No second iteration required")
self.device_manager.devices.lsamx.read_only = True
self.device_manager.devices.lsamy.read_only = True
self.device_manager.devices.lsamx.enabled_set = False
self.device_manager.devices.lsamy.enabled_set = False
yield from self.stubs.send_rpc_and_wait("rtx", "controller.feedback_enable_without_reset")
@@ -178,12 +174,8 @@ class LamNIMoveToScanCenter(RequestBase, LamNIMixin):
scan_report_hint = None
scan_type = "step"
required_kwargs = []
arg_input = {
"shift_x": ScanArgType.FLOAT,
"shift_y": ScanArgType.FLOAT,
"angle": ScanArgType.FLOAT,
}
arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1}
arg_input = [ScanArgType.FLOAT, ScanArgType.FLOAT, ScanArgType.FLOAT]
arg_bundle_size = None
def __init__(self, *args, parameter=None, **kwargs):
"""
@@ -207,10 +199,10 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
scan_report_hint = "table"
scan_type = "step"
required_kwargs = ["fov_size", "exp_time", "step", "angle"]
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
arg_input = []
arg_bundle_size = None
def __init__(self, *args, parameter: dict = None, **kwargs):
def __init__(self, *args, parameter=None, **kwargs):
"""
A LamNI scan following Fermat's spiral.
@@ -365,8 +357,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
self.stitch_x, self.stitch_y, self.angle
)
logger.info(
f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x},"
f" {_shfity+y_stitch_shift/1000+self.shift_y}"
f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x}, {_shfity+y_stitch_shift/1000+self.shift_y}"
)
return (
_shfitx + x_stitch_shift / 1000 + self.shift_x,
@@ -457,17 +448,16 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
yield from self.stubs.kickoff(device="rtx")
while True:
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
msg = self.device_manager.connector.get(MessageEndpoints.device_status("rt_scan"))
msg = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan"))
if msg:
status = msg
status = messages.DeviceStatusMessage.loads(msg)
status_id = status.content.get("status", 1)
request_id = status.metadata.get("RID")
if status_id == 0 and self.metadata.get("RID") == request_id:
break
if status_id == 2 and self.metadata.get("RID") == request_id:
raise ScanAbortion(
"An error occured during the LamNI readout:"
f" {status.metadata.get('error')}"
f"An error occured during the LamNI readout: {status.metadata.get('error')}"
)
time.sleep(1)

View File

@@ -26,7 +26,7 @@ but they are executed in a specific order:
# import numpy as np
# from bec_lib import MessageEndpoints, bec_logger, messages
# from bec_server.scan_server.errors import ScanAbortion
# from bec_server.scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase
# from scan_server.errors import ScanAbortion
# from scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase
# logger = bec_logger.logger

View File

@@ -0,0 +1,182 @@
import os
import json
import subprocess
import requests
from bec_lib.file_utils import FileWriterMixin
#from bec_lib.bec_service import SERVICE_CONFIG
class csaxs_archiver:
"""Class to archive data from a beamtime.
To run this script from a shell, go to discovery.psi.ch and copy your token.
Complement the information in user_input below in the if __name__ == __main__ part of the script.
Afterwards, get a Keberos token (kinit) for yourself in the shell.
Activate the bec_venv by doing "source bec_venv/bin/activate" and then run this code via python $filename.
As a last step, adjust the dictionary below in if __name__ == '__main__' with your token as well as information about the experiment
"""
def __init__(
self,
start_scan: int,
stop_scan: int,
base_path: str,
log_path: str,
eacc: str,
pi: str,
pi_email: str,
token: str,
type: str = "raw",
overwrite: bool = False,
online: bool = True,
):
self.start_scan = start_scan
self.stop_scan = stop_scan
self.log_path = os.path.expanduser(log_path)
self.eacc = eacc
self.pi = pi
self.pi_email = pi_email
self.token = token
self.type = type
self.overwrite = overwrite
self.online = online
#from bec_lib.bec_service import SERVICE_CONFIG
#SERVICE_CONFIG.config["service_config"]["file_writer"]
self._load_datacatalogue_module()
self._create_directory(base_path)
self._disable_mail_confirmation()
self.service_cfg = {'base_path' : f'{self.base_path}'}
self.file_writer = FileWriterMixin(self.service_cfg)
def _disable_mail_confirmation(self):
# Define the URL and payload
url = "https://dacat.psi.ch/api/v3/Policies/updatewhere"
payload = {
"ownerGroupList": f'p{self.eacc[1:]}',
"data": '{"archiveEmailNotification": false, "accessGroups": ["slscsaxs"]}'
}
# Define headers
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
}
# Add the access_token to the URL
url += "?access_token=" + self.token
# Make a POST request
print(url, payload, headers)
response = requests.post(url, data=payload, headers=headers)
# Check the response
if response.status_code == 200:
print("Request was successful.")
print(response.json())
else:
print("Request failed with status code:", response.status_code)
print(response.text)
def _create_directory(self, base_path: str) -> None:
if self.online:
self.base_path = os.path.expanduser("~/Data10")
else:
self.base_path = base_path
if not os.path.exists(self.log_path):
os.makedirs(self.log_path)
def _load_datacatalogue_module(self):
command = 'module add datacatalog/1.1.9'
os.popen(command)
# result = subprocess.run(
# command,
# shell=False,
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
# universal_newlines=True,
# )
# if result.returncode == 0:
# print(f"Command {command} was succesful")
# else:
# print(f"Failed to run command {command} with return message {result.returncode}")
def prep_metadata(self, scannr: int) -> dict:
user_metadata = {}
user_metadata.update(
{
"principalInvestigator": self.pi_email,
"owner": self.pi,
"ownerEmail": self.pi_email,
"sourceFolder": self.base_path,
"creationLocation": "/PSI/SLS/CSAXS",
"type": "raw",
"ownerGroup": f"p{self.eacc.strip('e')}",
"datasetName": f"S{scannr:05d}",
}
)
return user_metadata
def _write_ingestion_log(self, scannr: int) -> None:
...
def run_for_all_scans(self):
for scan in range(self.start_scan, self.stop_scan + 1):
print(f"Start ingestion for scan {scan}")
fname = os.path.join(os.path.expanduser(self.log_path), f"ingestion_log_S{scan:05d}")
self.datafile_name = f"{fname}.txt"
if os.path.isfile(self.datafile_name) and not self.overwrite == True:
print(
f"Skipping scan {scan}, already ingested due to logs, moving on to next scan {scan+1}"
)
continue
user_metadata = self.prep_metadata(scan)
# Write metadata file in json file
self.metadata_file = f"{fname}.json"
with open(self.metadata_file, "w") as file:
json.dump(user_metadata, file)
# Compile datapath based on structure a cSAXS
datadir_path = os.path.join('data', self.file_writer.get_scan_directory(scan, 1000, 5))
print(f"Archiving directory {datadir_path}")
if not os.path.isdir(os.path.join(self.base_path, datadir_path)):
print(f"Did not find directory {datadir_path}, skipping scan {scan}")
continue
# Create datafile path for archiving
with open(self.datafile_name, "w") as file:
file.write(datadir_path)
print(f"Starting ingestion for S#{scan}")
command = f'datasetIngestor -allowexistingsource -ingest -autoarchive -noninteractive -token {self.token} {self.metadata_file} {self.datafile_name}'
rtr = os.popen(command)
#with open(os.path.join(fname,'_log.txt'), "w") as file:
# print(f'Writing reponse to file')
# file.write(rtr.read())
# result = subprocess.run(command, shell=False, stdout = subprocess.PIPE, stderr = subprocess.PIPE, universal_newlines=True)
# if result.returncode == 0:
# print(f"Command {command} was succesful")
# else:
# print(f"Failed to run command {command} with return message {result.returncode}")
if __name__ == "__main__":
# Generate dictionary with user input.
user_input = {
"base_path": "~/Data10",
"eacc": "e20638",
"pi": "Emma Sparr",
"pi_email": "emma.sparr@fkem1.lu.se",
'log_path' : '~/Data10/documentation/ingestion_logs/',
'token' : 'YK8gkmQmEVxVxjiA57D6tVmpBVs7T235nWEuBT0behN9BPM2BdWARWPPgEsQVrPe',
'start_scan' : 1,
'stop_scan' : 450,
}
archiver = csaxs_archiver(**user_input)
archiver.run_for_all_scans()

View File

@@ -0,0 +1,88 @@
import csv
import os
from collections import defaultdict
from collections.abc import Callable
import numpy as np
class ScanItem:
def __init__(self, offset_xy: Callable) -> None:
self.start_entry = None
self.end_entry = None
self.offset_xy = offset_xy
def to_scan_params(self) -> dict:
scan_params = {
"start_x": float(self.start_entry["X"]) + self.offset_xy()[0],
"start_y": float(self.start_entry["Y"]) + self.offset_xy()[1],
"end_x": float(self.end_entry["X"]) + self.offset_xy()[0],
"end_y": float(self.end_entry["Y"]) + self.offset_xy()[1],
"interval_x": int(
np.round(
np.abs(float(self.start_entry["X"]) - float(self.end_entry["X"]))
/ (float(self.start_entry["step_x [mu]"]) * 1e-3)
)
),
"interval_y": int(
np.round(
np.abs(float(self.start_entry["Y"]) - float(self.end_entry["Y"]))
/ (float(self.start_entry["step_y [mu]"]) * 1e-3)
)
),
"exp_time": float(self.start_entry["exp_time [s]"]),
"readout_time": 3e-3,
"md": {"sample_name": self.start_entry["sample name"]},
}
if scan_params["interval_x"] < 1 or scan_params["interval_x"] < 1:
raise ValueError("Bugger off...")
return scan_params
class SAXSParams:
def __init__(self, offset: Callable):
self.offset_xy = offset
self.data = defaultdict(lambda: ScanItem(offset))
def load_from_csv(self, file_path: str) -> None:
"""
Load the acquisition parameter from a csv file.
"""
if not os.path.exists(file_path):
raise FileNotFoundError(
f"The specified CSV file could not be found. Please check that the given path is correct: {file_path}."
)
data_transposed = defaultdict(lambda: [])
with open(os.path.expanduser(file_path), "r") as file:
csv_reader = csv.DictReader(file)
for row in csv_reader:
for key, val in row.items():
data_transposed[key].append(val)
if int(row["start"]):
self.data[row["sample name"]].start_entry = row
else:
self.data[row["sample name"]].end_entry = row
self._check_params(dict(data_transposed))
def _check_params(self, data_transposed: dict) -> dict:
sample_names = set(data_transposed["sample name"])
if len(data_transposed["start"]) != len(sample_names) * 2:
raise ValueError(
f"The given params file does not provide N*2 start/stop positions. Found {len(sample_names)} samples and {len(data_transposed['start'])} start/stop positions."
)
if __name__ == "__main__":
from pprint import pprint
INPUT_FILE = "/sls/X12SA/data/e21206/Data10/software/test_script.csv"
def my_offset():
return [0, 0]
params = SAXSParams(my_offset)
params.load_from_csv(INPUT_FILE)
for key in params.data:
pprint(params.data[key].to_scan_params())

View File

@@ -1,7 +1,3 @@
""" Script used for parsing scan parameters from a CSV file as created by the motormap GUI.
This needs to be reviewed and tested during the deployment phase."""
# pylint: skip-file
import csv
import os
from collections import defaultdict

View File

@@ -0,0 +1,13 @@
import yaml
CONFIG_PATH = "/sls/X12SA/data/gac-x12saop/bec/config/bec_service_config.yaml"
def load_service_config() -> dict:
"""Load the service configuration from the YAML file."""
with open(CONFIG_PATH, "r", encoding="utf-8") as stream:
try:
config = yaml.safe_load(stream)
except yaml.YAMLError as exc:
print(exc)
return config

5
bin/open_tunnel.sh Executable file
View File

@@ -0,0 +1,5 @@
for i in `seq 1 8`
do
ssh -N -R 6379:localhost:6379 x12sa-cn-$i &
done

48
bin/setup_bec.sh Executable file
View File

@@ -0,0 +1,48 @@
if [ "pc15543.psi.ch" != "$(hostname)" ]; then
echo "Please run this script on pc15543"
exit 1
fi
module use unstable
module add psi-python311/2024.02
echo module add tmux/3.2 >> ~/.bashrc
echo module add redis/7.0.12 >> ~/.bashrc
source ~/.bashrc
cd ~/Data10
mkdir -p software/
mkdir -p ~/bec/scripts
cd software
git clone https://gitlab.psi.ch/bec/bec.git
git clone https://gitlab.psi.ch/bec/ophyd_devices.git
git clone https://gitlab.psi.ch/bec/bec-widgets.git
python -m venv ./bec_venv
source ./bec_venv/bin/activate
cd bec
git checkout sastt-online-changes
pip install -e ./bec_server[dev]
cd ../csaxs-bec
pip install -e .[dev]
#redis-server --protected-mode no &
read -p "Do you want to set the current BEC account to $(whoami)? (yes/no) " yn
val=$(whoami)
case $yn in
yes ) echo ok, setting account to $val;
redis-cli SET internal/account:val $val;;
no ) echo ;;
* ) echo invalid response;
exit 1;;
esac
$(pwd)/open_tunnel.sh

17
bin/setup_bec_widgets.sh Executable file
View File

@@ -0,0 +1,17 @@
module use unstable
module add psi-python311/2024.02
cd ~/Data10/software
python -m venv ./bec_widgets_venv
source ./bec_widgets_venv/bin/activate
pip install --upgrade pip
cd ~/Data10/software/bec/bec_lib
pip install -e .
cd ~/Data10/software/csaxs-bec
pip install -e .
cd ~/Data10/software/bec-widgets
pip install -e .
echo "For the moment widgets only run on beamline consoles comp1/2 and cons1"

View File

@@ -1,2 +0,0 @@
from .load_additional_correction import lamni_read_additional_correction
from .x_ray_eye_align import DataDrivenLamNI, LamNI, MagLamNI, XrayEyeAlign

View File

@@ -1,2 +0,0 @@
from .cSAXS_beamline import epics_get, epics_put, fshclose, fshopen, fshstatus
from .csaxs_bl_checks import cSAXSBeamlineChecks

View File

@@ -1,122 +0,0 @@
import builtins
import datetime
import threading
import time
from bec_lib import bec_logger
logger = bec_logger.logger
if builtins.__dict__.get("bec"):
bec = builtins.__dict__.get("bec")
class cSAXSBeamlineChecks:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.check_shutter = True
self.check_light_available = True
self.check_fofb = True
self._check_msgs = []
self._beam_is_okay = True
self._stop_beam_check_event = None
self.beam_check_thread = None
def get_beamline_checks_enabled(self):
print(
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:"
f" {self.check_light_available}"
)
@property
def beamline_checks_enabled(self):
return {
"shutter": self.check_shutter,
"fofb": self.check_fofb,
"light available": self.check_light_available,
}
@beamline_checks_enabled.setter
def beamline_checks_enabled(self, val: bool):
self.check_shutter = val
self.check_light_available = val
self.check_fofb = val
self.get_beamline_checks_enabled()
def _run_beamline_checks(self):
msgs = []
dev = builtins.__dict__.get("dev")
try:
if self.check_shutter:
shutter_val = dev.x12sa_es1_shutter_status.read(cached=True)
if shutter_val["value"].lower() != "open":
self._beam_is_okay = False
msgs.append("Check beam failed: Shutter is closed.")
if self.check_light_available:
machine_status = dev.sls_machine_status.read(cached=True)
if machine_status["value"] not in ["Light Available", "Light-Available"]:
self._beam_is_okay = False
msgs.append("Check beam failed: Light not available.")
if self.check_fofb:
fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True)
if fast_orbit_feedback["value"] != "running":
self._beam_is_okay = False
msgs.append("Check beam failed: Fast orbit feedback is not running.")
except Exception:
logger.warning("Failed to check beam.")
return msgs
def _check_beam(self):
while not self._stop_beam_check_event.is_set():
self._check_msgs = self._run_beamline_checks()
if not self._beam_is_okay:
self._stop_beam_check_event.set()
time.sleep(1)
def _start_beam_check(self):
self._beam_is_okay = True
self._stop_beam_check_event = threading.Event()
self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True)
self.beam_check_thread.start()
def _was_beam_okay(self):
self._stop_beam_check_event.set()
self.beam_check_thread.join()
return self._beam_is_okay
def _print_beamline_checks(self):
for msg in self._check_msgs:
logger.warning(msg)
def _wait_for_beamline_checks(self):
self._print_beamline_checks()
try:
msg = bec.logbook.LogbookMessage()
msg.add_text(
"<p><mark class='pen-red'><strong>Beamline checks failed at"
f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
).add_tag(["BEC", "beam_check"])
bec.logbook.send_logbook_message(msg)
except Exception:
logger.warning("Failed to send update to SciLog.")
while True:
self._beam_is_okay = True
self._check_msgs = self._run_beamline_checks()
if self._beam_is_okay:
break
self._print_beamline_checks()
time.sleep(1)
try:
msg = bec.logbook.LogbookMessage()
msg.add_text(
"<p><mark class='pen-red'><strong>Operation resumed at"
f" {str(datetime.datetime.now())}.</strong></mark></p>"
).add_tag(["BEC", "beam_check"])
bec.logbook.send_logbook_message(msg)
except Exception:
logger.warning("Failed to send update to SciLog.")

View File

@@ -1 +0,0 @@
from .flomni import Flomni

File diff suppressed because it is too large Load Diff

View File

@@ -1,177 +0,0 @@
import time
from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
class FlomniOpticsMixin:
@staticmethod
def _get_user_param_safe(device, var):
param = dev[device].user_parameter
if not param or param.get(var) is None:
raise ValueError(f"Device {device} has no user parameter definition for {var}.")
return param.get(var)
def feye_out(self):
fshclose()
self.foptics_in()
feyex_out = self._get_user_param_safe("feyex", "out")
umv(dev.feyex, feyex_out)
epics_put("XOMNYI-XEYE-ACQ:0", 2)
# move rotation stage to zero to avoid problems with wires
umv(dev.fsamroy, 0)
# umv(dev.fttrx1, 9.2)
def feye_in(self):
bec.queue.next_dataset_number += 1
# umv(dev.fttrx1, -17)
feyex_in = self._get_user_param_safe("feyex", "in")
feyey_in = self._get_user_param_safe("feyey", "in")
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
self.align.update_frame()
def _ffzp_in(self):
foptx_in = self._get_user_param_safe("foptx", "in")
fopty_in = self._get_user_param_safe("fopty", "in")
umv(dev.foptx, foptx_in)
umv(
dev.fopty, fopty_in
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
def ffzp_in(self):
"""
move in the flomni zone plate.
This will disable rt feedback, move the FZP and re-enabled the feedback.
"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
self._ffzp_in()
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_enable_with_reset()
def foptics_in(self):
"""
Move in the flomni optics, including the FZP and the OSA.
"""
self.ffzp_in()
self.fosa_in()
def foptics_out(self):
"""Move out the flomni optics"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
self.fosa_out()
fopty_out = self._get_user_param_safe("fopty", "out")
umv(dev.fopty, fopty_out)
if "rtx" in dev and dev.rtx.enabled:
time.sleep(1)
dev.rtx.controller.feedback_enable_with_reset()
def fosa_in(self):
# 6.2 keV, 170 um FZP
# umv(dev.losax, -1.4450000, dev.losay, -0.1800)
# umv(dev.losaz, -1)
# 6.7, 170
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
# umv(dev.losaz, 1.0000)
# 7.2, 150
fosax_in = self._get_user_param_safe("fosax", "in")
fosay_in = self._get_user_param_safe("fosay", "in")
fosaz_in = self._get_user_param_safe("fosaz", "in")
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
dev.fosay.limits = [fosay_in - 0.1, fosay_in + 0.1]
dev.fosaz.limits = [fosaz_in - 0.1, fosaz_in + 0.1]
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
umv(dev.fosaz, fosaz_in)
# 11 kev
# umv(dev.losax, -1.161000, dev.losay, -0.196)
# umv(dev.losaz, 1.0000)
def fosa_out(self):
self.ensure_fheater_up()
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()
if not curtain_is_triggered:
fosaz_out = self._get_user_param_safe("fosaz", "out")
dev.fosaz.limits = [fosaz_out - 0.1, fosaz_out + 0.1]
umv(dev.fosaz, fosaz_out)
fosax_out = self._get_user_param_safe("fosax", "out")
dev.fosax.limits = [fosax_out - 0.1, fosax_out + 0.1]
umv(dev.fosax, fosax_out)
def ffzp_info(self, mokev_val=-1):
if mokev_val == -1:
try:
mokev_val = dev.mokev.readback.get()
except:
print(
"Device mokev does not exist. You can specify the energy in keV as an argument instead."
)
return
foptz_val = dev.foptz.readback.get()
distance = -foptz_val + 43.15 + 36.7
print(f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the FZP.\n")
print(f"At the current energy of {mokev_val:.4f} keV we have following options:\n")
diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6]
console = Console()
table = Table(title="Outermost zone width \033[1m60 nm\033[0m", box=box.SQUARE)
table.add_column("Diameter", justify="center")
table.add_column("Focal distance", justify="center")
table.add_column("Current beam size", justify="center")
wavelength = 1.2398e-9 / mokev_val
for diameter in diameters:
outermost_zonewidth = 60e-9
focal_distance = diameter * outermost_zonewidth / wavelength * 1000
beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9
table.add_row(
f"{diameter*1e6:.2f} microns",
f"{focal_distance:.2f} mm",
f"{beam_size:.2f} microns",
)
console.print(table)
diameters = [150e-6, 250e-6]
console = Console()
table = Table(title="Outermost zone width \033[1m30 nm\033[0m", box=box.SQUARE)
table.add_column("Diameter", justify="center")
table.add_column("Focal distance", justify="center")
table.add_column("Current beam size", justify="center")
wavelength = 1.2398e-9 / mokev_val
for diameter in diameters:
outermost_zonewidth = 30e-9
focal_distance = diameter * outermost_zonewidth / wavelength * 1000
beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9
table.add_row(
f"{diameter*1e6:.2f} microns",
f"{focal_distance:.2f} mm",
f"{beam_size:.2f} microns",
)
console.print(table)
fosaz_val = dev.fosaz.readback.get()
print("\nOSA Information:")
print(f" Current fosaz {fosaz_val:.1f}")
print(
f" The OSA will collide with a normal OMNY pin at fosaz \033[1m{(33-fosaz_val):.1f}\033[0m"
)
print(f" Remaining space: \033[1m{-fosaz_val+(33-foptz_val):.1f}\033[0m")

View File

@@ -1,244 +0,0 @@
from __future__ import annotations
import builtins
import os
import time
from typing import TYPE_CHECKING
from bec_lib import bec_logger
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
logger = bec_logger.logger
# import builtins to avoid linter errors
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
umvr = builtins.__dict__.get("umvr")
if TYPE_CHECKING:
from bec_ipython_client.plugins.flomni import Flomni
class XrayEyeAlign:
# pixel calibration, multiply to get mm
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
def __init__(self, client, flomni: Flomni) -> None:
self.client = client
self.flomni = flomni
self.device_manager = client.device_manager
self.scans = client.scans
self.alignment_values = {}
self.flomni.reset_correction()
self.flomni.reset_tomo_alignment_fit()
def _reset_init_values(self):
self.shift_xy = [0, 0]
self._xray_fov_xy = [0, 0]
def save_frame(self):
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
def update_frame(self):
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
# start live
epics_put("XOMNYI-XEYE-ACQ:0", 1)
# wait for start live
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
time.sleep(0.5)
print("waiting for live view to start...")
fshopen()
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
print("waiting for new frame...")
time.sleep(0.5)
time.sleep(0.5)
# stop live view
epics_put("XOMNYI-XEYE-ACQ:0", 0)
time.sleep(1)
# fshclose
print("got new frame")
def tomo_rotate(self, val: float):
# pylint: disable=undefined-variable
umv(self.device_manager.devices.fsamroy, val)
def get_tomo_angle(self):
return self.device_manager.devices.fsamroy.readback.get()
def update_fov(self, k: int):
self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0])
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
@property
def movement_buttons_enabled(self):
return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")]
@movement_buttons_enabled.setter
def movement_buttons_enabled(self, enabled: bool):
enabled = int(enabled)
epics_put("XOMNYI-XEYE-ENAMVX:0", enabled)
epics_put("XOMNYI-XEYE-ENAMVY:0", enabled)
def send_message(self, msg: str):
epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg)
def align(self):
# reset shift xy and fov params
self._reset_init_values()
self.flomni.lights_off()
self.tomo_rotate(0)
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
self.flomni.feye_in()
self.flomni.laser_tracker_on()
self.flomni.rt_feedback_enable_with_reset()
# disable movement buttons
self.movement_buttons_enabled = False
sample_name = self.flomni.sample_get_name(0)
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
# this makes sure we are in a defined state
self.flomni.rt_feedback_disable()
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
self.flomni.fosa_out()
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
umv(dev.fsamx, fsamx_in - 0.25)
self.flomni.ffzp_in()
self.update_frame()
# enable submit buttons
self.movement_buttons_enabled = False
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
epics_put("XOMNYI-XEYE-STEP:0", 0)
self.send_message("Submit center value of FZP.")
k = 0
while True:
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm
self.alignment_values[k] = val_x
print(f"Clicked position {k}: x {self.alignment_values[k]}")
rtx_position = dev.rtx.readback.get() / 1000
print(f"Current rtx position {rtx_position}")
self.alignment_values[k] -= rtx_position
print(f"Corrected position {k}: x {self.alignment_values[k]}")
if k == 0: # received center value of FZP
self.send_message("please wait ...")
self.movement_buttons_enabled = False
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
self.flomni.rt_feedback_disable()
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
umv(dev.fsamx, fsamx_in)
self.flomni.foptics_out()
self.flomni.rt_feedback_disable()
umv(dev.fsamx, fsamx_in - 0.25)
self.update_frame()
epics_put("XOMNYI-XEYE-RECBG:0", 1)
while epics_get("XOMNYI-XEYE-RECBG:0") == 1:
time.sleep(0.5)
print("waiting for background frame...")
umv(dev.fsamx, fsamx_in)
time.sleep(0.5)
self.flomni.rt_feedback_enable_with_reset()
self.update_frame()
self.send_message("Adjust sample height and submit center")
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
self.movement_buttons_enabled = True
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
self.send_message("please wait ...")
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
self.movement_buttons_enabled = False
umv(dev.rtx, 0)
self.tomo_rotate(k * 45)
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
self.update_frame()
self.send_message("Submit sample center")
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
self.update_fov(k)
elif k == 5: # received sample center value at samroy 270 and done
self.send_message("done...")
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
self.movement_buttons_enabled = False
self.update_fov(k)
break
k += 1
epics_put("XOMNYI-XEYE-STEP:0", k)
_xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0")
if _xrayeyalignmvx != 0:
umvr(dev.rtx, _xrayeyalignmvx)
print(f"Current rtx position {dev.rtx.readback.get() / 1000}")
epics_put("XOMNYI-XEYE-MVX:0", 0)
if k > 0:
epics_put(f"XOMNYI-XEYE-STAGEPOSX:{k}", dev.rtx.readback.get() / 1000)
time.sleep(3)
self.update_frame()
if k < 2:
# allow movements, store movements to calculate center
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
if _xrayeyalignmvy != 0:
self.flomni.rt_feedback_disable()
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
time.sleep(2)
epics_put("XOMNYI-XEYE-MVY:0", 0)
self.flomni.rt_feedback_enable_with_reset()
self.update_frame()
time.sleep(0.2)
self.write_output()
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
self.tomo_rotate(0)
umv(dev.rtx, 0)
# free camera
epics_put("XOMNYI-XEYE-ACQ:0", 2)
print(
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
f" = {fovy:.0f} microns"
)
print("Use the matlab routine to FIT the current alignment...")
print("Then LOAD ALIGNMENT PARAMETERS by running flomni.read_alignment_offset()\n")
def write_output(self):
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
if not os.path.exists(file):
os.makedirs(os.path.dirname(file), exist_ok=True)
with open(file, "w") as alignment_values_file:
alignment_values_file.write("angle\thorizontal\n")
for k in range(1, 6):
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n")

View File

@@ -1,86 +0,0 @@
"""
Post startup script for the BEC client. This script is executed after the
IPython shell is started. It is used to load the beamline specific
information and to setup the prompts.
The script is executed in the global namespace of the IPython shell. This
means that all variables defined here are available in the shell.
If needed, bec command-line arguments can be parsed here. For example, to
parse the --session argument, add the following lines to the script:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--session", help="Session name", type=str, default="my_default_session")
args = parser.parse_args()
if args.session == "my_session":
print("Loading my_session session")
from bec_plugins.bec_ipython_client.plugins.my_session import *
else:
print("Loading default session")
from bec_plugins.bec_ipython_client.plugins.default_session import *
"""
# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module
from bec_lib import bec_logger
logger = bec_logger.logger
logger.info("Using the cSAXS startup script.")
# pylint: disable=import-error
_args = _main_dict["args"]
_session_name = "cSAXS"
if _args.session.lower() == "lamni":
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
from csaxs_bec.bec_ipython_client.plugins.LamNI import *
_session_name = "LamNI"
lamni = LamNI(bec)
logger.success("LamNI session loaded.")
elif _args.session.lower() == "csaxs":
print("Loading cSAXS session")
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
logger.success("cSAXS session loaded.")
# SETUP BEAMLINE INFO
from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
from csaxs_bec.bec_ipython_client.plugins.cSAXS.beamline_info import BeamlineInfo
bec._beamline_mixin._bl_info_register(BeamlineInfo)
bec._beamline_mixin._bl_info_register(SLSInfo)
bec._beamline_mixin._bl_info_register(OperatorInfo)
# SETUP PROMPTS
bec._ip.prompts.username = _session_name
bec._ip.prompts.status = 1
# REGISTER BEAMLINE CHECKS
from bec_lib.bl_conditions import (
FastOrbitFeedbackCondition,
LightAvailableCondition,
ShutterCondition,
)
if "sls_machine_status" in dev:
print("Registering light available condition for SLS machine status")
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
bec.bl_checks.register(_light_available_condition)
if "x12sa_es1_shutter_status" in dev:
print("Registering shutter condition for X12SA ES1 shutter status")
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
bec.bl_checks.register(_shutter_condition)
# if hasattr(dev, "sls_fast_orbit_feedback"):
# print("Registering fast orbit feedback condition for SLS fast orbit feedback")
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
# bec.bl_checks.register(_fast_orbit_feedback_condition)

View File

@@ -1,14 +0,0 @@
"""
Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to add additional command line arguments.
"""
def extend_command_line_args(parser):
"""
Extend the command line arguments of the BEC client.
"""
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
return parser

View File

@@ -1,295 +0,0 @@
# This configuration file was used for the cSAXS beamtimes in September 2023
##################################################
#############Config for cSAXS SAXS imaging########
##################################################
bpm4i:
description: 'XBPM 4: integrated counts'
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: X12SA-OP1-SCALER.
deviceTags:
- monitor
enabled: true
readOnly: false
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
mokev:
description: Monochromator energy in keV
deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
- monitor
enabled: true
readOnly: false
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
mcs:
description: Mcs scalar card for transmission readout
deviceClass: csaxs_bec.devices.epics.devices.MCScSAXS
deviceConfig:
prefix: 'X12SA-MCS:'
mcs_config:
num_lines: 1
deviceTags:
- cSAXS
- mcs
onFailure: buffer
enabled: true
readoutPriority: monitored
softwareTrigger: false
eiger9m:
description: Eiger9m HPC area detector 9M
deviceClass: csaxs_bec.devices.epics.devices.Eiger9McSAXS
deviceConfig:
prefix: 'X12SA-ES-EIGER9M:'
deviceTags:
- cSAXS
- eiger9m
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
ddg_detectors:
description: DelayGenerator for detector triggering
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG1:'
ddg_config:
delay_burst: 40.e-3
delta_width: 0
additional_triggers: 0
polarity:
- 1 # T0 -> DDG MCS
- 0 # eiger
- 1 # falcon
- 1
- 1
amplitude: 4.5
offset: 0
thres_trig_level: 2.5
set_high_on_exposure: False
set_high_on_stage: False
deviceTags:
- cSAXS
- ddg_detectors
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
ddg_mcs:
description: DelayGenerator for mcs triggering
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG2:'
ddg_config:
delay_burst: 0
delta_width: 0
additional_triggers: 1
polarity:
- 1
- 0
- 1
- 1
- 1
amplitude: 4.5
offset: 0
thres_trig_level: 2.5
set_high_on_exposure: False
set_high_on_stage: False
set_trigger_source: EXT_RISING_EDGE
trigger_width: 3.e-3
deviceTags:
- cSAXS
- ddg_mcs
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
ddg_fsh:
description: DelayGenerator for fast shutter control
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG3:'
ddg_config:
delay_burst: 0
delta_width: 80.e-3
additional_triggers: 0
polarity:
- 1
- 1
- 1
- 1
- 1
amplitude: 4.5
offset: 0
thres_trig_level: 2.5
set_high_on_exposure: True
set_high_on_stage: False
deviceTags:
- cSAXS
- ddg_fsh
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
falcon:
description: Falcon detector x-ray fluoresence
deviceClass: csaxs_bec.devices.epics.devices.FalconcSAXS
deviceConfig:
prefix: 'X12SA-SITORO:'
deviceTags:
- cSAXS
- falcon
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
pilatus_2:
description: Pilatus2 HPC area detector 300k
deviceClass: csaxs_bec.devices.epics.devices.PilatuscSAXS
deviceConfig:
prefix: 'X12SA-ES-PILATUS300K:'
deviceTags:
- cSAXS
- pilatus_2
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
samx:
description: SGalil motor stage
deviceClass: csaxs_bec.devices.galil.SGalilMotor
deviceConfig:
axis_Id: "E"
host: '129.129.122.26'
port: 23
sign: -1
limits:
- -181
- -0.1
deviceTags:
- cSAXS
- sgalil
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
samy:
description: SGalil motor stage
deviceClass: csaxs_bec.devices.galil.SGalilMotor
deviceConfig:
axis_Id: "C"
host: '129.129.122.26'
port: 23
sign: -1
limits:
- -66
- -12
deviceTags:
- cSAXS
- sgalil
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES06
motor_resolution: 0.00125
base_velocity: 0.25
velocity: 2.5
backlash_distance: 0.125
acceleration: 0.4
user_offset_dir: 0
deviceTags:
- cSAXS
- micfoc
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
owis_samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
owis_samy:
description: Owis motor stage samx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 1
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- rotx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
roty:
description: Rotation stage rotx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 1
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- rotx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false

File diff suppressed because it is too large Load Diff

View File

@@ -1,344 +0,0 @@
feyex:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
limits:
- -30
- -1
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -16.267
out: -1
feyey:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
limits:
- -1
- -10
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -10.467
fheater:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -15
- 0
port: 8082
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
flomni_samples:
description: phase plate angle
deviceClass: csaxs_bec.devices.epics.devices.flomni_sample_storage.FlomniSampleStorage
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
foptx:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -17
- -12
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: -13.761
fopty:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
limits:
- 0
- 4
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 0.552
out: 0.752
foptz:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 0
- 27
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 23
fosax:
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 10.2
- 10.6
port: 3334
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 9.124
out: 5.3
fosay:
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -3.1
- -2.9
port: 3334
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0.367
fosaz:
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -6
- -4
port: 3334
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 8.5
out: 6
fsamroy:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- -5
- 365
port: 8084
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
fsamx:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
limits:
- -162
- 0
port: 8081
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: -1.1
fsamy:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
limits:
- 2
- 3.1
port: 8081
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 2.75
ftracky:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: H
host: mpc2844.psi.ch
limits:
- 2.2
- 2.8
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
ftrackz:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: G
host: mpc2844.psi.ch
limits:
- 4.5
- 5.5
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
ftransx:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- 0
- 50
port: 8081
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
ftransy:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- -100
- 0
port: 8081
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
ftransz:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- 0
- 145
port: 8081
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
ftray:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
limits:
- -200
- 0
port: 8081
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
rtx:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
low_signal: 11000
min_signal: 10000
rt_pid_voltage: -0.06219
rty:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
tomo_additional_offsety: 0
rtz:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request

View File

@@ -1,340 +0,0 @@
fheater:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -15
- 0
port: 8082
sign: -1
onFailure: buffer
enabled: true
readOnly: false
feyex:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
limits:
- -30
- -1
port: 8082
sign: 1
onFailure: buffer
userParameter:
in: -16.267
out: -1
enabled: true
readOnly: false
feyey:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
limits:
- -1
- -10
port: 8082
sign: 1
onFailure: buffer
userParameter:
in: -10.467
enabled: true
readOnly: false
foptx:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -17
- -12
port: 8082
sign: 1
onFailure: buffer
userParameter:
in: -13.761
enabled: true
readOnly: false
fopty:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
limits:
- 0
- 4
port: 8082
sign: 1
onFailure: buffer
userParameter:
in: 0.552
out: 0.752
enabled: true
readOnly: false
foptz:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 0
- 27
port: 8082
sign: 1
onFailure: buffer
userParameter:
in: 23
enabled: true
readOnly: false
fosax:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 10.2
- 10.6
port: 3334
sign: -1
onFailure: buffer
userParameter:
in: 9.124
out: 5.3
enabled: true
readOnly: false
fosay:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -3.1
- -2.9
port: 3334
sign: -1
onFailure: buffer
userParameter:
in: 0.367
enabled: true
readOnly: false
fosaz:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -6
- -4
port: 3334
sign: 1
onFailure: buffer
userParameter:
in: 8.5
out: 6
enabled: true
readOnly: false
fsamroy:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- -5
- 365
port: 8084
sign: -1
onFailure: buffer
enabled: true
readOnly: false
fsamx:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
limits:
- -162
- 0
port: 8081
sign: 1
onFailure: buffer
enabled: true
readOnly: false
userParameter:
in: -1.1
fsamy:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
limits:
- 2
- 3.1
port: 8081
sign: 1
onFailure: buffer
enabled: true
readOnly: false
userParameter:
in: 2.75
ftracky:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: H
host: mpc2844.psi.ch
limits:
- 2.2
- 2.8
port: 8082
sign: 1
onFailure: buffer
enabled: true
readOnly: false
ftrackz:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: G
host: mpc2844.psi.ch
limits:
- 4.5
- 5.5
port: 8082
sign: 1
onFailure: buffer
enabled: true
readOnly: false
ftransx:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- 0
- 50
port: 8081
sign: 1
onFailure: buffer
enabled: true
readOnly: false
ftransy:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- -100
- 0
port: 8081
sign: 1
onFailure: buffer
enabled: true
readOnly: false
ftransz:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- 0
- 145
port: 8081
sign: 1
onFailure: buffer
enabled: true
readOnly: false
ftray:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
limits:
- -200
- 0
port: 8081
sign: -1
onFailure: buffer
enabled: true
readOnly: false
flomni_samples:
readoutPriority: baseline
description: phase plate angle
deviceClass: csaxs_bec.devices.epics.devices.FlomniSampleStorage
deviceConfig:
onFailure: buffer
enabled: true
readOnly: false
rtx:
readoutPriority: on_request
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
port: 2222
sign: 1
onFailure: buffer
enabled: true
readOnly: false
rty:
readoutPriority: on_request
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
port: 2222
sign: 1
onFailure: buffer
enabled: true
readOnly: false
rtz:
readoutPriority: on_request
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
port: 2222
sign: 1
onFailure: buffer
enabled: true
readOnly: false

File diff suppressed because it is too large Load Diff

View File

@@ -1,173 +0,0 @@
import os
import time
from bec_lib import MessageEndpoints, bec_logger, messages
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal
logger = bec_logger.logger
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]
# if (_eigerinvac_is_on == 1) {
# tic("setup eiger in vac")
# epics_put("X12SA-ES1-DOUBLE-02",0)
# unix(sprintf("mkdir -p /sls/X12SA/data/%s/Data10/eiger_4/S%05d-%05d/S%05d",_username,int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
# epics_put("XOMNYI-DET-OUTDIR:0.DESC",sprintf("/sls/X12SA/data/%s/Data10/eiger_4/",_username))
# epics_put("XOMNYI-DET-OUTDIR:1.DESC",sprintf("S%05d-%05d/S%05d",int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
# epics_put("XOMNYI-DET-INDEX:0", SCAN_N+1)
# epics_put("XOMNYI-DET-CONTROL:0.DESC", "begin")
# if(_eigerinvac_burst==0)
# {
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
# metadata_set("eiger_burst", "int", 1, 1)
# }
# else
# {
# metadata_set("eiger_burst", "int", 1, (int($2/0.0085)))
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts*(int($2/0.0085)))
# epics_put("XOMNYI-DET-EXPTIME:0", 0.005)
# }
# global _DC_acquisition_ID
# _DC_acquisition_ID = SCAN_N+1
class Eiger1p5MDetector(Device):
USER_ACCESS = []
file_path = Cpt(EpicsSignal, name="file_path", read_pv="XOMNYI-DET-OUTDIR:0.DESC")
detector_out_scan_dir = Cpt(
EpicsSignal, name="detector_out_scan_dir", read_pv="XOMNYI-DET-OUTDIR:1.DESC"
)
frames = Cpt(EpicsSignal, name="frames", read_pv="XOMNYI-DET-CYCLES:0")
exp_time = Cpt(EpicsSignal, name="exp_time", read_pv="XOMNYI-DET-EXPTIME:0")
index = Cpt(EpicsSignal, name="index", read_pv="XOMNYI-DET-INDEX:0")
detector_control = Cpt(
EpicsSignal, name="detector_control", read_pv="XOMNYI-DET-CONTROL:0.DESC"
)
framescaught = Cpt(EpicsSignalRO, name="framescaught", read_pv="XOMNYI-DET-CONTROL:0.VAL")
file_pattern = Cpt(_SLSDetectorConfigSignal, name="file_pattern", value="")
burst = Cpt(_SLSDetectorConfigSignal, name="burst", value=1)
save_file = Cpt(_SLSDetectorConfigSignal, name="save_file", value=False)
def __init__(self, *, name, kind=None, parent=None, device_manager=None, **kwargs):
self.device_manager = device_manager
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
self.sim_state = {
f"{self.name}_file_path": "~/Data10/data/",
f"{self.name}_file_pattern": f"{self.name}_{{:05d}}.h5",
f"{self.name}_frames": 1,
f"{self.name}_burst": 1,
f"{self.name}_save_file": False,
f"{self.name}_exp_time": 0,
}
self._stopped = False
self.file_name = ""
self.metadata = {}
self.username = "e20588" # TODO get from config
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
return self.device_manager.connector.get(MessageEndpoints.scan_status())
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
if leading_zeros is None:
leading_zeros = len(str(scan_bundle))
floor_dir = scan_number // scan_bundle * scan_bundle
return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}"
def stage(self) -> list[object]:
scan_msg = self._get_current_scan_msg()
self.metadata = {
"scan_id": scan_msg.content["scan_id"],
"RID": scan_msg.content["info"]["RID"],
"queue_id": scan_msg.content["info"]["queue_id"],
}
scan_number = scan_msg.content["info"]["scan_number"]
exp_time = scan_msg.content["info"]["exp_time"]
# set base path for detector output
self.file_path.set(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/")
# set scan directory (e.g. S00000-00999/S00020)
scan_dir = self._get_scan_dir(scan_bundle=1000, scan_number=scan_number, leading_zeros=5)
self.detector_out_scan_dir.set(scan_dir)
self.file_name = os.path.join(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/", scan_dir)
# set the scan number
self.index.set(scan_number)
# set the number of frames
self.frames.set(scan_msg.content["info"]["num_points"])
# set the exposure time
self.exp_time.set(exp_time)
# wait for detector control to become "ready"
while True:
det_ctrl = self.detector_control.get()
if det_ctrl == "ready":
break
time.sleep(0.005)
# send the "begin" flag to start processing the above commands
self.detector_control.set("begin")
# wait for detector to be "armed"
logger.info("Waiting for detector setup")
while True:
det_ctrl = self.detector_control.get()
if det_ctrl == "armed":
break
time.sleep(0.005)
self.detector_control.put("acquiring")
return super().stage()
def unstage(self) -> list[object]:
time_waited = 0
sleep_time = 0.2
framesexpected = self.frames.get()
while True:
framescaught = self.framescaught.get()
if self.framescaught.get() < framesexpected:
logger.info(
f"Waiting for frames: Transferred {framescaught} out of {framesexpected}"
)
time_waited += sleep_time
time.sleep(sleep_time)
if self._stopped:
break
continue
break
self.detector_control.put("stop")
signals = {"config": self.read(), "data": self.file_name}
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg)
self._stopped = False
return super().unstage()
def stop(self, *, success=False):
self.detector_control.put("stop")
super().stop(success=success)
self._stopped = True
if __name__ == "__main__":
eiger = Eiger1p5MDetector(name="eiger1p5m", label="eiger1p5m")
breakpoint()

View File

@@ -1,8 +0,0 @@
# Standard ophyd classes
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
from ophyd.quadem import QuadEM
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
from .devices.delay_generator_csaxs import DelayGeneratorcSAXS
from .devices.flomni_sample_storage import FlomniSampleStorage
from .devices.InsertionDevice import InsertionDevice

View File

@@ -1,28 +0,0 @@
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind, PVPositioner
class InsertionDevice(PVPositioner):
"""Python wrapper for the CSAXS insertion device control
This wrapper provides a positioner interface for the ID control.
is completely custom XBPM with templates directly in the
VME repo. Thus it needs a custom ophyd template as well...
WARN: The x and y are not updated by the IOC
"""
status = Component(EpicsSignalRO, "-USER:STATUS", auto_monitor=True)
errorSource = Component(EpicsSignalRO, "-USER:ERROR-SOURCE", auto_monitor=True)
isOpen = Component(EpicsSignalRO, "-GAP:ISOPEN", auto_monitor=True)
# PVPositioner interface
setpoint = Component(EpicsSignal, "-GAP:SET", auto_monitor=True)
readback = Component(EpicsSignalRO, "-GAP:READ", auto_monitor=True, kind=Kind.hinted)
done = Component(EpicsSignalRO, ":DONE", auto_monitor=True)
stop_signal = Component(EpicsSignal, "-GAP:STOP", kind=Kind.omitted)
# Automatically start simulation if directly invoked
# (NA for important devices)
if __name__ == "__main__":
pass

View File

@@ -1,136 +0,0 @@
import numpy as np
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO
class XbpmCsaxsOp(Device):
"""Python wrapper for custom XBPMs in the cSAXS optics hutch
This is completely custom XBPM with templates directly in the
VME repo. Thus it needs a custom ophyd template as well...
WARN: The x and y are not updated by the IOC
"""
sum = Component(EpicsSignalRO, "SUM", auto_monitor=True)
x = Component(EpicsSignalRO, "POSH", auto_monitor=True)
y = Component(EpicsSignalRO, "POSV", auto_monitor=True)
s1 = Component(EpicsSignalRO, "CHAN1", auto_monitor=True)
s2 = Component(EpicsSignalRO, "CHAN2", auto_monitor=True)
s3 = Component(EpicsSignalRO, "CHAN3", auto_monitor=True)
s4 = Component(EpicsSignalRO, "CHAN4", auto_monitor=True)
class XbpmBase(Device):
"""Python wrapper for X-ray Beam Position Monitors
XBPM's consist of a metal-coated diamond window that ejects
photoelectrons from the incoming X-ray beam. These electons
are collected and their current is measured. Effectively
they act as four quadrant photodiodes and are used as BPMs
at the undulator beamlines of SLS.
Note: EPICS provided signals are read only, but the user can
change the beam position offset.
"""
# Motor interface
s1 = Component(EpicsSignalRO, "Current1", auto_monitor=True)
s2 = Component(EpicsSignalRO, "Current2", auto_monitor=True)
s3 = Component(EpicsSignalRO, "Current3", auto_monitor=True)
s4 = Component(EpicsSignalRO, "Current4", auto_monitor=True)
sum = Component(EpicsSignalRO, "SumAll", auto_monitor=True)
asymH = Component(EpicsSignalRO, "asymH", auto_monitor=True)
asymV = Component(EpicsSignalRO, "asymV", auto_monitor=True)
x = Component(EpicsSignalRO, "X", auto_monitor=True)
y = Component(EpicsSignalRO, "Y", auto_monitor=True)
scaleH = Component(EpicsSignal, "PositionScaleX", auto_monitor=False)
offsetH = Component(EpicsSignal, "PositionOffsetX", auto_monitor=False)
scaleV = Component(EpicsSignal, "PositionScaleY", auto_monitor=False)
offsetV = Component(EpicsSignal, "PositionOffsetY", auto_monitor=False)
class XbpmSim(XbpmBase):
"""Python wrapper for simulated X-ray Beam Position Monitors
XBPM's consist of a metal-coated diamond window that ejects
photoelectrons from the incoming X-ray beam. These electons
are collected and their current is measured. Effectively
they act as four quadrant photodiodes and are used as BPMs
at the undulator beamlines of SLS.
Note: EPICS provided signals are read only, but the user can
change the beam position offset.
This simulation device extends the basic proxy with a script that
fills signals with quasi-randomized values.
"""
# Motor interface
s1w = Component(EpicsSignal, "Current1:RAW.VAL", auto_monitor=False)
s2w = Component(EpicsSignal, "Current2:RAW.VAL", auto_monitor=False)
s3w = Component(EpicsSignal, "Current3:RAW.VAL", auto_monitor=False)
s4w = Component(EpicsSignal, "Current4:RAW.VAL", auto_monitor=False)
rangew = Component(EpicsSignal, "RANGEraw", auto_monitor=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._MX = 0
self._MY = 0
self._I0 = 255.0
self._x = np.linspace(-5, 5, 64)
self._y = np.linspace(-5, 5, 64)
self._x, self._y = np.meshgrid(self._x, self._y)
def _simFrame(self):
"""Generator to simulate a jumping gaussian"""
# define normalized 2D gaussian
def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1):
return np.exp(-((x - mx) ** 2.0 / (2.0 * sx**2.0) + (y - my) ** 2.0 / (2.0 * sy**2.0)))
# Generator for dynamic values
self._MX = 0.75 * self._MX + 0.25 * (10.0 * np.random.random() - 5.0)
self._MY = 0.75 * self._MY + 0.25 * (10.0 * np.random.random() - 5.0)
self._I0 = 0.75 * self._I0 + 0.25 * (255.0 * np.random.random())
arr = self._I0 * gaus2d(self._x, self._y, self._MX, self._MY)
return arr
def sim(self):
# Get next frame
beam = self._simFrame()
total = np.sum(beam)
rnge = np.floor(np.log10(total) - 0.0)
s1 = np.sum(beam[32:64, 32:64]) / 10**rnge
s2 = np.sum(beam[0:32, 32:64]) / 10**rnge
s3 = np.sum(beam[32:64, 0:32]) / 10**rnge
s4 = np.sum(beam[0:32, 0:32]) / 10**rnge
self.s1w.set(s1).wait()
self.s2w.set(s2).wait()
self.s3w.set(s3).wait()
self.s4w.set(s4).wait()
self.rangew.set(rnge).wait()
# Print debug info
print(f"Raw signals: R={rnge}\t{s1}\t{s2}\t{s3}\t{s4}")
# plt.imshow(beam)
# plt.show(block=False)
# plt.pause(0.5)
# Automatically start simulation if directly invoked
if __name__ == "__main__":
xbpm1 = XbpmSim("X01DA-FE-XBPM1:", name="xbpm1")
xbpm2 = XbpmSim("X01DA-FE-XBPM2:", name="xbpm2")
xbpm1.wait_for_connection(timeout=5)
xbpm2.wait_for_connection(timeout=5)
xbpm1.rangew.set(1).wait()
xbpm2.rangew.set(1).wait()
while True:
print("---")
xbpm1.sim()
xbpm2.sim()

View File

@@ -1,14 +0,0 @@
# Standard ophyd classes
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
from ophyd.quadem import QuadEM
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
from .delay_generator_csaxs import DelayGeneratorcSAXS
from .eiger9m_csaxs import Eiger9McSAXS
# cSAXS
from .falcon_csaxs import FalconcSAXS
from .flomni_sample_storage import FlomniSampleStorage
from .InsertionDevice import InsertionDevice
from .mcs_csaxs import MCScSAXS
from .pilatus_csaxs import PilatuscSAXS

View File

@@ -1,32 +0,0 @@
""" TODO This class seems to be missing various imports and appears to have not been tested in motion yet."""
TABLES_DT_PUSH_DIST_MM = 890
class DetectorTableTheta(PseudoPositioner):
"""Detector table tilt motor
Small wrapper to adjust the detector table tilt as angle.
The table is pushed from one side by a single vertical motor.
Note: Rarely used!
"""
# Real axis (in degrees)
pusher = Component(EpicsMotor, "", name="pusher")
# Virtual axis
theta = Component(PseudoSingle, name="theta")
_real = ["pusher"]
@pseudo_position_argument
def forward(self, pseudo_pos):
return self.RealPosition(
pusher=tan(pseudo_pos.theta * 3.141592 / 180.0) * TABLES_DT_PUSH_DIST_MM
)
@real_position_argument
def inverse(self, real_pos):
return self.PseudoPosition(
theta=-180 * atan(real_pos.pusher / TABLES_DT_PUSH_DIST_MM) / 3.141592
)

View File

@@ -1,345 +0,0 @@
from bec_lib import bec_logger
from ophyd import Component
from ophyd_devices.epics.devices.psi_delay_generator_base import (
DDGCustomMixin,
PSIDelayGeneratorBase,
TriggerSource,
)
from ophyd_devices.utils import bec_utils
logger = bec_logger.logger
class DelayGeneratorError(Exception):
"""Exception raised for errors."""
class DDGSetup(DDGCustomMixin):
"""
Mixin class for DelayGenerator logic at cSAXS.
At cSAXS, multiple DDGs were operated at the same time. There different behaviour is
implemented in the ddg_config signals that are passed via the device config.
"""
def initialize_default_parameter(self) -> None:
"""Method to initialize default parameters."""
for ii, channel in enumerate(self.parent.all_channels):
self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel])
self.parent.set_channels("amplitude", self.parent.amplitude.get())
self.parent.set_channels("offset", self.parent.offset.get())
# Setup reference
self.parent.set_channels(
"reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs]
)
self.parent.set_channels(
"reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs]
)
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
# Set threshold level for ext. pulses
self.parent.level.put(self.parent.thres_trig_level.get())
def prepare_ddg(self) -> None:
"""
Method to prepare scan logic of cSAXS
Two scantypes are supported: "step" and "fly":
- step: Scan is performed by stepping the motor and acquiring data at each step
- fly: Scan is performed by moving the motor with a constant velocity and acquiring data
Custom logic for different DDG behaviour during scans.
- set_high_on_exposure : If True, then TTL signal is high during
the full exposure time of the scan (all frames).
E.g. Keep shutter open for the full scan.
- fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel.
If the value is 0, then the width of the TTL pulse is determined,
no matter which parameters are passed from the scaninfo for exposure time
- set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones
were: SINGLE_SHOT, EXT_RISING_EDGE
"""
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
# scantype "step"
if self.parent.scaninfo.scan_type == "step":
# High on exposure means that the signal
if self.parent.set_high_on_exposure.get():
# caluculate parameters
num_burst_cycle = 1 + self.parent.additional_triggers.get()
exp_time = (
self.parent.delta_width.get()
+ self.parent.scaninfo.frames_per_trigger
* (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time)
)
total_exposure = exp_time
delay_burst = self.parent.delay_burst.get()
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
if not self.parent.trigger_width.get():
self.parent.set_channels("width", exp_time)
else:
self.parent.set_channels("width", self.parent.trigger_width.get())
for value, channel in zip(
self.parent.fixed_ttl_width.get(), self.parent.all_channels
):
logger.debug(f"Trying to set DDG {channel} to {value}")
if value != 0:
self.parent.set_channels("width", value, channels=[channel])
else:
# caluculate parameters
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
total_exposure = exp_time + self.parent.scaninfo.readout_time
delay_burst = self.parent.delay_burst.get()
num_burst_cycle = (
self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get()
)
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
if not self.parent.trigger_width.get():
self.parent.set_channels("width", exp_time)
else:
self.parent.set_channels("width", self.parent.trigger_width.get())
# scantype "fly"
elif self.parent.scaninfo.scan_type == "fly":
if self.parent.set_high_on_exposure.get():
# caluculate parameters
exp_time = (
self.parent.delta_width.get()
+ self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points
+ self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1)
)
total_exposure = exp_time
delay_burst = self.parent.delay_burst.get()
num_burst_cycle = 1 + self.parent.additional_triggers.get()
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
if not self.parent.trigger_width.get():
self.parent.set_channels("width", exp_time)
else:
self.parent.set_channels("width", self.parent.trigger_width.get())
for value, channel in zip(
self.parent.fixed_ttl_width.get(), self.parent.all_channels
):
logger.debug(f"Trying to set DDG {channel} to {value}")
if value != 0:
self.parent.set_channels("width", value, channels=[channel])
else:
# caluculate parameters
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
total_exposure = exp_time + self.parent.scaninfo.readout_time
delay_burst = self.parent.delay_burst.get()
num_burst_cycle = (
self.parent.scaninfo.num_points + self.parent.additional_triggers.get()
)
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
if not self.parent.trigger_width.get():
self.parent.set_channels("width", exp_time)
else:
self.parent.set_channels("width", self.parent.trigger_width.get())
else:
raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}")
# Set common DDG parameters
self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
self.parent.set_channels("delay", 0.0)
def on_trigger(self) -> None:
"""Method to be executed upon trigger"""
if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT:
self.parent.trigger_shot.put(1)
def check_scan_id(self) -> None:
"""
Method to check if scan_id has changed.
If yes, then it changes parent.stopped to True, which will stop further actions.
"""
old_scan_id = self.parent.scaninfo.scan_id
self.parent.scaninfo.load_scan_metadata()
if self.parent.scaninfo.scan_id != old_scan_id:
self.parent.stopped = True
def finished(self) -> None:
"""Method checks if DDG finished acquisition"""
def on_pre_scan(self) -> None:
"""
Method called by pre_scan hook in parent class.
Executes trigger if premove_trigger is Trus.
"""
if self.parent.premove_trigger.get() is True:
self.parent.trigger_shot.put(1)
class DelayGeneratorcSAXS(PSIDelayGeneratorBase):
"""
DG645 delay generator at cSAXS (multiple can be in use depending on the setup)
Default values for setting up DDG.
Note: checks of set calues are not (only partially) included, check manual for details on possible settings.
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
- delay_burst : (float >=0) Delay between trigger and first pulse in burst mode
- delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition
- additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line)
- polarity : (list of 0/1) polarity for different channels
- amplitude : (float) amplitude voltage of TTLs
- offset : (float) offset for ampltitude
- thres_trig_level : (float) threshold of trigger amplitude
Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg):
- set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan.
# TODO trigger_width and fixed_ttl could be combined into single list.
- fixed_ttl_width : (list of either 1 or 0), one for each channel.
- trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value.
- set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG.
- premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan).
- set_high_on_stage : (bool) if True, then TTL signal should go high already on stage.
"""
custom_prepare_cls = DDGSetup
delay_burst = Component(
bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_config"
)
delta_width = Component(
bec_utils.ConfigSignal, name="delta_width", kind="config", config_storage_name="ddg_config"
)
additional_triggers = Component(
bec_utils.ConfigSignal,
name="additional_triggers",
kind="config",
config_storage_name="ddg_config",
)
polarity = Component(
bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config"
)
fixed_ttl_width = Component(
bec_utils.ConfigSignal,
name="fixed_ttl_width",
kind="config",
config_storage_name="ddg_config",
)
amplitude = Component(
bec_utils.ConfigSignal, name="amplitude", kind="config", config_storage_name="ddg_config"
)
offset = Component(
bec_utils.ConfigSignal, name="offset", kind="config", config_storage_name="ddg_config"
)
thres_trig_level = Component(
bec_utils.ConfigSignal,
name="thres_trig_level",
kind="config",
config_storage_name="ddg_config",
)
set_high_on_exposure = Component(
bec_utils.ConfigSignal,
name="set_high_on_exposure",
kind="config",
config_storage_name="ddg_config",
)
set_high_on_stage = Component(
bec_utils.ConfigSignal,
name="set_high_on_stage",
kind="config",
config_storage_name="ddg_config",
)
set_trigger_source = Component(
bec_utils.ConfigSignal,
name="set_trigger_source",
kind="config",
config_storage_name="ddg_config",
)
trigger_width = Component(
bec_utils.ConfigSignal,
name="trigger_width",
kind="config",
config_storage_name="ddg_config",
)
premove_trigger = Component(
bec_utils.ConfigSignal,
name="premove_trigger",
kind="config",
config_storage_name="ddg_config",
)
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
device_manager=None,
sim_mode=False,
ddg_config=None,
**kwargs,
):
"""
Args:
prefix (str, optional): Prefix of the device. Defaults to "".
name (str): Name of the device.
kind (str, optional): Kind of the device. Defaults to None.
read_attrs (list, optional): List of attributes to read. Defaults to None.
configuration_attrs (list, optional): List of attributes to configure. Defaults to None.
parent (Device, optional): Parent device. Defaults to None.
device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None.
sim_mode (bool, optional): Simulation mode flag. Defaults to False.
ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None.
"""
# Default values for ddg_config signals
self.ddg_config = {
# Setup default values
f"{name}_delay_burst": 0,
f"{name}_delta_width": 0,
f"{name}_additional_triggers": 0,
f"{name}_polarity": [1, 1, 1, 1, 1],
f"{name}_amplitude": 4.5,
f"{name}_offset": 0,
f"{name}_thres_trig_level": 2.5,
# Values for different behaviour during scans
f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0],
f"{name}_trigger_width": None,
f"{name}_set_high_on_exposure": False,
f"{name}_set_high_on_stage": False,
f"{name}_set_trigger_source": "SINGLE_SHOT",
f"{name}_premove_trigger": False,
}
if ddg_config is not None:
# pylint: disable=expression-not-assigned
[self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()]
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
device_manager=device_manager,
sim_mode=sim_mode,
**kwargs,
)
if __name__ == "__main__":
# Start delay generator in simulation mode.
# Note: To run, access to Epics must be available.
dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True)

View File

@@ -1,427 +0,0 @@
import enum
import os
import threading
import time
from typing import Any
import numpy as np
from bec_lib import messages, threadlocked
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from ophyd import ADComponent as ADCpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
from std_daq_client import StdDaqClient
logger = bec_logger.logger
class EigerError(Exception):
"""Base class for exceptions in this module."""
class EigerTimeoutError(EigerError):
"""Raised when the Eiger does not respond in time."""
class Eiger9MSetup(CustomDetectorMixin):
"""Eiger setup class
Parent class: CustomDetectorMixin
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self.std_rest_server_url = (
kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000"
)
self.std_client = None
self._lock = threading.RLock()
def initialize_default_parameter(self) -> None:
"""Set default parameters for Eiger9M detector"""
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize detector"""
# Stops the detector
self.stop_detector()
# Sets the trigger source to GATING
self.parent.set_trigger(TriggerSource.GATING)
def initialize_detector_backend(self) -> None:
"""Initialize detector backend"""
# Std client
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
# Stop writer
self.std_client.stop_writer()
# Change e-account
eacc = self.parent.scaninfo.username
self.update_std_cfg("writer_user_id", int(eacc.strip(" e")))
signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")]
if not self.wait_for_signals(
signal_conditions=signal_conditions, timeout=self.parent.timeout, all_signals=True
):
raise EigerTimeoutError(
f"Std client not in READY state, returns: {self.std_client.get_status()}"
)
def update_std_cfg(self, cfg_key: str, value: Any) -> None:
"""
Update std_daq config
Checks that the new value matches the type of the former entry.
Args:
cfg_key (str) : config key of value to be updated
value (Any) : value to be updated for the specified key
Raises:
Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value
"""
# Load config from client and check old value
cfg = self.std_client.get_config()
old_value = cfg.get(cfg_key)
if old_value is None:
raise EigerError(
f"Tried to change entry for key {cfg_key} in std_config that does not exist"
)
if not isinstance(value, type(old_value)):
raise EigerError(
f"Type of new value {type(value)}:{value} does not match old value"
f" {type(old_value)}:{old_value}"
)
# Update config with new value and send back to client
cfg.update({cfg_key: value})
logger.debug(cfg)
self.std_client.set_config(cfg)
logger.debug(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}")
def stop_detector(self) -> None:
"""Stop the detector"""
# Stop detector
self.parent.cam.acquire.put(0)
# Check if detector returned in idle state
signal_conditions = [
(
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
"value"
],
DetectorState.IDLE,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
check_stopped=True,
all_signals=False,
):
# Retry stop detector and wait for remaining time
self.parent.cam.acquire.put(0)
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
check_stopped=True,
all_signals=False,
):
raise EigerTimeoutError(
f"Failed to stop detector, detector state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None:
"""Close file writer"""
self.std_client.stop_writer()
def prepare_detector(self) -> None:
"""Prepare detector for scan"""
self.set_detector_threshold()
self.set_acquisition_params()
self.parent.set_trigger(TriggerSource.GATING)
def set_detector_threshold(self) -> None:
"""
Set the detector threshold
The function sets the detector threshold automatically to 1/2 of the beam energy.
"""
# get current beam energy from device manageer
mokev = self.parent.device_manager.devices.mokev.obj.read()[
self.parent.device_manager.devices.mokev.name
]["value"]
factor = 1
# Check if energies are eV or keV, assume keV as the default
unit = getattr(self.parent.cam.threshold_energy, "units", None)
if unit is not None and unit == "eV":
factor = 1000
# set energy on detector
setpoint = int(mokev * factor)
energy = self.parent.cam.beam_energy.read()[self.parent.cam.beam_energy.name]["value"]
if setpoint != energy:
self.parent.cam.beam_energy.set(setpoint)
# set threshold on detector
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
"value"
]
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
self.parent.cam.threshold_energy.set(setpoint / 2)
def set_acquisition_params(self) -> None:
"""Set acquisition parameters for the detector"""
# Set number of images and frames (frames is for internal burst of detector)
self.parent.cam.num_images.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.cam.num_frames.put(1)
# Update the readout time of the detector
self.update_readout_time()
def prepare_data_backend(self) -> None:
"""Prepare the data backend for the scan"""
self.parent.filepath = self.parent.filewriter.compile_full_filename(
f"{self.parent.name}.h5"
)
self.filepath_exists(self.parent.filepath)
self.stop_detector_backend()
try:
self.std_client.start_writer_async(
{
"output_file": self.parent.filepath,
"n_images": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
}
)
except Exception as exc:
time.sleep(5)
if self.std_client.get_status()["state"] == "READY":
raise EigerTimeoutError(f"Timeout of start_writer_async with {exc}") from exc
# Check status of std_daq
signal_conditions = [
(lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES")
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=False,
all_signals=True,
):
raise EigerTimeoutError(
"Timeout of 5s reached for std_daq start_writer_async with std_daq client status"
f" {self.std_client.get_status()}"
)
def filepath_exists(self, filepath: str) -> None:
"""Check if filepath exists"""
signal_conditions = [(lambda: os.path.exists(os.path.dirname(filepath)), True)]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=False,
all_signals=True,
):
raise EigerError(f"Timeout of 3s reached for filepath {filepath}")
def arm_acquisition(self) -> None:
"""Arm Eiger detector for acquisition"""
self.parent.cam.acquire.put(1)
signal_conditions = [
(
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
"value"
],
DetectorState.RUNNING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=False,
):
raise EigerTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def check_scan_id(self) -> None:
"""Checks if scan_id has changed and stops the scan if it has"""
old_scan_id = self.parent.scaninfo.scan_id
self.parent.scaninfo.load_scan_metadata()
if self.parent.scaninfo.scan_id != old_scan_id:
self.parent.stopped = True
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent.connector.pipeline()
if successful is None:
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
else:
msg = messages.FileMessage(
file_path=self.parent.filepath, done=done, successful=successful
)
self.parent.connector.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
msg,
pipe=pipe,
)
self.parent.connector.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
)
pipe.execute()
@threadlocked
def finished(self):
"""Check if acquisition is finished."""
signal_conditions = [
(
lambda: self.parent.cam.acquire.read()[self.parent.cam.acquire.name]["value"],
DetectorState.IDLE,
),
(lambda: self.std_client.get_status()["acquisition"]["state"], "FINISHED"),
(
lambda: self.std_client.get_status()["acquisition"]["stats"]["n_write_completed"],
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger),
),
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=True,
):
raise EigerTimeoutError(
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
" the file writer"
)
self.stop_detector()
self.stop_detector_backend()
class SLSDetectorCam(Device):
"""
SLS Detector Camera - Eiger9M
Base class to map EPICS PVs to ophyd signals.
"""
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
num_images = ADCpt(EpicsSignalWithRBV, "NumCycles")
num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
acquire = ADCpt(EpicsSignal, "Acquire")
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
class TriggerSource(enum.IntEnum):
"""Trigger signals for Eiger9M detector"""
AUTO = 0
TRIGGER = 1
GATING = 2
BURST_TRIGGER = 3
class DetectorState(enum.IntEnum):
"""Detector states for Eiger9M detector"""
IDLE = 0
ERROR = 1
WAITING = 2
FINISHED = 3
TRANSMITTING = 4
RUNNING = 5
STOPPED = 6
STILL_WAITING = 7
INITIALIZING = 8
DISCONNECTED = 9
ABORTED = 10
class Eiger9McSAXS(PSIDetectorBase):
"""
Eiger9M detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
Various EpicsPVs for controlling the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
# specify Setup class
custom_prepare_cls = Eiger9MSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
# specify class attributes
cam = ADCpt(SLSDetectorCam, "cam1:")
def set_trigger(self, trigger_source: TriggerSource) -> None:
"""Set trigger source for the detector.
Check the TriggerSource enum for possible values
Args:
trigger_source (TriggerSource): Trigger source for the detector
"""
value = trigger_source
self.cam.trigger_mode.put(value)
def stage(self) -> list[object]:
"""
Add functionality to stage, and arm the detector
Additional call to:
- custom_prepare.arm_acquisition()
"""
rtr = super().stage()
self.custom_prepare.arm_acquisition()
return rtr
if __name__ == "__main__":
eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)

View File

@@ -1,356 +0,0 @@
import enum
import os
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd.mca import EpicsMCARecord
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
logger = bec_logger.logger
class FalconError(Exception):
"""Base class for exceptions in this module."""
class FalconTimeoutError(FalconError):
"""Raised when the Falcon does not respond in time."""
class DetectorState(enum.IntEnum):
"""Detector states for Falcon detector"""
DONE = 0
ACQUIRING = 1
class TriggerSource(enum.IntEnum):
"""Trigger source for Falcon detector"""
USER = 0
GATE = 1
SYNC = 2
class MappingSource(enum.IntEnum):
"""Mapping source for Falcon detector"""
SPECTRUM = 0
MAPPING = 1
class EpicsDXPFalcon(Device):
"""
DXP parameters for Falcon detector
Base class to map EPICS PVs from DXP parameters to ophyd signals.
"""
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
# Energy Filter PVs
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
# Misc PVs
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
class FalconHDF5Plugins(Device):
"""
HDF5 parameters for Falcon detector
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
"""
capture = Cpt(EpicsSignalWithRBV, "Capture")
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
class FalconSetup(CustomDetectorMixin):
"""
Falcon setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def initialize_default_parameter(self) -> None:
"""
Set default parameters for Falcon
This will set:
- readout (float): readout time in seconds
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
"""
self.parent.value_pixel_per_buffer = 20
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize Falcon detector"""
self.stop_detector()
self.stop_detector_backend()
self.parent.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# 1 Realtime
self.parent.preset_mode.put(1)
# 0 Normal, 1 Inverted
self.parent.input_logic_polarity.put(0)
# 0 Manual 1 Auto
self.parent.auto_pixels_per_buffer.put(0)
# Sets the number of pixels/spectra in the buffer
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
def stop_detector(self) -> None:
"""Stops detector"""
self.parent.stop_all.put(1)
self.parent.erase_all.put(1)
signal_conditions = [
(lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
all_signals=False,
):
# Retry stop detector and wait for remaining time
raise FalconTimeoutError(
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None:
"""Stop the detector backend"""
self.parent.hdf5.capture.put(0)
def initialize_detector_backend(self) -> None:
"""Initialize the detector backend for Falcon."""
self.parent.hdf5.enable.put(1)
# file location of h5 layout for cSAXS
self.parent.hdf5.xml_file_name.put("layout.xml")
# TODO Check if lazy open is needed and wanted!
self.parent.hdf5.lazy_open.put(1)
self.parent.hdf5.temp_suffix.put("")
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
self.parent.hdf5.queue_size.put(2000)
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.parent.nd_array_mode.put(1)
def prepare_detector(self) -> None:
"""Prepare detector for acquisition"""
self.parent.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
self.parent.pixels_per_run.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
def prepare_data_backend(self) -> None:
"""Prepare data backend for acquisition"""
self.parent.filepath = self.parent.filewriter.compile_full_filename(
f"{self.parent.name}.h5"
)
file_path, file_name = os.path.split(self.parent.filepath)
self.parent.hdf5.file_path.put(file_path)
self.parent.hdf5.file_name.put(file_name)
self.parent.hdf5.file_template.put("%s%s")
self.parent.hdf5.num_capture.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.hdf5.file_write_mode.put(2)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.parent.hdf5.array_counter.put(0)
# Start file writing
self.parent.hdf5.capture.put(1)
def arm_acquisition(self) -> None:
"""Arm detector for acquisition"""
self.parent.start_all.put(1)
signal_conditions = [
(
lambda: self.parent.state.read()[self.parent.state.name]["value"],
DetectorState.ACQUIRING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=False,
):
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def check_scan_id(self) -> None:
"""Checks if scan_id has changed and stops the scan if it has"""
old_scan_id = self.parent.scaninfo.scan_id
self.parent.scaninfo.load_scan_metadata()
if self.parent.scaninfo.scan_id != old_scan_id:
self.parent.stopped = True
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent.connector.pipeline()
if successful is None:
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
else:
msg = messages.FileMessage(
file_path=self.parent.filepath, done=done, successful=successful
)
self.parent.connector.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
msg,
pipe=pipe,
)
self.parent.connector.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
)
pipe.execute()
def finished(self) -> None:
"""Check if scan finished succesfully"""
total_frames = int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
)
signal_conditions = [
(self.parent.dxp.current_pixel.get, total_frames),
(self.parent.hdf5.array_counter.get, total_frames),
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=True,
):
logger.debug(
f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()},"
f" send data {self.parent.hdf5.array_counter.get()} from total_frames"
f" {total_frames}"
)
self.stop_detector()
self.stop_detector_backend()
class FalconcSAXS(PSIDetectorBase):
"""
Falcon Sitoro detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
mca (EpicsMCARecord) : MCA parameters for Falcon detector
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
MIN_READOUT (float) : Minimum readout time for the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
# specify Setup class
custom_prepare_cls = FalconSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
# specify class attributes
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
mca = Cpt(EpicsMCARecord, "mca1")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
stop_all = Cpt(EpicsSignal, "StopAll")
erase_all = Cpt(EpicsSignal, "EraseAll")
start_all = Cpt(EpicsSignal, "StartAll")
state = Cpt(EpicsSignal, "Acquiring")
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
preset_real = Cpt(EpicsSignal, "PresetReal")
preset_events = Cpt(EpicsSignal, "PresetEvents")
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
def set_trigger(
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
) -> None:
"""
Set triggering mode for detector
Args:
mapping_mode (MappingSource): Mapping mode for the detector
trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal
ignore_gate (int): Ignore gate from TTL signal; defaults to 0
"""
mapping = int(mapping_mode)
trigger = trigger_source
self.collect_mode.put(mapping)
self.pixel_advance_mode.put(trigger)
self.ignore_gate.put(ignore_gate)
def stage(self) -> list[object]:
"""Stage"""
rtr = super().stage()
self.custom_prepare.arm_acquisition()
return rtr
if __name__ == "__main__":
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)

View File

@@ -1,116 +0,0 @@
import time
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal
from prettytable import PrettyTable
class FlomniSampleStorageError(Exception):
pass
class FlomniSampleStorage(Device):
USER_ACCESS = [
"is_sample_slot_used",
"is_sample_in_gripper",
"set_sample_slot",
"unset_sample_slot",
"set_sample_in_gripper",
"unset_sample_in_gripper",
"show_all",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
sample_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21)
}
sample_placed = Dcpt(sample_placed)
sample_names = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True})
for i in range(21)
}
sample_names = Dcpt(sample_names)
sample_in_gripper = Cpt(
EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET"
)
sample_in_gripper_name = Cpt(
EpicsSignal,
name="sample_in_gripper_name",
read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC",
string=True,
)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.sample_placed.sample1.subscribe(self._emit_value)
def _emit_value(self, **kwargs):
timestamp = kwargs.pop("timestamp", time.time())
self.wait_for_connection()
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
def set_sample_slot(self, slot_nr: int, name: str) -> bool:
if slot_nr > 20:
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
getattr(self.sample_placed, f"sample{slot_nr}").set(1)
getattr(self.sample_names, f"sample{slot_nr}").set(name)
def unset_sample_slot(self, slot_nr: int) -> bool:
if slot_nr > 20:
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
getattr(self.sample_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_names, f"sample{slot_nr}").set("-")
def set_sample_in_gripper(self, name: str) -> bool:
self.sample_in_gripper.set(1)
self.sample_in_gripper_name.set(name)
def unset_sample_in_gripper(self) -> bool:
self.sample_in_gripper.set(0)
self.sample_in_gripper_name.set("-")
def is_sample_slot_used(self, slot_nr: int) -> bool:
val = getattr(self.sample_placed, f"sample{slot_nr}").get()
return bool(val)
def is_sample_in_gripper(self) -> bool:
val = self.sample_in_gripper.get()
return bool(val)
def get_sample_name(self, slot_nr) -> str:
val = getattr(self.sample_names, f"sample{slot_nr}").get()
return str(val)
def show_all(self):
t = PrettyTable()
t.title = "flOMNI sample storage"
field_names = [""]
field_names.extend(str(ax) for ax in range(1, 11))
for ct in range(0, 2):
t.field_names = field_names
row = ["Container " + str(ct)]
row.extend(
"used" if self.is_sample_slot_used(slot_nr) else "free"
for slot_nr in range((ct * 10) + 1, (ct * 10) + 11)
)
t.add_row(row)
print(t)
print("\n\nFollowing samples are currently loaded:\n")
for ct in range(1, 21):
if self.is_sample_slot_used(ct):
print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}")
if self.sample_in_gripper.get():
print(f"\n Gripper: {self.sample_in_gripper_name.get()}\n")
else:
print(f"\n Gripper: no sample\n")
if self.is_sample_slot_used(0):
print(f" flOMNI stage: {self.get_sample_name(0)}\n")
else:
print(f" flOMNI stage: no sample\n")

View File

@@ -1,310 +0,0 @@
import enum
import threading
from collections import defaultdict
import numpy as np
from bec_lib import MessageEndpoints, bec_logger, messages, threadlocked
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
from ophyd_devices.utils import bec_utils
logger = bec_logger.logger
class MCSError(Exception):
"""Base class for exceptions in this module."""
class MCSTimeoutError(MCSError):
"""Raise when MCS card runs into a timeout"""
class TriggerSource(int, enum.Enum):
"""Trigger source for mcs card - see manual for more information"""
MODE0 = 0
MODE1 = 1
MODE2 = 2
MODE3 = 3
MODE4 = 4
MODE5 = 5
MODE6 = 6
class ChannelAdvance(int, enum.Enum):
"""Channel advance pixel mode for mcs card - see manual for more information"""
INTERNAL = 0
EXTERNAL = 1
class ReadoutMode(int, enum.Enum):
"""Readout mode for mcs card - see manual for more information"""
PASSIVE = 0
EVENT = 1
IO_INTR = 2
FREQ_0_1HZ = 3
FREQ_0_2HZ = 4
FREQ_0_5HZ = 5
FREQ_1HZ = 6
FREQ_2HZ = 7
FREQ_5HZ = 8
FREQ_10HZ = 9
FREQ_100HZ = 10
class MCSSetup(CustomDetectorMixin):
"""Setup mixin class for the MCS card"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self._lock = threading.RLock()
self._stream_ttl = 1800
self.acquisition_done = False
self.counter = 0
self.n_points = 0
self.mca_names = [
signal for signal in self.parent.component_names if signal.startswith("mca")
]
self.mca_data = defaultdict(lambda: [])
def initialize_detector(self) -> None:
"""Initialize detector"""
# External trigger for pixel advance
self.parent.channel_advance.set(ChannelAdvance.EXTERNAL)
# Use internal clock for channel 1
self.parent.channel1_source.set(ChannelAdvance.INTERNAL)
self.parent.user_led.set(0)
# Set number of channels to 5
self.parent.mux_output.set(5)
# Trigger Mode used for cSAXS
self.parent.set_trigger(TriggerSource.MODE3)
# specify polarity of trigger signals
self.parent.input_polarity.set(0)
self.parent.output_polarity.set(1)
# do not start counting on start
self.parent.count_on_start.set(0)
self.stop_detector()
def initialize_detector_backend(self) -> None:
"""Initialize detector backend"""
for mca in self.mca_names:
signal = getattr(self.parent, mca)
signal.subscribe(self._on_mca_data, run=False)
self.parent.current_channel.subscribe(self._progress_update, run=False)
def _progress_update(self, value, **kwargs) -> None:
"""Progress update on the scan"""
num_lines = self.parent.num_lines.get()
max_value = self.parent.scaninfo.num_points
# self.counter seems to be a deprecated variable from a former implementation of the mcs card
# pylint: disable=protected-access
self.parent._run_subs(
sub_type=self.parent.SUB_PROGRESS,
value=self.counter * int(self.parent.scaninfo.num_points / num_lines) + value,
max_value=max_value,
# TODO check if that is correct with
done=bool(max_value == value), # == self.counter),
)
@threadlocked
def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None:
"""Callback function for scan progress"""
if not isinstance(value, (list, np.ndarray)):
return
self.mca_data[obj.attr_name] = value
if len(self.mca_names) != len(self.mca_data):
return
self.acquisition_done = True
self._send_data_to_bec()
self.mca_data = defaultdict(lambda: [])
def _send_data_to_bec(self) -> None:
"""Sends bundled data to BEC"""
if self.parent.scaninfo.scan_msg is None:
return
metadata = self.parent.scaninfo.scan_msg.metadata
metadata.update({"async_update": "append", "num_lines": self.parent.num_lines.get()})
msg = messages.DeviceMessage(
signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata
)
self.parent.connector.xadd(
topic=MessageEndpoints.device_async_readback(
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
),
msg={"data": msg},
expire=self._stream_ttl,
)
def prepare_detector(self) -> None:
"""Prepare detector for scan"""
self.set_acquisition_params()
self.parent.set_trigger(TriggerSource.MODE3)
def set_acquisition_params(self) -> None:
"""Set acquisition parameters for scan"""
if self.parent.scaninfo.scan_type == "step":
self.n_points = int(self.parent.scaninfo.frames_per_trigger) * int(
self.parent.scaninfo.num_points
)
elif self.parent.scaninfo.scan_type == "fly":
self.n_points = int(self.parent.scaninfo.num_points) # / int(self.num_lines.get()))
else:
raise MCSError(f"Scantype {self.parent.scaninfo} not implemented for MCS card")
if self.n_points > 10000:
raise MCSError(
f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card"
" 10000 (N-1)"
)
self.parent.num_use_all.set(self.n_points)
self.parent.preset_real.set(0)
def prepare_detector_backend(self) -> None:
"""Prepare detector backend for scan"""
self.parent.erase_all.set(1)
self.parent.read_mode.set(ReadoutMode.EVENT)
def arm_acquisition(self) -> None:
"""Arm detector for acquisition"""
self.counter = 0
self.parent.erase_start.set(1)
def finished(self) -> None:
"""Check if acquisition is finished, if not successful, rais MCSTimeoutError"""
signal_conditions = [
(lambda: self.acquisition_done, True),
(self.parent.acquiring.get, 0), # Considering making a enum.Int class for this state
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=True,
):
total_frames = self.counter * int(
self.parent.scaninfo.num_points / self.parent.num_lines.get()
) + max(self.parent.current_channel.get(), 0)
raise MCSTimeoutError(
f"Reached timeout with mcs in state {self.parent.acquiring.get()} and"
f" {total_frames} frames arriving at the mcs card"
)
def stop_detector(self) -> None:
"""Stop detector"""
self.parent.stop_all.set(1)
return super().stop_detector()
def stop_detector_backend(self) -> None:
"""Stop acquisition of data"""
self.acquisition_done = True
class SIS38XX(Device):
"""SIS38XX card for access to EPICs PVs at cSAXS beamline"""
class MCScSAXS(PSIDetectorBase):
"""MCS card for cSAXS for implementation at cSAXS beamline"""
USER_ACCESS = ["describe", "_init_mcs"]
SUB_PROGRESS = "progress"
SUB_VALUE = "value"
_default_sub = SUB_VALUE
# specify Setup class
custom_prepare_cls = MCSSetup
# specify minimum readout time for detector
MIN_READOUT = 0
# PV access to SISS38XX card
# Acquisition
erase_all = Cpt(EpicsSignal, "EraseAll")
erase_start = Cpt(EpicsSignal, "EraseStart") # ,trigger_value=1
start_all = Cpt(EpicsSignal, "StartAll")
stop_all = Cpt(EpicsSignal, "StopAll")
acquiring = Cpt(EpicsSignal, "Acquiring")
preset_real = Cpt(EpicsSignal, "PresetReal")
elapsed_real = Cpt(EpicsSignal, "ElapsedReal")
read_mode = Cpt(EpicsSignal, "ReadAll.SCAN")
read_all = Cpt(EpicsSignal, "DoReadAll.VAL") # ,trigger_value=1
num_use_all = Cpt(EpicsSignal, "NuseAll")
current_channel = Cpt(EpicsSignal, "CurrentChannel")
dwell = Cpt(EpicsSignal, "Dwell")
channel_advance = Cpt(EpicsSignal, "ChannelAdvance")
count_on_start = Cpt(EpicsSignal, "CountOnStart")
software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance")
channel1_source = Cpt(EpicsSignal, "Channel1Source")
prescale = Cpt(EpicsSignal, "Prescale")
enable_client_wait = Cpt(EpicsSignal, "EnableClientWait")
client_wait = Cpt(EpicsSignal, "ClientWait")
acquire_mode = Cpt(EpicsSignal, "AcquireMode")
mux_output = Cpt(EpicsSignal, "MUXOutput")
user_led = Cpt(EpicsSignal, "UserLED")
input_mode = Cpt(EpicsSignal, "InputMode")
input_polarity = Cpt(EpicsSignal, "InputPolarity")
output_mode = Cpt(EpicsSignal, "OutputMode")
output_polarity = Cpt(EpicsSignal, "OutputPolarity")
model = Cpt(EpicsSignalRO, "Model", string=True)
firmware = Cpt(EpicsSignalRO, "Firmware")
max_channels = Cpt(EpicsSignalRO, "MaxChannels")
# PV access to MCA signals
mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True)
mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True)
mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True)
current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True)
# Custom signal readout from device config
num_lines = Cpt(
bec_utils.ConfigSignal, name="num_lines", kind="config", config_storage_name="mcs_config"
)
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
device_manager=None,
sim_mode=False,
mcs_config=None,
**kwargs,
):
self.mcs_config = {f"{name}_num_lines": 1}
if mcs_config is not None:
# pylint: disable=expression-not-assigned
[self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()]
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
device_manager=device_manager,
sim_mode=sim_mode,
**kwargs,
)
def set_trigger(self, trigger_source: TriggerSource) -> None:
"""Set trigger mode from TriggerSource"""
value = int(trigger_source)
self.input_mode.set(value)
def stage(self) -> list[object]:
"""stage the detector for upcoming acquisition"""
rtr = super().stage()
self.custom_prepare.arm_acquisition()
return rtr
# Automatically connect to test environmenr if directly invoked
if __name__ == "__main__":
mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True)

View File

@@ -1,282 +0,0 @@
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, FRAME
class OMNYSampleStorageError(Exception):
pass
class OMNYSampleStorage(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",
"set_sample_in_samplestage",
"unset_sample_in_samplestage",
"get_sample_name_in_samplestage",
"get_sample_name",
"is_sample_in_samplestage",
"set_shuttle_slot",
"unset_shuttle_slot",
"get_shuttle_name_slot",
"is_shuttle_slot_used",
"search_shuttle_in_slot",
"show_all",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
sample_shuttle_A_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}", {}) for i in range(1, 7)
}
sample_shuttle_A_placed = Dcpt(sample_shuttle_A_placed)
sample_shuttle_B_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}", {}) for i in range(1, 7)
}
sample_shuttle_B_placed = Dcpt(sample_shuttle_B_placed)
sample_shuttle_C_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7)
}
sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed)
sample_shuttle_C_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7)
}
sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed)
parking_placed = {
f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}", {}) for i in range(1, 7)
}
parking_placed = Dcpt(parking_placed)
sample_placed = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {})
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]
}
sample_placed = Dcpt(sample_placed)
sample_shuttle_A_names = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}.DESC", {"string": True})
for i in range(1, 7)
}
sample_shuttle_A_names = Dcpt(sample_shuttle_A_names)
sample_shuttle_B_names = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}.DESC", {"string": True})
for i in range(1, 7)
}
sample_shuttle_B_names = Dcpt(sample_shuttle_B_names)
sample_shuttle_C_names = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}.DESC", {"string": True})
for i in range(1, 7)
}
sample_shuttle_C_names = Dcpt(sample_shuttle_C_names)
parking_names = {
f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}.DESC", {"string": True})
for i in range(1, 7)
}
parking_names = Dcpt(parking_names)
sample_names = {
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}.DESC", {"string": True})
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]
}
sample_names = Dcpt(sample_names)
sample_in_gripper = Cpt(
EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_omny:110.VAL"
)
sample_in_gripper_name = Cpt(
EpicsSignal,
name="sample_in_gripper_name",
read_pv="XOMNY-SAMPLE_DB_omny:110.DESC",
string=True,
)
sample_in_samplestage = Cpt(
EpicsSignal, name="sample_in_samplestage", read_pv="XOMNY-SAMPLE_DB_omny:0.VAL"
)
sample_in_samplestage_name = Cpt(
EpicsSignal,
name="sample_in_samplestage_name",
read_pv="XOMNY-SAMPLE_DB_omny:0.DESC",
string=True,
)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.sample_shuttle_A_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, container: str, slot_nr: int, name: str) -> bool:
if slot_nr > 20:
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
if container == "A":
getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(1)
getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set(name)
elif container == "B":
getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(1)
getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set(name)
elif container == "C":
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(1)
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set(name)
def unset_sample_slot(self, shuttle: str, slot_nr: int) -> bool:
if slot_nr > 20:
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
if shuttle == "A":
getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set("-")
if shuttle == "B":
getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set("-")
if shuttle == "C":
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set("-")
def set_shuttle_slot(self, container: str, slot_nr: int) -> bool:
if slot_nr > 6:
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
getattr(self.parking_placed, f"parking{slot_nr}").set(1)
getattr(self.parking_names, f"parking{slot_nr}").set(container)
def unset_shuttle_slot(self, slot_nr: int) -> bool:
if slot_nr > 6:
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
getattr(self.parking_placed, f"parking{slot_nr}").set(0)
getattr(self.parking_names, f"parking{slot_nr}").set("none")
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 set_sample_in_samplestage(self, name: str) -> bool:
self.sample_in_samplestage.set(1)
self.sample_in_samplestage_name.set(name)
def unset_sample_in_samplestage(self) -> bool:
self.sample_in_samplestage.set(0)
self.sample_in_samplestage_name.set("-")
def is_sample_slot_used(self, container, slot_nr: int) -> bool:
if container == "A":
val = getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").get()
if container == "B":
val = getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").get()
if container == "C":
val = getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").get()
elif container == "O":
val = getattr(self.sample_placed, f"sample{slot_nr}").get()
return bool(val)
def is_shuttle_slot_used(self, slot_nr: int) -> bool:
val = getattr(self.parking_placed, f"parking{slot_nr}").get()
return bool(val)
def is_sample_in_gripper(self) -> bool:
val = self.sample_in_gripper.get()
return bool(val)
def is_sample_in_samplestage(self) -> bool:
val = self.sample_in_samplestage.get()
return bool(val)
def get_sample_name(self, container, slot_nr) -> str:
if container == "A":
val = getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").get()
elif container == "B":
val = getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").get()
elif container == "C":
val = getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").get()
elif container == "O":
val = getattr(self.sample_names, f"sample{slot_nr}").get()
else:
val = "unknown container"
return str(val)
def get_shuttle_name_slot(self, slot_nr: int) -> str:
val = getattr(self.parking_names, f"parking{slot_nr}").get()
return str(val)
def get_sample_name_in_gripper(self) -> str:
val = self.sample_in_gripper_name.get()
return str(val)
def get_sample_name_in_samplestage(self) -> str:
val = self.sample_in_samplestage_name.get()
return str(val)
def search_shuttle_in_slot(self, shuttle: str) -> int:
returnvalue = 0
for i in range(1, 7):
if self.get_shuttle_name_slot(i) == shuttle:
returnvalue = i
return returnvalue
def show_all(self):
t = PrettyTable()
red = "\x1b[91m"
green = "\x1b[92m"
white = "\x1b[0m"
for ch in ["A", "B", "C"]:
t.clear()
shuttle_in_slot = self.search_shuttle_in_slot(ch)
if shuttle_in_slot > 0:
t.title = green + "Shuttle " + ch + " in OMNY slot " + str(shuttle_in_slot) + white
else:
t.title = red + "Shuttle " + ch + white
field_names = [""]
for ax in [1, 3, 5]:
row = []
row.extend([self.get_sample_name(ch, ax)])
row.extend(str(ax))
row.extend(str(ax + 1))
row.extend([self.get_sample_name(ch, ax + 1)])
t.add_row(row)
t.header = False
t.vrules = FRAME
print(t)
if self.is_sample_in_samplestage():
print(f"\n\n Sample stage: {self.get_sample_name_in_samplestage()}")
else:
print(f"\n\n Sample stage: no sample")
if self.is_sample_in_gripper():
print(f"\n Gripper: {self.get_sample_name_in_gripper()}\n")
else:
print(f"\n Gripper: no sample\n")
t.clear()
t.title = "Fixed positions in OMNY"
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]:
row = []
row.extend([f"Position {i:3d}"])
if self.is_sample_slot_used("O", i):
row.extend(self.get_sample_name("O", i))
else:
row.extend(["free"])
t.add_row(row)
print(t)

View File

@@ -1,416 +0,0 @@
import enum
import json
import os
import time
import numpy as np
import requests
from bec_lib import MessageEndpoints, bec_logger, messages
from ophyd import ADComponent as ADCpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
logger = bec_logger.logger
MIN_READOUT = 3e-3
class PilatusError(Exception):
"""Base class for exceptions in this module."""
class PilatusTimeoutError(PilatusError):
"""Raised when the Pilatus does not respond in time during unstage."""
class TriggerSource(enum.IntEnum):
"""Trigger source options for the detector"""
INTERNAL = 0
EXT_ENABLE = 1
EXT_TRIGGER = 2
MULTI_TRIGGER = 3
ALGINMENT = 4
class SLSDetectorCam(Device):
"""SLS Detector Camera - Pilatus
Base class to map EPICS PVs to ophyd signals.
"""
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
acquire = ADCpt(EpicsSignal, "Acquire")
armed = ADCpt(EpicsSignalRO, "Armed")
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True)
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
class PilatusSetup(CustomDetectorMixin):
"""Pilatus setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def initialize_default_parameter(self) -> None:
"""Set default parameters for Eiger9M detector"""
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize detector"""
# Stops the detector
self.stop_detector()
# Sets the trigger source to GATING
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
def prepare_detector(self) -> None:
"""
Prepare detector for scan.
Includes checking the detector threshold,
setting the acquisition parameters and setting the trigger source
"""
self.set_detector_threshold()
self.set_acquisition_params()
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
def set_detector_threshold(self) -> None:
"""
Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance
Threshold might be in ev or keV
"""
# get current beam energy from device manageer
mokev = self.parent.device_manager.devices.mokev.obj.read()[
self.parent.device_manager.devices.mokev.name
]["value"]
factor = 1
# Check if energies are eV or keV, assume keV as the default
unit = getattr(self.parent.cam.threshold_energy, "units", None)
if unit is not None and unit == "eV":
factor = 1000
# set energy on detector
setpoint = int(mokev * factor)
# set threshold on detector
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
"value"
]
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
self.parent.cam.threshold_energy.set(setpoint / 2)
def set_acquisition_params(self) -> None:
"""Set acquisition parameters for the detector"""
# Set number of images and frames (frames is for internal burst of detector)
self.parent.cam.num_images.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.cam.num_frames.put(1)
# Update the readout time of the detector
self.update_readout_time()
def create_directory(self, filepath: str) -> None:
"""Create directory if it does not exist"""
os.makedirs(filepath, exist_ok=True)
def stop_detector_backend(self) -> None:
"""Stop the file writer zmq service for pilatus_2"""
self.close_file_writer()
time.sleep(0.1)
self.stop_file_writer()
time.sleep(0.1)
def close_file_writer(self) -> None:
"""
Close the file writer for pilatus_2
Delete the data from x12sa-pd-2
"""
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
try:
res = self.send_requests_delete(url=url)
if not res.ok:
res.raise_for_status()
except Exception as exc:
logger.info(f"Pilatus2 close threw Exception: {exc}")
def stop_file_writer(self) -> None:
"""
Stop the file writer for pilatus_2
Runs on xbl-daq-34
"""
url = "http://xbl-daq-34:8091/pilatus_2/stop"
res = self.send_requests_put(url=url)
if not res.ok:
res.raise_for_status()
def prepare_data_backend(self) -> None:
"""
Prepare the detector backend of pilatus for a scan
A zmq service is running on xbl-daq-34 that is waiting
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
"""
self.stop_detector_backend()
self.parent.filepath = self.parent.filewriter.compile_full_filename("pilatus_2.h5")
self.parent.cam.file_path.put("/dev/shm/zmq/")
self.parent.cam.file_name.put(
f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}"
)
self.parent.cam.auto_increment.put(1) # auto increment
self.parent.cam.file_number.put(0) # first iter
self.parent.cam.file_format.put(0) # 0: TIFF
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
# TODO better to remove hard coded path with link to home directory/pilatus_2
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
self.parent.filepath_raw = os.path.join(
basepath,
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
)
# Make directory if needed
self.create_directory(self.parent.filepath_raw)
headers = {"Content-Type": "application/json", "Accept": "application/json"}
# start the stream on x12sa-pd-2
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
data_msg = {
"source": [
{
"searchPath": "/",
"searchPattern": "glob:*.cbf",
"destinationPath": self.parent.filepath_raw,
}
]
}
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# start the data receiver on xbl-daq-34
url = "http://xbl-daq-34:8091/pilatus_2/run"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"addr": "tcp://x12sa-pd-2:8888",
"dst": ["file"],
"numFrm": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
"ifType": "PULL",
"user": self.parent.scaninfo.username,
},
]
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# Wait for server to become available again
time.sleep(0.1)
logger.info(f"{res.status_code} -{res.text} - {res.content}")
# Send requests.put to xbl-daq-34 to wait for data
url = "http://xbl-daq-34:8091/pilatus_2/wait"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"frmCnt": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
},
]
try:
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res}")
if not res.ok:
res.raise_for_status()
except Exception as exc:
logger.info(f"Pilatus2 wait threw Exception: {exc}")
def send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
"""
Send a put request to the given url
Args:
url (str): url to send the request to
data (dict): data to be sent with the request (optional)
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
def send_requests_delete(self, url: str, headers: dict = None) -> object:
"""
Send a delete request to the given url
Args:
url (str): url to send the request to
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.delete(url=url, headers=headers, timeout=5)
def pre_scan(self) -> None:
"""
Pre_scan function call
This function is called just before the scan core.
Here it is used to arm the detector for the acquisition
"""
self.arm_acquisition()
def arm_acquisition(self) -> None:
"""Arms the detector for the acquisition"""
self.parent.cam.acquire.put(1)
# TODO is this sleep needed? to be tested with detector and for how long
time.sleep(0.5)
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS and publish the event for the h5_converter
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent.connector.pipeline()
if successful is None:
msg = messages.FileMessage(
file_path=self.parent.filepath,
done=done,
metadata={"input_path": self.parent.filepath_raw},
)
else:
msg = messages.FileMessage(
file_path=self.parent.filepath,
done=done,
successful=successful,
metadata={"input_path": self.parent.filepath_raw},
)
self.parent.connector.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
msg,
pipe=pipe,
)
self.parent.connector.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
)
pipe.execute()
def finished(self) -> None:
"""Check if acquisition is finished."""
# pylint: disable=protected-access
# TODO: at the moment this relies on device.mcs.obj._staged attribute
signal_conditions = [
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=True,
):
raise PilatusTimeoutError(
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
" the file writer"
)
self.stop_detector()
self.stop_detector_backend()
def stop_detector(self) -> None:
"""Stop detector"""
self.parent.cam.acquire.put(0)
def check_scan_id(self) -> None:
"""Checks if scan_id has changed and stops the scan if it has"""
old_scan_id = self.parent.scaninfo.scan_id
self.parent.scaninfo.load_scan_metadata()
if self.parent.scaninfo.scan_id != old_scan_id:
self.parent.stopped = True
class PilatuscSAXS(PSIDetectorBase):
"""Pilatus_2 300k detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
MIN_READOUT (float) : Minimum readout time for the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
# specify Setup class
custom_prepare_cls = PilatusSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
# specify class attributes
cam = ADCpt(SLSDetectorCam, "cam1:")
def set_trigger(self, trigger_source: TriggerSource) -> None:
"""Set trigger source for the detector"""
value = trigger_source
self.cam.trigger_mode.put(value)
if __name__ == "__main__":
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)

View File

@@ -1,304 +0,0 @@
# -*- coding: utf-8 -*-
"""
Created on Wed Oct 13 18:06:15 2021
@author: mohacsi_i
IMPORTANT: Virtual monochromator axes should be implemented already in EPICS!!!
"""
import time
from math import asin, atan, isclose, sin, sqrt, tan
import numpy as np
from ophyd import (
Component,
Device,
EpicsMotor,
EpicsSignal,
EpicsSignalRO,
Kind,
PseudoPositioner,
PseudoSingle,
PVPositioner,
Signal,
)
from ophyd.pseudopos import pseudo_position_argument, real_position_argument
class PmMonoBender(PseudoPositioner):
"""Monochromator bender
Small wrapper to combine the four monochromator bender motors.
"""
# Real axes
ai = Component(EpicsMotor, "TRYA", name="ai")
bo = Component(EpicsMotor, "TRYB", name="bo")
co = Component(EpicsMotor, "TRYC", name="co")
di = Component(EpicsMotor, "TRYD", name="di")
# Virtual axis
bend = Component(PseudoSingle, name="bend")
_real = ["ai", "bo", "co", "di"]
@pseudo_position_argument
def forward(self, pseudo_pos):
delta = pseudo_pos.bend - 0.25 * (
self.ai.position + self.bo.position + self.co.position + self.di.position
)
return self.RealPosition(
ai=self.ai.position + delta,
bo=self.bo.position + delta,
co=self.co.position + delta,
di=self.di.position + delta,
)
@real_position_argument
def inverse(self, real_pos):
return self.PseudoPosition(
bend=0.25 * (real_pos.ai + real_pos.bo + real_pos.co + real_pos.di)
)
def r2d(radians):
return radians * 180 / 3.141592
def d2r(degrees):
return degrees * 3.141592 / 180.0
class PmDetectorRotation(PseudoPositioner):
"""Detector rotation pseudo motor
Small wrapper to convert detector pusher position to rotation angle.
"""
_tables_dt_push_dist_mm = 890
# Real axes
dtpush = Component(EpicsMotor, "", name="dtpush")
# Virtual axis
dtth = Component(PseudoSingle, name="dtth")
_real = ["dtpush"]
@pseudo_position_argument
def forward(self, pseudo_pos):
return self.RealPosition(
dtpush=d2r(tan(-3.14 / 180 * pseudo_pos.dtth)) * self._tables_dt_push_dist_mm
)
@real_position_argument
def inverse(self, real_pos):
return self.PseudoPosition(dtth=r2d(-atan(real_pos.dtpush / self._tables_dt_push_dist_mm)))
class GirderMotorX1(PVPositioner):
"""Girder X translation pseudo motor"""
setpoint = Component(EpicsSignal, ":X_SET", name="sp")
readback = Component(EpicsSignalRO, ":X1", name="rbv")
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
class GirderMotorY1(PVPositioner):
"""Girder Y translation pseudo motor"""
setpoint = Component(EpicsSignal, ":Y_SET", name="sp")
readback = Component(EpicsSignalRO, ":Y1", name="rbv")
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
class GirderMotorYAW(PVPositioner):
"""Girder YAW pseudo motor"""
setpoint = Component(EpicsSignal, ":YAW_SET", name="sp")
readback = Component(EpicsSignalRO, ":YAW1", name="rbv")
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
class GirderMotorROLL(PVPositioner):
"""Girder ROLL pseudo motor"""
setpoint = Component(EpicsSignal, ":ROLL_SET", name="sp")
readback = Component(EpicsSignalRO, ":ROLL1", name="rbv")
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
class GirderMotorPITCH(PVPositioner):
"""Girder YAW pseudo motor"""
setpoint = Component(EpicsSignal, ":PITCH_SET", name="sp")
readback = Component(EpicsSignalRO, ":PITCH1", name="rbv")
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
class VirtualEpicsSignalRO(EpicsSignalRO):
"""This is a test class to create derives signals from one or
multiple original signals...
"""
def calc(self, val):
return val
def get(self, *args, **kwargs):
raw = super().get(*args, **kwargs)
return self.calc(raw)
class MonoTheta1(VirtualEpicsSignalRO):
"""Converts the pusher motor position to theta angle"""
_mono_a0_enc_scale1 = -1.0
_mono_a1_lever_length1 = 206.706
_mono_a2_pusher_offs1 = 6.85858
_mono_a3_enc_offs1 = -16.9731
def calc(self, val):
asin_arg = (val - self._mono_a2_pusher_offs1) / self._mono_a1_lever_length1
theta1 = (
self._mono_a0_enc_scale1 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs1
)
return theta1
class MonoTheta2(VirtualEpicsSignalRO):
"""Converts the pusher motor position to theta angle"""
_mono_a3_enc_offs2 = -19.7072
_mono_a2_pusher_offs2 = 5.93905
_mono_a1_lever_length2 = 206.572
_mono_a0_enc_scale2 = -1.0
def calc(self, val):
asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2
theta2 = (
self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2
)
return theta2
MONO_THETA2_OFFSETS_FILENAME = (
"/sls/X12SA/data/gac-x12saop/spec/macros/spec_data/mono_th2_offsets.txt"
)
class EnergyKev(VirtualEpicsSignalRO):
"""Converts the pusher motor position to energy in keV"""
_mono_add_offs = True
_mono_a3_enc_offs2 = -19.7072
_mono_a2_pusher_offs2 = 5.93905
_mono_a1_lever_length2 = 206.572
_mono_a0_enc_scale2 = -1.0
_mono_hce = 12.39852066
_mono_2d2 = 2 * 5.43102 / sqrt(3)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._th2_offsets = np.loadtxt(MONO_THETA2_OFFSETS_FILENAME)
def _mono_get_th2_offs(self, energy_keV):
if self._th2_offsets is None:
return 0.0
max_offs = np.max(self._th2_offsets[:, 1])
if max_offs > 0.2:
raise ValueError(
f"\nThe empirical moth2 corrections are as high as {max_offs} deg\nThis is unreasonable and the corrections will not be used.\n\n***PLEASE INFORM BEAMLINE SCIENTISTS***\n"
)
offs = np.interp(energy_keV, self._th2_offsets[:, 0], self._th2_offsets[:, 1])
# print(offs)
return offs
def calc(self, val):
_mono_sintheta2_to_Ekev = -self._mono_hce / self._mono_2d2
asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2
theta2_deg = (
self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2
)
E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592)
if self._mono_add_offs:
theta2_deg -= self._mono_get_th2_offs(E_keV)
E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592)
return E_keV
class CurrentSum(Signal):
"""Adds up four current signals from the parent"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent.ch1.subscribe(self._emit_value)
def _emit_value(self, **kwargs):
timestamp = kwargs.pop("timestamp", time.time())
self.wait_for_connection()
self._run_subs(sub_type="value", timestamp=timestamp, obj=self)
def get(self, *args, **kwargs):
# self.parent._cnt.set(1).wait()
self._metadata["timestamp"] = time.time()
total = (
self.parent.ch1.get()
+ self.parent.ch2.get()
+ self.parent.ch3.get()
+ self.parent.ch4.get()
)
return total
class Bpm4i(Device):
SUB_VALUE = "value"
_default_sub = SUB_VALUE
_cont = Component(EpicsSignal, "CONT", put_complete=True, kind=Kind.omitted)
_cnt = Component(EpicsSignal, "CNT", put_complete=True, kind=Kind.omitted)
ch1 = Component(EpicsSignalRO, "S2", auto_monitor=True, kind=Kind.omitted, name="ch1")
ch2 = Component(EpicsSignalRO, "S3", auto_monitor=True, kind=Kind.omitted, name="ch2")
ch3 = Component(EpicsSignalRO, "S4", auto_monitor=True, kind=Kind.omitted, name="ch3")
ch4 = Component(EpicsSignalRO, "S5", auto_monitor=True, kind=Kind.omitted, name="ch4")
sum = Component(CurrentSum, kind=Kind.hinted, name="sum")
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
**kwargs,
):
super().__init__(
prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
self.sum.name = self.name
# Ensure the scaler counts automatically
self._cont.wait_for_connection()
self._cont.set(1).wait()
self.ch1.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)
if __name__ == "__main__":
dut = Bpm4i("X12SA-OP1-SCALER.", name="bpm4")
dut.wait_for_connection()
print(dut.read())
print(dut.describe())

View File

@@ -1,4 +0,0 @@
from .fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
from .fupr_ophyd import FuprGalilController, FuprGalilMotor
from .galil_ophyd import GalilController, GalilMotor
from .sgalil_ophyd import SGalilMotor

View File

@@ -1,201 +0,0 @@
<mxfile host="app.diagrams.net" modified="2023-07-31T14:55:08.470Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0" etag="hKC99drhJ4x6bY93jFGH" version="21.6.6" type="device">
<diagram name="Page-1" id="b520641d-4fe3-3701-9064-5fc419738815">
<mxGraphModel dx="2049" dy="1206" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="21ea969265ad0168-6" value="SGalil stages" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
<mxGeometry x="12.5" y="24" width="160" height="110" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-7" value="grid fly scan (2D), TTL signal at the beginning of each line" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-6" vertex="1">
<mxGeometry y="26" width="160" height="54" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-14" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
<mxGeometry x="270" y="40" width="160" height="160" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-15" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
<mxGeometry y="26" width="160" height="26" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-16" value="operated in normal mode, controls fast shutter and triggers second DDG" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
<mxGeometry y="52" width="160" height="48" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-19" value="&lt;div&gt;channel AB&lt;/div&gt;" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
<mxGeometry y="100" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-21" value="&lt;div&gt;channel T0&lt;br&gt;&lt;/div&gt;" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
<mxGeometry y="130" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-22" value="Logical Card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
<mxGeometry x="710" y="250" width="160" height="104" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-24" value="&lt;div&gt;both signals low -&amp;gt; low&lt;br&gt;either on signal high -&amp;gt; high&lt;br&gt;both signals high -&amp;gt; low&lt;br&gt;&lt;br&gt;&lt;/div&gt;" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-22" vertex="1">
<mxGeometry y="26" width="160" height="54" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-26" value="MCS readout card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
<mxGeometry x="920" y="360" width="160" height="110" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-27" value="falling edge trigger&lt;br&gt;readout is triggered between to falling edges, thus logical board required" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-26" vertex="1">
<mxGeometry y="26" width="160" height="74" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-36" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21ea969265ad0168-6" target="21ea969265ad0168-15" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="21ea969265ad0168-38" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" target="O8qpyw_Cq4v1m74naMs6-3" edge="1">
<mxGeometry x="-0.0026" relative="1" as="geometry">
<mxPoint x="430" y="150" as="sourcePoint" />
<mxPoint x="680" y="60" as="targetPoint" />
<Array as="points">
<mxPoint x="660" y="155" />
<mxPoint x="660" y="43" />
</Array>
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="21ea969265ad0168-42" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="660" y="155" />
<mxPoint x="660" y="290" />
</Array>
<mxPoint x="710" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="21ea969265ad0168-43" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.378;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="21ea969265ad0168-24" edge="1" target="21ea969265ad0168-27">
<mxGeometry relative="1" as="geometry">
<mxPoint x="880" y="331" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-2" value="Fast shutter: fsh" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="750" width="160" height="104" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-3" value="FSH opening time, e.g. 20ms" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-2">
<mxGeometry y="26" width="160" height="34" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-11" value="TTL" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
<mxGeometry x="200" y="60" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-16" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="270" y="350" width="230" height="190" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-17" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
<mxGeometry y="26" width="230" height="24" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-18" value="operated in burst mode:&lt;br&gt;burstCount: N_points&lt;br&gt;burstPeriod: (exp_time + readout time)&lt;br&gt;burstDelay: fsh opening (20ms)" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
<mxGeometry y="50" width="230" height="68" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-29" value="Channel AB" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
<mxGeometry y="118" width="230" height="24" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-28" value="Channel CD " style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
<mxGeometry y="142" width="230" height="24" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-30" value="Channel EF" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
<mxGeometry y="166" width="230" height="24" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-27" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-21" target="O8qpyw_Cq4v1m74naMs6-17">
<mxGeometry x="-0.0026" relative="1" as="geometry">
<mxPoint x="220" y="262" as="sourcePoint" />
<mxPoint x="240" y="390" as="targetPoint" />
<Array as="points">
<mxPoint x="510" y="185" />
<mxPoint x="510" y="270" />
<mxPoint x="200" y="270" />
<mxPoint x="200" y="390" />
</Array>
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-36">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="640" y="504" />
<mxPoint x="640" y="610" />
</Array>
<mxPoint x="500" y="480" as="sourcePoint" />
<mxPoint x="910" y="610" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-32" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-34">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="640" y="504" />
<mxPoint x="640" y="610" />
<mxPoint x="760" y="610" />
<mxPoint x="760" y="528" />
</Array>
<mxPoint x="500" y="500" as="sourcePoint" />
<mxPoint x="880" y="540" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-33" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0.008;entryY=0.154;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-38">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="640" y="504" />
<mxPoint x="640" y="610" />
<mxPoint x="760" y="610" />
<mxPoint x="760" y="701" />
</Array>
<mxPoint x="500" y="480" as="sourcePoint" />
<mxPoint x="880" y="700" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-34" value="eiger" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="920" y="510" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-35" value="rising edge&lt;br&gt;set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-34">
<mxGeometry y="26" width="160" height="44" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-36" value="falcon" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="920" y="600" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-37" value="rising edge &lt;br&gt;set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-36">
<mxGeometry y="26" width="160" height="44" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-38" value="Pilatus 300k" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="920" y="690" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-39" value="rising edge &lt;br&gt;set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-38">
<mxGeometry y="26" width="160" height="44" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-40" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=-0.005;entryY=1.002;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-29" target="21ea969265ad0168-24">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="560" y="480" />
<mxPoint x="560" y="330" />
</Array>
<mxPoint x="510" y="520" as="sourcePoint" />
<mxPoint x="700" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-41" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitX=1;exitY=0.167;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-30">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="530" y="520" />
<mxPoint x="530" y="780" />
</Array>
<mxPoint x="500" y="530" as="sourcePoint" />
<mxPoint x="610" y="780" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-42" value="SGalil positions encoder" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
<mxGeometry x="610" y="740" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-43" value="triggered on falling edge" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-42">
<mxGeometry y="26" width="160" height="44" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-44" value="&lt;div align=&quot;justify&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;channel AB&lt;/span&gt;&lt;/div&gt;&lt;div align=&quot;justify&quot;&gt;&lt;div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;delay:0&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;width: fsh opening (20ms)&lt;br&gt;+ N_points * exp_time&lt;br&gt;+ (N_points-1) * readout time&lt;br&gt;&lt;/span&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
<mxGeometry x="450" y="44" width="180" height="90" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-45" value="&lt;div align=&quot;justify&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;channel AB&lt;/span&gt;&lt;/div&gt;&lt;div align=&quot;justify&quot;&gt;&lt;div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;delay:0&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;width: exp_time &lt;br&gt;or less&lt;br&gt;&lt;/span&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
<mxGeometry x="530" y="300" width="110" height="70" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-46" value="&lt;div align=&quot;justify&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;channel CD&lt;br&gt;split into 3 signals&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div align=&quot;justify&quot;&gt;&lt;div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;delay:0&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;width: exp_time &lt;br&gt;or less&lt;br&gt;&lt;/span&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
<mxGeometry x="575" y="450" width="120" height="90" as="geometry" />
</mxCell>
<mxCell id="O8qpyw_Cq4v1m74naMs6-47" value="&lt;div align=&quot;justify&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;channel EF&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div align=&quot;justify&quot;&gt;&lt;div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;delay:0&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;width: exp_time/2 &lt;br&gt;&lt;/span&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
<mxGeometry x="480" y="610" width="120" height="60" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -1,370 +0,0 @@
import threading
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError
from ophyd_devices.utils.controller import threadlocked
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
from csaxs_bec.devices.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilController,
GalilError,
GalilMotorIsMoving,
GalilMotorResolution,
GalilSetpointSignal,
GalilSignalRO,
retry_once,
)
logger = bec_logger.logger
class FlomniGalilController(GalilController):
USER_ACCESS = [
"describe",
"show_running_threads",
"galil_show_all",
"socket_put_and_receive",
"socket_put_confirmed",
"drive_axis_to_limit",
"find_reference",
"get_motor_limit_switch",
"fosaz_light_curtain_is_triggered",
"is_motor_on",
"all_axes_referenced",
"lights_off",
"lights_on",
]
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
if axis_Id is None and axis_Id_numeric is not None:
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
active_thread = self.is_thread_active(0)
motor_is_on = self.is_motor_on(axis_Id)
return bool(active_thread or motor_is_on)
def all_axes_referenced(self) -> bool:
# TODO: check if all axes are referenced in all controllers
return super().all_axes_referenced()
def fosaz_light_curtain_is_triggered(self) -> bool:
"""
Check the light curtain status for fosaz
Returns:
bool: True if the light curtain is triggered
"""
return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1
def lights_off(self) -> None:
"""
Turn off the lights
"""
self.socket_put_confirmed("CB15")
def lights_on(self) -> None:
"""
Turn on the lights
"""
self.socket_put_confirmed("SB15")
class FlomniGalilReadbackSignal(GalilSignalRO):
@retry_once
@threadlocked
def _socket_get(self) -> float:
"""Get command for the readback signal
Returns:
float: Readback value after adjusting for sign and motor resolution.
"""
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
current_pos *= self.parent.sign
step_mm = self.parent.motor_resolution.get()
return current_pos / step_mm
def read(self):
self._metadata["timestamp"] = time.time()
val = super().read()
return val
class FlomniGalilSetpointSignal(GalilSetpointSignal):
@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):
pass
class FlomniGalilMotorIsMoving(GalilMotorIsMoving):
pass
class FlomniGalilAxesReferenced(GalilAxesReferenced):
pass
class FlomniGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
all_axes_referenced = Cpt(
FlomniGalilAxesReferenced, signal_name="all_axes_referenced", kind="config"
)
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=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = FlomniGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.sign = sign
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_mapping = kwargs.pop("device_mapping", {})
self.device_manager = device_manager
if len(self.device_mapping) > 0 and self.device_manager is None:
raise BECConfigError(
"device_mapping has been specified but the device_manager cannot be accessed."
)
self.rt = self.device_mapping.get("rt")
self.pid_x_correction = 0
super().__init__(
prefix,
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():
logger.info("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.1)
val = self.readback.read()
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
if not success:
print(" stop")
self._done_moving(success=success)
logger.info("Move finished")
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("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("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)}")
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "mm"
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__":
# mock = False
# if not mock:
# leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
# leyey.stage()
# status = leyey.move(0, wait=True)
# status = leyey.move(10, wait=True)
# leyey.read()
# leyey.get()
# leyey.describe()
# leyey.unstage()
# else:
# from ophyd_devices.utils.socket import SocketMock
# leyex = GalilMotor(
# "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
# )
# leyey = GalilMotor(
# "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
# )
# leyex.stage()
# # leyey.stage()
# leyex.controller.galil_show_all()

View File

@@ -1,318 +0,0 @@
import functools
import threading
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError, ReadOnlyError
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
from prettytable import PrettyTable
from csaxs_bec.devices.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilCommunicationError,
GalilController,
GalilError,
GalilMotorIsMoving,
GalilMotorResolution,
GalilReadbackSignal,
GalilSetpointSignal,
retry_once,
)
logger = bec_logger.logger
class FuprGalilController(GalilController):
_axes_per_controller = 1
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
if axis_Id is None and axis_Id_numeric is not None:
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
return is_moving
def axis_is_referenced(self, axis_Id) -> bool:
return self.all_axes_referenced()
def all_axes_referenced(self) -> bool:
return bool(float(self.socket_put_and_receive("MG axisref").strip()))
def drive_axis_to_limit(self, axis_Id_numeric, direction: str) -> None:
raise NotImplementedError("This function is not implemented for the FuprGalilController.")
class FuprGalilReadbackSignal(GalilReadbackSignal):
@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"TP{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()
if self.parent.axis_Id_numeric == 0:
try:
rt = self.parent.device_manager.devices[self.parent.rt]
if rt.enabled:
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"])
except KeyError:
logger.warning("Failed to set RT value during readback.")
return val
class FuprGalilSetpointSignal(GalilSetpointSignal):
@retry_once
@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:
self.controller.socket_put_confirmed(
f"PA{self.parent.axis_Id}={int(self.setpoint*self.parent.MOTOR_RESOLUTION)}"
)
self.controller.socket_put_confirmed(f"BG{self.parent.axis_Id}")
else:
raise GalilError("Not all axes are referenced.")
class FuprGalilMotorResolution(GalilMotorResolution):
@retry_once
@threadlocked
def _socket_get(self):
return self.parent.MOTOR_RESOLUTION
class FuprGalilMotorIsMoving(GalilMotorIsMoving):
pass
class FuprGalilAxesReferenced(GalilAxesReferenced):
pass
class FuprGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
MOTOR_RESOLUTION = 25600
readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint")
motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config")
motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
all_axes_referenced = Cpt(
FuprGalilAxesReferenced, signal_name="all_axes_referenced", kind="config"
)
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=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = FuprGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.sign = sign
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_mapping = kwargs.pop("device_mapping", {})
self.device_manager = device_manager
if len(self.device_mapping) > 0 and self.device_manager is None:
raise BECConfigError(
"device_mapping has been specified but the device_manager cannot be accessed."
)
self.rt = self.device_mapping.get("rt", "rtx")
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():
logger.info("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.1)
val = self.readback.read()
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
if not success:
print(" stop")
self._done_moving(success=success)
logger.info("Move finished")
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)}")
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "mm"
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)

View File

@@ -1,604 +0,0 @@
import functools
import threading
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError, ReadOnlyError
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
from prettytable import PrettyTable
logger = bec_logger.logger
class GalilCommunicationError(Exception):
pass
class GalilError(Exception):
pass
class BECConfigError(Exception):
pass
def retry_once(fcn):
"""Decorator to rerun a function in case a Galil communication error 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 (GalilCommunicationError, GalilError):
val = fcn(self, *args, **kwargs)
return val
return wrapper
class GalilController(Controller):
_axes_per_controller = 8
USER_ACCESS = [
"describe",
"show_running_threads",
"galil_show_all",
"socket_put_and_receive",
"socket_put_confirmed",
"lgalil_is_air_off_and_orchestra_enabled",
"drive_axis_to_limit",
"find_reference",
"get_motor_limit_switch",
"is_motor_on",
"all_axes_referenced",
]
@threadlocked
def socket_put(self, val: str) -> None:
self.sock.put(f"{val}\r".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()
@retry_once
def socket_put_confirmed(self, val: str) -> None:
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
Args:
val (str): Message that should be sent to the socket
Raises:
GalilCommunicationError: Raised if the return value is not a colon.
"""
return_val = self.socket_put_and_receive(val)
if return_val != ":":
raise GalilCommunicationError(
f"Expected return value of ':' but instead received {return_val}"
)
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
if axis_Id is None and axis_Id_numeric is not None:
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[{axis_Id_numeric}]")) != 0)
return bool(
is_moving or backlash_is_active or self.is_thread_active(0) or self.is_thread_active(2)
)
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
def stop_all_axes(self) -> str:
return self.socket_put_and_receive(f"XQ#STOP,1")
def lgalil_is_air_off_and_orchestra_enabled(self) -> bool:
# TODO: move this to the LamNI-specific controller
rt_not_blocked_by_galil = bool(self.socket_put_and_receive(f"MG@OUT[9]"))
air_off = bool(self.socket_put_and_receive(f"MG@OUT[13]"))
return rt_not_blocked_by_galil and air_off
def axis_is_referenced(self, axis_Id_numeric) -> bool:
return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip()))
def all_axes_referenced(self) -> bool:
"""
Check if all axes are referenced.
"""
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str) -> None:
"""
Drive an axis to the limit in a specified direction.
Args:
axis_Id_numeric (int): Axis number
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
if direction == "forward":
direction_flag = 1
elif direction == "reverse":
direction_flag = -1
else:
raise ValueError(f"Invalid direction {direction}")
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
self.socket_put_confirmed(f"ndir={direction_flag}")
self.socket_put_confirmed("XQ#NEWPAR")
time.sleep(0.005)
self.socket_put_confirmed("XQ#FES")
time.sleep(0.01)
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.01)
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
# check if we actually hit the limit
if direction == "forward":
limit = self.get_motor_limit_switch(axis_Id)[1]
elif direction == "reverse":
limit = self.get_motor_limit_switch(axis_Id)[0]
if not limit:
raise GalilError(f"Failed to drive axis {axis_Id}/{axis_Id_numeric} to limit.")
def find_reference(self, axis_Id_numeric: int) -> None:
"""
Find the reference of an axis.
Args:
axis_Id_numeric (int): Axis number
"""
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
self.socket_put_and_receive("XQ#NEWPAR")
self.socket_put_confirmed("XQ#FRM")
time.sleep(0.1)
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.1)
if not self.axis_is_referenced(axis_Id_numeric):
raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.")
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
def show_running_threads(self) -> None:
t = PrettyTable()
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
t.field_names = [str(ax) for ax in range(self._axes_per_controller)]
t.add_row(
[
"active" if self.is_thread_active(t) else "inactive"
for t in range(self._axes_per_controller)
]
)
print(t)
def is_motor_on(self, axis_Id) -> bool:
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
def get_motor_limit_switch(self, axis_Id) -> list:
"""
Get the status of the motor limit switches.
Args:
axis_Id (str): Axis identifier (e.g. 'A', 'B', 'C', ...)
Returns:
list: List of two booleans indicating if the low and high limit switch is active, respectively.
"""
ret = self.socket_put_and_receive(f"MG _LR{axis_Id}, _LF{axis_Id}")
low, high = ret.strip().split(" ")
return [not bool(float(low)), not bool(float(high))]
def describe(self) -> None:
t = PrettyTable()
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
t.field_names = [
"Axis",
"Name",
"Connected",
"Referenced",
"Motor On",
"Limits",
"Position",
]
for ax in range(self._axes_per_controller):
axis = self._axis[ax]
if axis is not None:
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
axis.name,
axis.connected,
self.axis_is_referenced(axis.axis_Id_numeric),
self.is_motor_on(axis.axis_Id),
self.get_motor_limit_switch(axis.axis_Id),
axis.readback.read().get(axis.name).get("value"),
]
)
else:
t.add_row([None for t in t.field_names])
print(t)
self.show_running_threads()
def galil_show_all(self) -> None:
for controller in self._controller_instances.values():
if isinstance(controller, GalilController):
controller.describe()
@staticmethod
def axis_Id_to_numeric(axis_Id: str) -> int:
return ord(axis_Id.lower()) - 97
@staticmethod
def axis_Id_numeric_to_alpha(axis_Id_numeric: int) -> str:
return (chr(axis_Id_numeric + 97)).capitalize()
class GalilSignalBase(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 GalilSignalRO(GalilSignalBase):
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 GalilReadbackSignal(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()
if self.parent.axis_Id_numeric == 2:
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 GalilSetpointSignal(GalilSignalBase):
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 * self.parent.sign
@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)
if self.parent.axis_Id_numeric == 2:
angle_status = self.parent.device_manager.devices[
self.parent.rt
].obj.controller.feedback_status_angle_lamni()
if angle_status:
self.controller.socket_put_confirmed("angintf=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 GalilMotorResolution(GalilSignalRO):
@retry_once
@threadlocked
def _socket_get(self):
return float(
self.controller.socket_put_and_receive(f"MG stppermm[{self.parent.axis_Id_numeric}]")
)
class GalilMotorIsMoving(GalilSignalRO):
@threadlocked
def _socket_get(self):
return self.controller.is_axis_moving(self.parent.axis_Id, 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 GalilAxesReferenced(GalilSignalRO):
@threadlocked
def _socket_get(self):
return self.controller.all_axes_referenced()
class GalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
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=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = GalilController(socket_cls=socket_cls, socket_host=host, socket_port=port)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.sign = sign
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_mapping = kwargs.pop("device_mapping", {})
self.device_manager = device_manager
if len(self.device_mapping) > 0 and self.device_manager is None:
raise BECConfigError(
"device_mapping has been specified but the device_manager cannot be accessed."
)
self.rt = self.device_mapping.get("rt")
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():
logger.info("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.1)
val = self.readback.read()
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
if not success:
print(" stop")
self._done_moving(success=success)
logger.info("Move finished")
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 = self.controller.axis_Id_to_numeric(val)
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 = self.controller.axis_Id_numeric_to_alpha(val)
self._axis_Id_numeric = val
else:
raise TypeError(f"Expected value of type int but received {type(val)}")
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "mm"
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__":
# pytest: skip-file
mock = False
if not mock:
leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
leyey.stage()
status = leyey.move(0, wait=True)
status = leyey.move(10, wait=True)
leyey.read()
leyey.get()
leyey.describe()
leyey.unstage()
else:
from ophyd_devices.utils.socket import SocketMock
leyex = GalilMotor(
"G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
)
leyey = GalilMotor(
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
)
leyex.stage()
# leyey.stage()
leyex.controller.galil_show_all()

View File

@@ -1,415 +0,0 @@
'******************************************************************************
' scanning saxs stages controller code
' version 0.1 20160113, holler, based on example code
' version 0.2 20160321, holler, small adjustments
' version 0.3 20160323, position sampling
' version 0.4 grid scan implemented
' version 0.5 20160426, shutter control from controller
' version 0.6 20160614, code for manual stage tuning added
' version 0.7 20170609, prefact added
' version 0.8 20190327, stepper motor x axis E, encoder axis A
' DC motor y axis C
' version 0.9 20190403, Pos sampling averaging and ring buffered
' internal grid scan updated
' 20190507 various fixes during comm. at beamline
' off now in microns for higher resolution
' version 1.1 20191021, position samples were off compared to xrays
' use AL and RL commands for position latch
' to reduce delay for axis C (continuous)
' switch to DI3 required, averaging removed
' version 1.2 20200500, switch stepper motor to axis F
' because motor driver axis E defective
' version 2.0 20230816, adjustments in premove with BEC
' DO8 controls the shutter
' DI1 1 during exposure for pos sampling
' Thread overview
'******************************************************************************
#AUTO
DA*,*[];'DEALLOCATE ARRAYS
ssaxs_v=1.3
IA129,129,122,26
'acctim determines pre motion
acctim=2.5
prvspeed=0
posest=0
'prefact increases the distance for pre acceleration
'if in acctim limits
prefact=2.5
off=0
DM aposavg[2000]
DM cposavg[2000]
nums=1
JS#INIT
JS#SETPLAT
EN
#CALIBC
'ACC=1000000
'DCC=1000000
'SPC=1*mm
PAC=2*mm
BGC;AMC
WT 1000
PAC=5*mm
BGC;AMC
WT 1000
JP#CALIBC
EN
'default settings
#SETTOMO
IF(allaxref=1)
EN
ENDIF
KPC=100
PLC=0.3
KIC=10
KDC=30
ILC=9
FAC=10
FVC=240
EN
'set tuning parameters for scanning saxs plate
#SETPLAT
IF(allaxref=1)
EN
ENDIF
KPC=100
PLC=0.3
KIC=10
KDC=30
ILC=9
FAC=10
FVC=240
EN
'called with AB before execution
#SAMPLE
posct=0
sposct=0
IF(_XQ2=-1)
'arm position latch rising axis C
'set latch direction rising
'we do this prior the loop, to do the switching later asap
CN ,,1
ALC
XQ#SAMPLEL,2
ELSE
runerr=1
ENDIF
EN
#SAMPLEL
'wait for latch
AI3
'WT1
''#WAITLT1
''JP#WAITLT1,(_ALC=1);'WAIT UNTIL CAPTURED'
'write encoder position to a array
aposstrt=_TPA
'write latched position to c array
cposstrt=_RLC
'change latch direction falling axis C
CN ,,-1
ALC
'wait for latch
AI-3
'WT1
''#WAITLT2
''JP#WAITLT2,(_ALC=1);'WAIT UNTIL CAPTURED
'write encoder position to a array
aposend=_TPA
'write latched position to c array
cposend=_RLC
'arm position latch rising axis C for next cycle
'set latch direction rising
CN ,,1
ALC
aposavg[posct]=((aposstrt+aposend)/2)
cposavg[posct]=((cposstrt+cposend)/2)
posct=posct+1
sposct=sposct+1
IF(posct>1999)
posct=0
ENDIF
JP #SAMPLEL,(posct<nums)
EN
#SCANG
'a_start, a_end, speed is line axis
'b_start, gridmax, b_step is grid axis
gridct=0
'nums=1000
'wait for detector
WT350
IF(_XQ3=(-1))
XQ#SCANGL,3
ENDIF
EN
#SCANGL
gridpos=b_start+(gridct*b_step)
targE=gridpos
JS#POSE
WT10
IF(@INT[(gridct/2)]<>(gridct/2))
start=a_start
end=a_end
ELSE
start=a_end
end=a_start
ENDIF
'XQ#SCANL,5
'scanstat=-1
'#lineact
'WT2
'JP#lineact,(scanstat<>0)
JS#SCANL
gridct=gridct+1
JP #SCANGL,(gridct<=gridmax)
'close shutter
CB8
EN
#TEMP1
EN
'
#SCANL
'based on acceleration of 500 mm/s^2
'and a max speed of 2 mm/s
'the max distance needed for acceleration is 4 microns
'so we pre-move 10 microns
'variables to set in mm, mm/s
'start = start position
'end = end position
'speed = velocity
'the scan axis is defined in the init section
IF(allaxref=0)
EN
ENDIF
'
IF(end>start)
dir=1
ELSE
dir=-1
ENDIF
'measure required premove
IF((@ABS[(prvspeed-speed)])>0.001)
premv=speed*mm*5
IF(premv>(3*mm))
premv=3*mm
ENDIF
measpre=1
ELSE
measpre=0
ENDIF
'for internal grid scans reduce overshoot
'IF(_XQ3<>-1)
'redpremv=0.1 ;'case for int grid scan
'ELSE
'redpremv=1 ;'case for line based scans
'ENDIF
'we are doing an internal grid
'reduce overshoot
prepos=(start*mm)-(dir*redpremv*(premv+(off/1000*mm)))
'move to pre-start position if needed
'prepos=(start*mm)-(dir*speed*acctim*mm)
'IF(@ABS[(speed*acctim)]<0.01)
'prepos=(start*mm)-(dir*0.01*mm*prefact)
'ENDIF
'IF(@ABS[(speed*acctim)]>0.1)
'ENDIF
IF((@ABS[(_TDC-prepos)])>(0.002*mm))
scanstat=1
SPC=2*mm
PAC=prepos
BGC
AMC
'open the shutter
SB8
WT10
ENDIF
IF((_LFC<>0)&(_LRC=<>0))
scanstat=2
SPC=@RND[speed*mm]
'arm trigger
trigpos=(start*mm)+(dir*off/1000*mm)
IF(dir=1)
OCC=trigpos,0
ENDIF
IF(dir=-1)
OCC=trigpos,-65536;
ENDIF
'PAC=((end*mm)+(dir*premv))
PAC=((end*mm)+(dir*redpremv*(premv+(off/1000*mm))))
BGC
IF(measpre=1)
calstart=_TDC
WT25
#CALIBV
prevvel=_TVC
WT10
JP#CALIBV,((@ABS[(prevvel-(_TVC/mm))])<15)
calend=_TDC
premv=((@ABS[(calend-calstart)])*3)
'case of grid scan
prvspeed=speed
ENDIF
'
AMC
ENDIF
scanstat=0
'close the shutter
'#SHUTWT
'JP#SHUTWT,(@IN[1]=1)
'WT10
'JP#SHUTWT,(@IN[1]=1)
'
'we are doing an internal grid
IF(_XQ3=-1)
CB8
ENDIF
EN
'
#POSE
posctr=0
posest=1
sttime=TIME
PTF=1;' Position Tracking aktiv
errE=(targE-(_TPA/mm));' Fehler in mm
IF((@ABS[errE])>0.2)
SPF=12*stpmm
ENDIF
#CORRE
posest=2
errE=(targE-(_TPA/mm));' Fehler in mm
PAF=_TDF+(errE*stpmm)
IF((@ABS[errE])<0.1)
SPF=5*stpmm
ENDIF
IF((@ABS[errE])<0.0001)
posctr=posctr+1
IF(posctr>5)
STF
'MG TIME-sttime, (targE-(_TPA/mm))*1000
EN
ENDIF
ELSE
posctr=0
ENDIF
WT5
JP#CORRE
posest=3
MCF
EN
'
#ZZ
targE=120;XQ#POSE
WT12000
targE=20;XQ#POSE
WT12000
JP#ZZ
EN
#FINDREF
SB1;' Bremse C-Achse loesen (nur in Verbindung mit SHC)
SHC
SHF
JS#LIMSWI
JS#REFE
JS#REFC
allaxref=1
targE=0
EN
'
#REFE
SHF
JGF=-2*stpmm
BGF
'MG "suche negativen Endschalter E"
AMF
WT100
'step counter zero
DPF=0
'encoder zero
DPA=0
EN
'
#REFC
SB1;' Bremse loesen (nur in Verbindung mit SHC)
SHC
JGC=-2*mm
BGC
'MG "suche negativen Endschalter C"
AMC
WT100
DPC=0
EN
'
#LIMSWI
scanstat=0
JS#LFF,_LFF=0;' +LIMIT E
JS#LRF,_LRF=0;' -LIMIT E
JS#LFC,_LFC=0;' +LIMIT C
JS#LRC,_LRC=0;' -LIMIT C
RE;' RETURN FROM ERROR INTERUPT
'
#LRF
'MG "- LIMIT E-ACHSE "
STF
AMF
JGF=stpmm
BGF
#LOOPA1
JP#LOOPA1,_LRF=0
STF
MCF
EN
'
#LFF
'MG "+ LIMIT E-ACHSE "
STF
AMF
JGF=-stpmm
BGF
#LOOPA2
JP#LOOPA2,_LFF=0
STF
MCF
EN
'
#LRC
'MG "- LIMIT C-ACHSE "
STC
AMC
JGC=mm
BGC
#LOOPC1
JP#LOOPC1,_LRC=0
STC
AMC
EN
'
#LFC
'MG "- LIMIT C-ACHSE "
STC
AMC
JGC=-mm
BGC
#LOOPC2
JP#LOOPC2,_LFC=0
STC
AMC
EN
'
#INIT
'define fast scan axis redefined by spec
scanstat=0
mm=10000;' 100nm Aufloesung im encoder
stpmm=50000;' microsteps axis E/mm
ratio=5;'
allaxref=0
'acceleration rates
ACF=3000000
DCF=3000000
SPF=2*mm
OEF=0;' OF ON ERROR axis E ausgeschaltet
MTF=2
KSF=0.5;' Smoothing ausgeschaltet
ACC=10*mm
DCC=10*mm
DVC=1;' Dual loop deaktiviert
EN
'

View File

@@ -1,713 +0,0 @@
import functools
import threading
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, 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
from prettytable import PrettyTable
logger = bec_logger.logger
class GalilCommunicationError(Exception):
pass
class GalilError(Exception):
pass
class BECConfigError(Exception):
pass
def retry_once(fcn):
"""Decorator to rerun a function in case a Galil communication error 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 (GalilCommunicationError, GalilError):
val = fcn(self, *args, **kwargs)
return val
return wrapper
class GalilController(Controller):
USER_ACCESS = [
"describe",
"show_running_threads",
"galil_show_all",
"socket_put_and_receive",
"socket_put_confirmed",
"sgalil_reference",
"fly_grid_scan",
"read_encoder_position",
]
def __init__(
self,
*,
name="GalilController",
kind=None,
parent=None,
socket=None,
attr_name="",
labels=None,
):
if not hasattr(self, "_initialized") or not self._initialized:
self._galil_axis_per_controller = 8
self._axis = [None for axis_num in range(self._galil_axis_per_controller)]
super().__init__(
name=name,
socket=socket,
attr_name=attr_name,
parent=parent,
labels=labels,
kind=kind,
)
def on(self, controller_num=0) -> None:
"""Open a new socket connection to the controller"""
if not self.connected:
self.sock.open()
self.connected = True
else:
logger.info("The connection has already been established.")
# warnings.warn(f"The connection has already been established.", stacklevel=2)
def off(self) -> None:
"""Close the socket connection to the controller"""
if self.connected:
self.sock.close()
self.connected = False
else:
logger.info("The connection is already closed.")
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:
time.sleep(0.01)
self.sock.put(f"{val}\r".encode())
@threadlocked
def socket_get(self) -> str:
time.sleep(0.01)
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()
@retry_once
def socket_put_confirmed(self, val: str) -> None:
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
Args:
val (str): Message that should be sent to the socket
Raises:
GalilCommunicationError: Raised if the return value is not a colon.
"""
return_val = self.socket_put_and_receive(val)
if return_val != ":":
raise GalilCommunicationError(
f"Expected return value of ':' but instead received {return_val}"
)
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
# backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0)
return bool(is_moving) # bool(is_moving or backlash_is_active)
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
def stop_all_axes(self) -> str:
# return self.socket_put_and_receive(f"XQ#STOP,1")
# Command stops all threads and motors!
self.socket_put_and_receive(f"CB8")
return self.socket_put_and_receive(f"ST")
def axis_is_referenced(self) -> bool:
return bool(float(self.socket_put_and_receive(f"MG allaxref").strip()))
def show_running_threads(self) -> None:
t = PrettyTable()
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
t.field_names = [str(ax) for ax in range(self._galil_axis_per_controller)]
t.add_row(
[
"active" if self.is_thread_active(t) else "inactive"
for t in range(self._galil_axis_per_controller)
]
)
print(t)
def is_motor_on(self, axis_Id) -> bool:
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
def get_motor_limit_switch(self, axis_Id) -> list:
# SGalil specific
if axis_Id == "C":
ret = self.socket_put_and_receive(f"MG _LF{axis_Id}, _LR{axis_Id}")
high, low = ret.strip().split(" ")
elif axis_Id == "E":
ret = self.socket_put_and_receive(f"MG _LF{'F'}, _LR{'F'}")
high, low = ret.strip().split(" ")
return [not bool(float(low)), not bool(float(high))]
def describe(self) -> None:
t = PrettyTable()
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
t.field_names = [
"Axis",
"Name",
"Connected",
"Referenced",
"Motor On",
"Limits",
"Position",
]
for ax in range(self._galil_axis_per_controller):
axis = self._axis[ax]
if axis is not None:
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
axis.name,
axis.connected,
self.axis_is_referenced(),
self.is_motor_on(axis.axis_Id),
self.get_motor_limit_switch(axis.axis_Id),
axis.readback.read().get(axis.name).get("value"),
]
)
else:
t.add_row([None for t in t.field_names])
print(t)
self.show_running_threads()
def galil_show_all(self) -> None:
for controller in self._controller_instances.values():
if isinstance(controller, GalilController):
controller.describe()
def sgalil_reference(self) -> None:
"""Reference all axes of the controller"""
if self.axis_is_referenced():
print("All axes are already referenced.\n")
return
# Make sure no axes are moving, is this necessary?
self.stop_all_axes()
self.socket_put_and_receive(f"XQ#FINDREF")
print("Referencing. Please wait, timeout after 100s...\n")
timeout = time.time() + 100
while not self.axis_is_referenced():
if time.time() > timeout:
print("Abort reference sequence, timeout reached\n")
break
time.sleep(0.5)
# @threadlocked
def fly_grid_scan(
self,
status: DeviceStatus,
start_y: float,
end_y: float,
interval_y: int,
start_x: float,
end_x: float,
interval_x: int,
exp_time: float,
readout_time: float,
**kwargs,
) -> tuple:
"""_summary_
Args:
start_y (float): start position of y axis (fast axis)
end_y (float): end position of y axis (fast axis)
interval_y (int): number of points in y axis
start_x (float): start position of x axis (slow axis)
end_x (float): end position of x axis (slow axis)
interval_x (int): number of points in x axis
exp_time (float): exposure time in seconds
readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms)
Raises:
LimitError: Raised if any position of motion is outside of the limits
LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s
"""
#
if not self.axis_is_referenced():
raise GalilError("Axis are not referenced")
sign_y = self._axis[ord("c") - 97].sign
sign_x = self._axis[ord("e") - 97].sign
# Check limits
# TODO check sign of stage, or not necessary
check_values = [start_y, end_y, start_x, end_x]
for val in check_values:
self.check_value(val)
start_x *= sign_x
end_x *= sign_x
start_y *= sign_y
end_y *= sign_y
speed = np.abs(end_y - start_y) / (
(interval_y) * exp_time + (interval_y - 1) * readout_time
)
if speed > 2.00 or speed < 0.02:
raise LimitError(
f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s"
)
gridmax = int(interval_x - 1)
step_grid = (end_x - start_x) / interval_x
n_samples = int(interval_y * interval_x)
# Hard coded to maximum offset of 0.1mm to avoid long motions.
self.socket_put_and_receive(f"off={(0):f}")
self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}")
self.socket_put_and_receive(
f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}"
)
self.socket_put_and_receive(f"nums={n_samples}")
self.socket_put_and_receive("XQ#SAMPLE")
# sleep 50ms to avoid controller running into
time.sleep(0.1)
self.socket_put_and_receive("XQ#SCANG")
# self._block_while_active(3)
# time.sleep(0.1)
threading.Thread(target=self._block_while_active, args=(3, status), daemon=True).start()
# self._while_in_motion(3, n_samples)
def _block_while_active(self, thread_id: int, status) -> None:
while self.is_thread_active(thread_id):
time.sleep(1)
time.sleep(1)
while self.is_thread_active(thread_id):
time.sleep(1)
status.set_finished()
# TODO this is for reading out positions, readout is limited by stage triggering
def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple:
last_readout = 0
val_axis2 = [] # y axis
val_axis4 = [] # x axis
while self.is_thread_active(thread_id):
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}")
time.sleep(1)
if posct > last_readout:
positions = self.read_encoder_position(last_readout, posct)
val_axis4.extend(positions[0])
val_axis2.extend(positions[1])
last_readout = posct + 1
logger.info(len(val_axis2))
time.sleep(1)
# Readout of last positions after scan finished
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}")
if posct > last_readout:
positions = self.read_encoder_position(last_readout, posct)
val_axis4.extend(positions[0])
val_axis2.extend(positions[1])
return val_axis4, val_axis2
def read_encoder_position(self, fromval: int, toval: int) -> tuple:
val_axis2 = [] # y axis
val_axis4 = [] # x axis
for ii in range(fromval, toval + 1):
rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10")
if rts == ":":
val_axis4.append(rts)
val_axis2.append(rts)
continue
val_axis4.append(float(rts.strip().split(" ")[0]) / 100000)
val_axis2.append(float(rts.strip().split(" ")[1]) / 100000)
return val_axis4, val_axis2
class GalilSignalBase(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 GalilSignalRO(GalilSignalBase):
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 GalilReadbackSignal(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.
"""
if self.parent.axis_Id_numeric == 2:
current_pos = float(
self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm")
)
elif self.parent.axis_Id_numeric == 4:
# hardware controller readback from axis 4 is on axis 0, A instead of E
current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm"))
current_pos *= self.parent.sign
return current_pos
def read(self):
self._metadata["timestamp"] = time.time()
val = super().read()
return val
class GalilSetpointSignal(GalilSignalBase):
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 * self.parent.sign
@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.axis_is_referenced()
if not axes_referenced:
raise GalilError(
"Not all axes are referenced. Please use controller.sgalil_reference(). BE AWARE that axes start moving, potentially beyond limits, make sure full range of motion is safe"
)
while self.controller.is_thread_active(0):
time.sleep(0.1)
if self.parent.axis_Id_numeric == 2:
self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm")
self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}")
elif self.parent.axis_Id_numeric == 4:
self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}")
self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}")
while self.controller.is_thread_active(0):
time.sleep(0.005)
class GalilMotorIsMoving(GalilSignalRO):
@threadlocked
def _socket_get(self):
if self.parent.axis_Id_numeric == 2:
ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric)
return ret
if self.parent.axis_Id_numeric == 4:
# Motion signal from axis 4 is mapped to axis 5
ret = self.controller.is_axis_moving("F", 5)
return ret or self.controller.is_thread_active(4)
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 GalilAxesReferenced(GalilSignalRO):
@threadlocked
def _socket_get(self):
return self.controller.socket_put_and_receive("MG allaxref")
class SGalilMotor(Device, PositionerBase):
""" "SGalil Motors at cSAXS have a
DC motor (y axis - vertical) - implemented as C
and a step motor (x-axis horizontal) - implemented as E
that require different communication for control
"""
USER_ACCESS = ["controller"]
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
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="129.129.122.26",
port=23,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.axis_Id = axis_Id
self.sign = sign
self.controller = GalilController(socket=socket_cls(host=host, port=port))
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_mapping = kwargs.pop("device_mapping", {})
self.device_manager = device_manager
if len(self.device_mapping) > 0 and self.device_manager is None:
raise BECConfigError(
"device_mapping has been specified but the device_manager cannot be accessed."
)
self.rt = self.device_mapping.get("rt")
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():
logger.info("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(1.5)
val = self.readback.read()
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
if not success:
print(" stop")
self._done_moving(success=success)
logger.info("Move finished")
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.")
if val not in ["C", "E"]:
raise ValueError(
f"axis_id {val} is currently not supported, please use either 'C' or 'E'."
)
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 not in [2, 4]:
raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.")
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)}.")
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "mm"
def stop(self, *, success=False):
self.controller.stop_all_axes()
# last_speed = self.controller.socket_put_and_receive("MG")
rtr = self.controller.socket_put_and_receive(f"SPC={2*10000}")
logger.info(f"{rtr}")
# logger.info(f'Motor stopped, restored speed for samy from {last_speed}mm/s to 2mm/s')
return super().stop(success=success)
def kickoff(self) -> DeviceStatus:
status = DeviceStatus(self)
self.controller.fly_grid_scan(
status,
self._kickoff_params.get("start_y"),
self._kickoff_params.get("end_y"),
self._kickoff_params.get("interval_y"),
self._kickoff_params.get("start_x"),
self._kickoff_params.get("end_x"),
self._kickoff_params.get("interval_x"),
self._kickoff_params.get("exp_time"),
self._kickoff_params.get("readout_time"),
)
return status
def configure(self, parameter: dict, **kwargs) -> None:
self._kickoff_params = parameter
if __name__ == "__main__":
mock = False
if not mock:
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
else:
from ophyd_devices.utils.socket import SocketMock
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock)
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock)
samx.controller.galil_show_all()

View File

@@ -1,79 +0,0 @@
# Documentation SGalil ophyd wrapper
Ophyd wrapper for the SGalil controller and stages.
## TODO tests and evaluate whether its good to combine common functionaltiy with galil lamni/omny/flomni controller
## Integration of the device in IPython kernel
BEC needs to be able to reach the host TCP to initiate a connection to the device.
```Python
from csaxs_bec.devices.galil.sgalil_ophyd import SGalilMotor
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
# connect to the controller
samx.controller.on()
samx.read()
samx.move(5)
dir(samx)# for full printout of commands
# useful for development, check below socket communication with sgalil controller
samx.controller.socket_put_and_receive('#string: message_to_controller')
```
## TODO Integration of device in BEC device config!
to be tested too
## Fly scans
2D grid fly scan as implemented on the controller.
TTL triggers are sent for the start of each line.
The scan on the controller needs to be matched with an appropriate triggering scheme, as for instance shown in the attached scheme together with the Stanford Research DG645 device at cSAXS.
![image info](./csaxs_sgalil_triggering.png)
```Python
samx.controller.(start_y, end_y, interval_y, start_x, end_x, interval_x, exp_time, readtime)
# for example
samx.controller.fly_grid_scan(start_y= 16, end_y= 24, interval_y= 100, start_x= 18, end_x= 17.6, interval_x= 2, exp_time= 0.08, readtime= 0.005)
```
## TODO implement line scan
Check SPEC implementation for line scans with sgalil controller, and complement it with a suitable triggering scheme of the DG645.
## TODO readout of positions in encoder
Should this be integrated in the flyscan or not.
To be explored where this is most suitable.
## Socket communication with sgalil controller
### vertical axis (samy)
- initiate with axis 2, C
- in motion: "MG _BG{axis_char}", e.g. "MG _BGC" , 0 or 1
- limit switch not pressed: "MG _LR{axis_char}, _LF{axis_char}" , 0 or 1
- position: "MG _TP{axis_char}/mm" , position in mm
- Axis referenced: "MG allaxref", 0 or 1
- stop all axis: "XQ#STOP,1"
- is motor on: "MG _MO{axis_char}", 0 or 1
- is thread active: "MG _XQ{thread_id}", 0 or 1
**Specific for sgalil_y**
- set_motion_speed: "SP{axis_char}=2*mm", 2mm/s is max speed
- set_final_pos: "PA{axis_char}={val:04f}*mm", target pos in mm
- start motion: "BG{axis_char}", start motion
### horizontal axis (samx)
note: some hardware modifications were done that require access to different channels in the encoder. Encoder, motor and limit switches are not controlled by the same endpoint/axis of the controller... see below
- initiate with axis 4, E
**Specific for sgalil_x**
- set_final_pos: "targ{axis_char}={val:04f}", e.g. "targE=2.0000"
- start motion: "XQ#POSE,{axis_char}"
- For *in motion* and *limit switch not pressed* commands,
the key changes to AXIS 5 || F, e.g. "MG _BGF"
- For *position* switch to Axis 0 || A, e.g. "MG _TPA/mm"
### flyscan 2D grid commanes:
Last command ('XQ#SCANG') has to come with sufficient delay, important for setting up dedicated scans
f***ast axis***
- self.socket_put_and_receive(f'a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}')
***slow axis***
- self.socket_put_and_receive(f'b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}')
- self.socket_put_and_receive(f'nums={n_samples}') # Declare number of triggers for encoder
- self.socket_put_and_receive('XQ#SAMPLE') # Reset encoder counting --> sampling starts with 0
Start scan (be aware, needs some waiting from before)
- self.socket_put_and_receive('XQ#SCANG')
### Encoder readings!
The encoder readout is triggered by an TTL pulse.
Unfortunately, TTL triggers to the encoder can only be accepted with at least 12.5ms time between rising/falling edges. Therefore, maximum readout has to be ~25Hz, rather 30Hz (experimentally determined).
Socket commands for the readout:
- self.socket_put_and_receive('MGsposct') # get current position counter
- self.socket_put_and_receive('MGaposavg[{ii%2000}]*10, cposavg[{ii%2000}]*10,') # loop over ii

View File

@@ -1 +0,0 @@
from .npoint import NPointAxis, NPointController

View File

@@ -1,545 +0,0 @@
import functools
import socket
import threading
import time
from ophyd_devices.utils.controller import threadlocked
from ophyd_devices.utils.socket import raise_if_disconnected
from prettytable import PrettyTable
from typeguard import typechecked
def channel_checked(fcn):
"""Decorator to catch attempted access to channels that are not available."""
@functools.wraps(fcn)
def wrapper(self, *args, **kwargs):
self._check_channel(args[0])
return fcn(self, *args, **kwargs)
return wrapper
class SocketIO:
"""SocketIO helper class for TCP IP connections"""
def __init__(self, sock=None):
self.is_open = False
if sock is None:
self.open()
else:
self.sock = sock
def connect(self, host, port):
print(f"connecting to {host} port {port}")
# self.sock.create_connection((host, port))
self.sock.connect((host, port))
def _put(self, msg_bytes):
return self.sock.send(msg_bytes)
def _recv(self, buffer_length=1024):
return self.sock.recv(buffer_length)
def _initialize_socket(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(5)
def put(self, msg):
return self._put(msg)
def receive(self, buffer_length=1024):
return self._recv(buffer_length=buffer_length)
def open(self):
self._initialize_socket()
self.is_open = True
def close(self):
self.sock.close()
self.sock = None
self.is_open = False
class NPointController:
_controller_instance = None
NUM_CHANNELS = 3
_read_single_loc_bit = "A0"
_write_single_loc_bit = "A2"
_trailing_bit = "55"
_range_offset = "78"
_channel_base = ["11", "83"]
def __init__(
self, comm_socket: SocketIO, server_ip: str = "129.129.99.87", server_port: int = 23
) -> None:
self._lock = threading.RLock()
super().__init__()
self._server_and_port_name = (server_ip, server_port)
self.socket = comm_socket
self.connected = False
def __new__(cls, *args, **kwargs):
if not NPointController._controller_instance:
NPointController._controller_instance = object.__new__(cls)
return NPointController._controller_instance
@classmethod
def create(cls):
return cls(SocketIO())
def show_all(self) -> None:
"""Display current status of all channels
Returns:
None
"""
if not self.connected:
print("npoint controller is currently disabled.")
return
print(f"Connected to controller at {self._server_and_port_name}")
t = PrettyTable()
t.field_names = ["Channel", "Range", "Position", "Target"]
for ii in range(self.NUM_CHANNELS):
t.add_row(
[ii, self._get_range(ii), self._get_current_pos(ii), self._get_target_pos(ii)]
)
print(t)
@threadlocked
def on(self) -> None:
"""Enable the NPoint controller and open a new socket.
Raises:
TimeoutError: Raised if the socket connection raises a timeout.
Returns:
None
"""
if self.connected:
print("You are already connected to the NPoint controller.")
return
if not self.socket.is_open:
self.socket.open()
try:
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
except socket.timeout:
raise TimeoutError(
f"Failed to connect to the specified server and port {self._server_and_port_name}."
)
except OSError:
print("ERROR while connecting. Let's try again")
self.socket.close()
time.sleep(0.5)
self.socket.open()
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
self.connected = True
@threadlocked
def off(self) -> None:
"""Disable the controller and close the socket.
Returns:
None
"""
self.socket.close()
self.connected = False
@channel_checked
def _get_range(self, channel: int) -> int:
"""Get the range of the specified channel axis.
Args:
channel (int): Channel for which the range should be requested.
Raises:
RuntimeError: Raised if the received message doesn't have the expected number of bytes (10).
Returns:
int: Range
"""
# for first channel: 0x11 83 10 78
addr = self._channel_base.copy()
addr.extend([f"{16 + 16 * channel:x}", self._range_offset])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
if len(recvd) != 10:
raise RuntimeError(
f"Received buffer is corrupted. Expected 10 bytes and instead got {len(recvd)}"
)
device_range = self._hex_list_to_int(recvd[5:-1], signed=False)
return device_range
@channel_checked
def _get_current_pos(self, channel: int) -> float:
# for first channel: 0x11 83 13 34
addr = self._channel_base.copy()
addr.extend([f"{19 + 16 * channel:x}", "34"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
pos_buffer = recvd[5:-1]
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
return pos
@channel_checked
def _set_target_pos(self, channel: int, pos: float) -> None:
# for first channel: 0x11 83 12 18 00 00 00 00
addr = self._channel_base.copy()
addr.extend([f"{18 + channel * 16:x}", "18"])
target = int(round(1048574 / 100 * pos))
data = [f"{m:02x}" for m in target.to_bytes(4, byteorder="big", signed=True)]
send_buffer = self.__write_single_location_buffer(addr, data)
self._put(send_buffer)
@channel_checked
def _get_target_pos(self, channel: int) -> float:
# for first channel: 0x11 83 12 18
addr = self._channel_base.copy()
addr.extend([f"{18 + channel * 16:x}", "18"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
pos_buffer = recvd[5:-1]
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
return pos
@channel_checked
def _set_servo(self, channel: int, enable: bool) -> None:
print("Not tested")
return
# for first channel: 0x11 83 10 84 00 00 00 00
addr = self._channel_base.copy()
addr.extend([f"{16 + channel * 16:x}", "84"])
if enable:
data = ["00"] * 3 + ["01"]
else:
data = ["00"] * 4
send_buffer = self.__write_single_location_buffer(addr, data)
self._put(send_buffer)
@channel_checked
def _get_servo(self, channel: int) -> int:
# for first channel: 0x11 83 10 84 00 00 00 00
addr = self._channel_base.copy()
addr.extend([f"{16 + channel * 16:x}", "84"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
buffer = recvd[5:-1]
status = self._hex_list_to_int(buffer)
return status
@threadlocked
def _put(self, buffer: list) -> None:
"""Translates a list of hex values to bytes and sends them to the socket.
Args:
buffer (list): List of hex values without leading 0x
Returns:
None
"""
buffer = b"".join([bytes.fromhex(m) for m in buffer])
self.socket.put(buffer)
@threadlocked
def _put_and_receive(self, msg_hex_list: list) -> list:
"""Send msg to socket and wait for a reply.
Args:
msg_hex_list (list): List of hex values without leading 0x.
Returns:
list: Received message as a list of hex values
"""
buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list])
self.socket.put(buffer)
recv_msg = self.socket.receive()
recv_hex_list = [hex(m) for m in recv_msg]
self._verify_received_msg(msg_hex_list, recv_hex_list)
return recv_hex_list
def _verify_received_msg(self, in_list: list, out_list: list) -> None:
"""Ensure that the first address bits of sent and received messages are the same.
Args:
in_list (list): list containing the sent message
out_list (list): list containing the received message
Raises:
RuntimeError: Raised if first two address bits of 'in' and 'out' are not identical
Returns:
None
"""
# first, translate hex (str) values to int
in_list_int = [int(val, 16) for val in in_list]
out_list_int = [int(val, 16) for val in out_list]
# first ints of the reply should be the same. Otherwise something went wrong
if not in_list_int[:2] == out_list_int[:2]:
raise RuntimeError("Connection failure. Please restart the controller.")
def _check_channel(self, channel: int) -> None:
if channel >= self.NUM_CHANNELS:
raise ValueError(
f"Channel {channel+1} exceeds the available number of channels ({self.NUM_CHANNELS})"
)
@staticmethod
def _hex_list_to_int(in_buffer: list, byteorder="little", signed=True) -> int:
"""Translate hex list to int.
Args:
in_buffer (list): Input buffer; received as list of hex values
byteorder (str, optional): Byteorder of in_buffer. Defaults to "little".
signed (bool, optional): Whether the hex list represents a signed int. Defaults to True.
Returns:
int: Translated integer.
"""
if byteorder == "little":
in_buffer.reverse()
# make sure that all hex strings have the same format ("FF")
val_hex = [f"{int(m, 16):02x}" for m in in_buffer]
val_bytes = [bytes.fromhex(m) for m in val_hex]
val = int.from_bytes(b"".join(val_bytes), byteorder="big", signed=signed)
return val
@staticmethod
def __read_single_location_buffer(addr) -> list:
"""Prepare buffer for reading from a single memory location (hex address).
Number of bytes: 6
Format: 0xA0 [addr] 0x55
Return Value: 0xA0 [addr] [data] 0x55
Sample Hex Transmission from PC to LC.400: A0 18 12 83 11 55
Sample Hex Return Transmission from LC.400 to PC: A0 18 12 83 11 64 00 00 00 55
Args:
addr (list): Hex address to read from
Returns:
list: List of hex values representing the read instruction.
"""
buffer = []
buffer.append(NPointController._read_single_loc_bit)
if isinstance(addr, list):
addr.reverse()
buffer.extend(addr)
else:
buffer.append(addr)
buffer.append(NPointController._trailing_bit)
return buffer
@staticmethod
def __write_single_location_buffer(addr: list, data: list) -> list:
"""Prepare buffer for writing to a single memory location (hex address).
Number of bytes: 10
Format: 0xA2 [addr] [data] 0x55
Return Value: none
Sample Hex Transmission from PC to C.400: A2 18 12 83 11 E8 03 00 00 55
Args:
addr (list): List of hex values representing the address to write to.
data (list): List of hex values representing the data that should be written.
Returns:
list: List of hex values representing the write instruction.
"""
buffer = []
buffer.append(NPointController._write_single_loc_bit)
if isinstance(addr, list):
addr.reverse()
buffer.extend(addr)
else:
buffer.append(addr)
if isinstance(data, list):
data.reverse()
buffer.extend(data)
else:
buffer.append(data)
buffer.append(NPointController._trailing_bit)
return buffer
@staticmethod
def __read_array():
raise NotImplementedError
@staticmethod
def __write_next_command():
raise NotImplementedError
def __del__(self):
if self.connected:
print("Closing npoint socket")
self.off()
class NPointAxis:
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
super().__init__()
self._axis_range = 100
self.controller = controller
self.channel = channel
self.name = name
self.controller._check_channel(channel)
self._settling_time = 0.1
if self.settling_time == 0:
self.settling_time = 0.1
print(f"Setting the npoint settling time to {self.settling_time:.2f} s.")
print(
"You can set the settling time depending on the stage tuning\nusing the settling_time property."
)
print("This is the waiting time before the counting is done.")
def show_all(self) -> None:
self.controller.show_all()
@raise_if_disconnected
def get(self) -> float:
"""Get current position for this channel.
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
float: position
"""
return self.controller._get_current_pos(self.channel)
@raise_if_disconnected
def get_target_pos(self) -> float:
"""Get target position for this channel.
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
float: position
"""
return self.controller._get_target_pos(self.channel)
@raise_if_disconnected
@typechecked
def set(self, pos: float) -> None:
"""Set a new target position and wait until settled (settling_time).
Args:
pos (float): New target position
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
None
"""
self.controller._set_target_pos(self.channel, pos)
time.sleep(self.settling_time)
@property
def connected(self) -> bool:
return self.controller.connected
@property
@raise_if_disconnected
def servo(self) -> int:
"""Get servo status
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
int: Servo status
"""
return self.controller._get_servo(self.channel)
@servo.setter
@raise_if_disconnected
@typechecked
def servo(self, val: bool) -> None:
"""Set servo status
Args:
val (bool): Servo status
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
None
"""
self.controller._set_servo(self.channel, val)
@property
def settling_time(self) -> float:
return self._settling_time
@settling_time.setter
@typechecked
def settling_time(self, val: float) -> None:
self._settling_time = val
print(f"Setting the npoint settling time to {val:.2f} s.")
class NPointEpics(NPointAxis):
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
super().__init__(controller, channel, name)
self.low_limit = -50
self.high_limit = 50
self._prefix = name
def get_pv(self) -> str:
return self.name
def get_position(self, readback=True) -> float:
if readback:
return self.get()
else:
return self.get_target_pos()
def within_limits(self, pos: float) -> bool:
return pos > self.low_limit and pos < self.high_limit
def move(self, position: float, wait=True) -> None:
self.set(position)
if __name__ == "__main__":
## EXAMPLES ##
#
# Create controller and socket instance explicitly:
# controller = NPointController(SocketIO())
# npointx = NPointAxis(controller, 0, "nx")
# npointy = NPointAxis(controller, 1, "ny")
# Create controller instance explicitly
# controller = NPointController.create()
# npointx = NPointAxis(controller, 0, "nx")
# npointy = NPointAxis(controller, 1, "ny")
# Single-line axis:
# npointx = NPointAxis(NPointController.create(), 0, "nx")
#
# EPICS wrapper:
# nx = NPointEpics(NPointController.create(), 0, "nx")
controller = NPointController.create()
npointx = NPointAxis(NPointController.create(), 0, "nx")
npointy = NPointAxis(NPointController.create(), 0, "ny")

View File

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

View File

@@ -1,811 +0,0 @@
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 csaxs_bec.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
from prettytable import PrettyTable
logger = bec_logger.logger
class RtFlomniController(RtController):
USER_ACCESS = [
"socket_put_and_receive",
"set_rotation_angle",
"feedback_disable",
"feedback_enable_without_reset",
"feedback_enable_with_reset",
"feedback_is_running",
"add_pos_to_scan",
"get_pid_x",
"move_samx_to_scan_region",
"clear_trajectory_generator",
"show_cyclic_error_compensation",
"laser_tracker_on",
"laser_tracker_off",
"laser_tracker_show_all",
"show_signal_strength_interferometer",
"read_ssi_interferometer",
"laser_tracker_check_signalstrength",
"laser_tracker_check_enabled",
]
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]:.05f},{pos[1]:.05f},{pos[2]:.05f}")
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)
def move_samx_to_scan_region(self, fovx: float, cenx: float):
time.sleep(0.05)
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("w1").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_check_enabled(self) -> bool:
self.laser_update_tracker_info()
if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]:
return True
else:
return False
def laser_tracker_on(self):
if not self.laser_tracker_check_enabled():
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):
if self.feedback_is_running():
print(
"Interferometer feedback is running. Cannot disable the tracker. First disable the feedback using rt_feedback_disable()"
)
else:
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 laser_tracker_check_signalstrength(self):
if not self.laser_tracker_check_enabled():
returnval = "disabled"
else:
returnval = "ok"
self.laser_tracker_wait_on_target()
signal = self.read_ssi_interferometer(1)
rtx = self.get_device_manager().devices.rtx
min_signal = rtx.user_parameter.get("min_signal")
low_signal = rtx.user_parameter.get("low_signal")
if signal < min_signal:
time.sleep(1)
if signal < min_signal:
print(
f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m"
)
returnval = "toolow"
# raise RtError("The interferometer signal of tracker is too low.")
elif signal < low_signal:
print(
f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m"
)
returnval = "low"
return returnval
def show_signal_strength_interferometer(self):
t = PrettyTable()
t.title = f"Interferometer signal strength"
t.field_names = ["Axis", "Value"]
for i in range(4):
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, point_id=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, point_id=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, point_id):
self.get_device_manager().connector.set_and_publish(
MessageEndpoints.device_read("rt_flomni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **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._stopped = False
# 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() and not self._stopped:
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(success=(not self._stopped))
self._stopped = False
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()
self._stopped = True
return super().stop(success=success)
if __name__ == "__main__":
rtcontroller = RtFlomniController(
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222
)
rtcontroller.on()
rtcontroller.laser_tracker_on()

View File

@@ -1,847 +0,0 @@
import functools
import threading
import time
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 RtLamniCommunicationError(Exception):
pass
class RtLamniError(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 (RtLamniCommunicationError, RtLamniError):
val = fcn(self, *args, **kwargs)
return val
return wrapper
class RtLamniController(Controller):
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 __init__(
self,
*,
name="RtLamniController",
kind=None,
parent=None,
socket=None,
attr_name="",
labels=None,
):
if not hasattr(self, "_initialized") or not self._initialized:
self._rtlamni_axis_per_controller = 3
self._axis = [None for axis_num in range(self._rtlamni_axis_per_controller)]
self._min_scan_buffer_reached = False
super().__init__(
name=name,
socket=socket,
attr_name=attr_name,
parent=parent,
labels=labels,
kind=kind,
)
self.readout_metadata = {}
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 RtLamniCommunicationError from conn_error
else:
logger.info("The connection has already been established.")
# warnings.warn(f"The connection has already been established.", stacklevel=2)
self._update_flyer_device_info()
def off(self) -> None:
"""Close the socket connection to the controller"""
if self.connected:
self.sock.close()
self.connected = False
else:
logger.info("The connection is already closed.")
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]:.05f},{pos[1]:05f},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 RtLamniCommunicationError(
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 RtLamniError(
"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 RtLamniError("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),
)
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
),
)
# 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, point_id=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, point_id=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
),
)
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, point_id):
self.get_device_manager().connector.set_and_publish(
MessageEndpoints.device_read("rt_lamni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
),
)
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 RtLamniError(
"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 RtLamniError(
"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 RtLamniSignalBase(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 RtLamniSignalRO(RtLamniSignalBase):
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 RtLamniReadbackSignal(RtLamniSignalRO):
@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 RtLamniError("Currently, only two axes are supported.")
current_pos = float(return_table[readback_index])
current_pos *= self.parent.sign
return current_pos
class RtLamniSetpointSignal(RtLamniSignalBase):
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:
RtLamniError: 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 RtLamniError(
"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 RtLamniMotorIsMoving(RtLamniSignalRO):
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 RtLamniFeedbackRunning(RtLamniSignalRO):
@threadlocked
def _socket_get(self):
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
return 1
else:
return 0
class RtLamniMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint")
motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal")
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 = RtLamniController(socket=socket_cls(host=host, port=port))
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
self.tolerance = kwargs.pop("tolerance", 0.5)
super().__init__(
prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
self.readback.name = self.name
self.controller.subscribe(
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
)
self._update_connection_state()
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
if limits is not None:
assert len(limits) == 2
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
walk.item._metadata["connected"] = self.controller.connected
def _forward_readback(self, **kwargs):
kwargs.pop("sub_type")
self._run_subs(sub_type="readback", **kwargs)
@raise_if_disconnected
def move(self, position, wait=True, **kwargs):
"""Move to a specified position, optionally waiting for motion to
complete.
Parameters
----------
position
Position to move to
moved_cb : callable
Call this callback when movement has finished. This callback must
accept one keyword argument: 'obj' which will be set to this
positioner instance.
timeout : float, optional
Maximum time to wait for the motion. If None, the default timeout
for this positioner is used.
Returns
-------
status : MoveStatus
Raises
------
TimeoutError
When motion takes longer than `timeout`
ValueError
On invalid positions
RuntimeError
If motion fails other than timing out
"""
self._started_moving = False
timeout = kwargs.pop("timeout", 100)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)
def move_and_finish():
while self.motor_is_moving.get():
print("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.01)
print("Move finished")
self._done_moving()
threading.Thread(target=move_and_finish, daemon=True).start()
try:
if wait:
status_wait(status)
except KeyboardInterrupt:
self.stop()
raise
return status
@property
def axis_Id(self):
return self._axis_Id_alpha
@axis_Id.setter
def axis_Id(self, val):
if isinstance(val, str):
if len(val) != 1:
raise ValueError(f"Only single-character axis_Ids are supported.")
self._axis_Id_alpha = val
self._axis_Id_numeric = ord(val.lower()) - 97
else:
raise TypeError(f"Expected value of type str but received {type(val)}")
@property
def axis_Id_numeric(self):
return self._axis_Id_numeric
@axis_Id_numeric.setter
def axis_Id_numeric(self, val):
if isinstance(val, int):
if val > 26:
raise ValueError(f"Numeric value exceeds supported range.")
self._axis_Id_alpha = val
self._axis_Id_numeric = (chr(val + 97)).capitalize()
else:
raise TypeError(f"Expected value of type int but received {type(val)}")
def kickoff(self, metadata, **kwargs) -> None:
self.controller.kickoff(metadata)
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "um"
# how is this used later?
def stage(self) -> list[object]:
return super().stage()
def unstage(self) -> list[object]:
return super().unstage()
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
mock = False
if not mock:
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
rty.stage()
status = rty.move(0, wait=True)
status = rty.move(10, wait=True)
rty.read()
rty.get()
rty.describe()
rty.unstage()
else:
from ophyd_devices.utils.socket import SocketMock
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
rtx.stage()
# rty.stage()

View File

@@ -1,817 +0,0 @@
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, point_id=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, point_id=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, point_id):
self.get_device_manager().connector.set_and_publish(
MessageEndpoints.device_read("rt_lamni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
).dumps(),
)
def feedback_status_angle_lamni(self) -> bool:
return_table = (self.socket_put_and_receive(f"J7")).split(",")
logger.debug(
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
)
return bool(return_table[0])
def feedback_enable_with_reset(self):
if not self.feedback_status_angle_lamni():
self.feedback_disable_and_even_reset_lamni_angle_interferometer()
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
else:
self.feedback_disable()
logger.info(
f"LamNI resetting interferomter except angular interferometer which is already running."
)
# set these as closed loop target position
self.socket_put(f"pa0,0")
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
self.socket_put(f"pa1,0")
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
self.socket_put(
f"pa2,0"
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
self.clear_trajectory_generator()
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
galil_controller_rt_status = (
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
)
if galil_controller_rt_status == 0:
logger.error(
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
)
raise RtError(
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
)
time.sleep(0.03)
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
raise RuntimeError("lsamx center is not defined")
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
raise RuntimeError("lsamy center is not defined")
lsamx_center = lsamx_user_params.get("center")
lsamy_center = lsamy_user_params.get("center")
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
self.socket_put("J1")
_waitforfeedbackctr = 0
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100:
time.sleep(0.01)
_waitforfeedbackctr = _waitforfeedbackctr + 1
interferometer_feedback_not_running = int(
(self.socket_put_and_receive("J2")).split(",")[0]
)
self.set_device_enabled("lsamx", False)
self.set_device_enabled("lsamy", False)
self.set_device_enabled("loptx", False)
self.set_device_enabled("lopty", False)
self.set_device_enabled("loptz", False)
if interferometer_feedback_not_running == 1:
logger.error(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
raise RtError(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
time.sleep(0.01)
# ptychography_alignment_done = 0
def set_device_enabled(self, device_name: str, enabled: bool) -> None:
"""enable / disable a device"""
if device_name not in self.get_device_manager().devices:
logger.warning(
f"Device {device_name} is not configured and cannot be enabled/disabled."
)
return
self.get_device_manager().devices[device_name].read_only = not enabled
class RtSignalBase(SocketSignal):
def __init__(self, signal_name, **kwargs):
self.signal_name = signal_name
super().__init__(**kwargs)
self.controller = self.parent.controller
self.sock = self.parent.controller.sock
class RtSignalRO(RtSignalBase):
def __init__(self, signal_name, **kwargs):
super().__init__(signal_name, **kwargs)
self._metadata["write_access"] = False
def _socket_set(self, val):
raise ReadOnlyError("Read-only signals cannot be set")
class RtReadbackSignal(RtSignalRO):
@retry_once
@threadlocked
def _socket_get(self) -> float:
"""Get command for the readback signal
Returns:
float: Readback value after adjusting for sign and motor resolution.
"""
return_table = (self.controller.socket_put_and_receive(f"J4")).split(",")
print(return_table)
if self.parent.axis_Id_numeric == 0:
readback_index = 2
elif self.parent.axis_Id_numeric == 1:
readback_index = 1
else:
raise RtError("Currently, only two axes are supported.")
current_pos = float(return_table[readback_index])
current_pos *= self.parent.sign
return current_pos
class RtSetpointSignal(RtSignalBase):
setpoint = 0
def _socket_get(self) -> float:
"""Get command for receiving the setpoint / target value.
The value is not pulled from the controller but instead just the last setpoint used.
Returns:
float: setpoint / target value
"""
return self.setpoint
@retry_once
@threadlocked
def _socket_set(self, val: float) -> None:
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
Args:
val (float): Target value / setpoint value
Raises:
RtError: Raised if interferometer feedback is disabled.
"""
interferometer_feedback_not_running = int(
(self.controller.socket_put_and_receive("J2")).split(",")[0]
)
if interferometer_feedback_not_running != 0:
raise RtError(
"The interferometer feedback is not running. Either it is turned off or and interferometer error occured."
)
self.set_with_feedback_disabled(val)
def set_with_feedback_disabled(self, val):
target_val = val * self.parent.sign
self.setpoint = target_val
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
class RtMotorIsMoving(RtSignalRO):
def _socket_get(self):
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
def get(self):
val = super().get()
if val is not None:
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
return val
class RtFeedbackRunning(RtSignalRO):
@threadlocked
def _socket_get(self):
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
return 1
else:
return 0
class RtMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(RtReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint")
motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal")
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
SUB_READBACK = "readback"
SUB_CONNECTION_CHANGE = "connection_change"
_default_sub = SUB_READBACK
def __init__(
self,
axis_Id,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
host="mpc2680.psi.ch",
port=3333,
sign=1,
socket_cls=SocketIO,
device_manager=None,
limits=None,
**kwargs,
):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtController(socket=socket_cls(host=host, port=port))
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
self.tolerance = kwargs.pop("tolerance", 0.5)
super().__init__(
prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
self.readback.name = self.name
self.controller.subscribe(
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
)
self._update_connection_state()
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
if limits is not None:
assert len(limits) == 2
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
walk.item._metadata["connected"] = self.controller.connected
def _forward_readback(self, **kwargs):
kwargs.pop("sub_type")
self._run_subs(sub_type="readback", **kwargs)
@raise_if_disconnected
def move(self, position, wait=True, **kwargs):
"""Move to a specified position, optionally waiting for motion to
complete.
Parameters
----------
position
Position to move to
moved_cb : callable
Call this callback when movement has finished. This callback must
accept one keyword argument: 'obj' which will be set to this
positioner instance.
timeout : float, optional
Maximum time to wait for the motion. If None, the default timeout
for this positioner is used.
Returns
-------
status : MoveStatus
Raises
------
TimeoutError
When motion takes longer than `timeout`
ValueError
On invalid positions
RuntimeError
If motion fails other than timing out
"""
self._started_moving = False
timeout = kwargs.pop("timeout", 100)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)
def move_and_finish():
while self.motor_is_moving.get():
print("motor is moving")
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.01)
print("Move finished")
self._done_moving()
threading.Thread(target=move_and_finish, daemon=True).start()
try:
if wait:
status_wait(status)
except KeyboardInterrupt:
self.stop()
raise
return status
@property
def axis_Id(self):
return self._axis_Id_alpha
@axis_Id.setter
def axis_Id(self, val):
if isinstance(val, str):
if len(val) != 1:
raise ValueError(f"Only single-character axis_Ids are supported.")
self._axis_Id_alpha = val
self._axis_Id_numeric = ord(val.lower()) - 97
else:
raise TypeError(f"Expected value of type str but received {type(val)}")
@property
def axis_Id_numeric(self):
return self._axis_Id_numeric
@axis_Id_numeric.setter
def axis_Id_numeric(self, val):
if isinstance(val, int):
if val > 26:
raise ValueError(f"Numeric value exceeds supported range.")
self._axis_Id_alpha = val
self._axis_Id_numeric = (chr(val + 97)).capitalize()
else:
raise TypeError(f"Expected value of type int but received {type(val)}")
def kickoff(self, metadata, **kwargs) -> None:
self.controller.kickoff(metadata)
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "um"
# how is this used later?
def stage(self) -> List[object]:
return super().stage()
def unstage(self) -> List[object]:
return super().unstage()
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
mock = False
if not mock:
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
rty.stage()
status = rty.move(0, wait=True)
status = rty.move(10, wait=True)
rty.read()
rty.get()
rty.describe()
rty.unstage()
else:
from ophyd_devices.utils.socket import SocketMock
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
rtx.stage()
# rty.stage()

View File

@@ -1,9 +0,0 @@
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal
class Xeye(Device):
save_frame = Cpt(EpicsSignal, "XOMNY-XEYE-SAVEFRAME:0")
acquisition_done = Cpt(EpicsSignal, "XOMNY-XEYE-ACQDONE:0")
acquisition = Cpt(EpicsSignal, "XOMNY-XEYE-ACQ:0")
x_width = Cpt(EpicsSignal, "XOMNY-XEYE-XWIDTH_X:0")

View File

@@ -1,2 +0,0 @@
from .smaract_controller import SmaractController
from .smaract_ophyd import SmaractMotor

View File

@@ -1,44 +0,0 @@
import json
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.ophydobj import OphydObject
from ophyd_devices.utils.socket import SocketMock
def get_user_functions(obj) -> list:
exclude_list = ["log", "SUB_CONNECTION_CHANGE"]
exclude_classes = [Device, OphydObject, PositionerBase, Signal, Cpt]
for cls in exclude_classes:
exclude_list.extend(dir(cls))
access_list = [
func for func in dir(obj) if not func.startswith("_") and func not in exclude_list
]
return access_list
def is_serializable(f) -> bool:
try:
json.dumps(f)
return True
except (TypeError, OverflowError):
return False
def get_user_interface(obj, obj_interface):
# user_funcs = get_user_functions(obj)
for f in [f for f in dir(obj) if f in obj.USER_ACCESS]:
if f == "controller" or f == "on":
print(f)
m = getattr(obj, f)
if not callable(m):
if is_serializable(m):
obj_interface[f] = {"type": type(m).__name__}
elif isinstance(m, SocketMock):
pass
else:
obj_interface[f] = get_user_interface(m, {})
else:
obj_interface[f] = {"type": "func"}
return obj_interface

View File

@@ -1,507 +0,0 @@
import enum
import functools
import json
import logging
import os
import time
import numpy as np
from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked
from prettytable import PrettyTable
from typeguard import typechecked
from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
logger = logging.getLogger("smaract_controller")
class SmaractCommunicationMode(enum.Enum):
SYNC = 0
ASYNC = 1
def retry_once(fcn):
"""Decorator to rerun a function in case a SmaractCommunicationError 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 (SmaractCommunicationError, SmaractErrorCode):
val = fcn(self, *args, **kwargs)
return val
return wrapper
class SmaractChannelStatus(enum.Enum):
STOPPED = 0
STEPPING = 1
SCANNING = 2
HOLDING = 3
TARGETING = 4
MOVE_DELAY = 5
CALIBRATING = 6
FINDING_REFERENCE_MARK = 7
LOCKED = 9
class SmaractSensorDefinition:
def __init__(self, symbol, type_code, positioner_series, comment, reference_type) -> None:
self.symbol = symbol
self.type_code = type_code
self.comment = comment
self.positioner_series = positioner_series
self.reference_type = reference_type
class SmaractSensors:
smaract_sensor_definition_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "smaract_sensors.json"
)
def __init__(self) -> None:
self.avail_sensors = {}
with open(self.smaract_sensor_definition_file) as json_file:
sensor_list = json.load(json_file)
for sensor in sensor_list:
self.avail_sensors[sensor["type_code"]] = SmaractSensorDefinition(**sensor)
class SmaractController(Controller):
_axes_per_controller = 6
_initialized = False
USER_ACCESS = [
"socket_put_and_receive",
"smaract_show_all",
"move_open_loop_steps",
"find_reference_mark",
"describe",
"axis_is_referenced",
"all_axes_referenced",
]
def __init__(
self,
*,
name="SmaractController",
kind=None,
parent=None,
socket_cls=None,
socket_host=None,
socket_port=None,
attr_name="",
labels=None,
):
if not self._initialized:
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._sensors = SmaractSensors()
@threadlocked
def socket_put(self, val: str):
self.sock.put(f":{val}\n".encode())
@threadlocked
def socket_get(self):
return self.sock.receive().decode()
@threadlocked
def socket_put_and_receive(
self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False
) -> str:
self.socket_put(val)
return_val = ""
max_wait_time = 1
elapsed_time = 0
sleep_time = 0.01
while True:
ret = self.socket_get()
return_val += ret
if ret.endswith("\n"):
break
time.sleep(sleep_time)
elapsed_time += sleep_time
if elapsed_time > max_wait_time:
break
if remove_trailing_chars:
return_val = self._remove_trailing_characters(return_val)
logger.debug(f"Sending {val}; Returned {return_val}")
if check_for_errors:
self._check_for_error(return_val, raise_if_not_status=raise_if_not_status)
return return_val
@retry_once
@axis_checked
def get_status(self, axis_Id_numeric: int) -> SmaractChannelStatus:
"""Returns the current movement status code of a positioner or end effector.This command can be used to check whether a previously issued movement command has been completed.
Args:
axis_Id_numeric (int): Axis number
Returns:
SmaractChannelStatus: Channel status
"""
return_val = self.socket_put_and_receive(f"GS{axis_Id_numeric}")
if self._message_starts_with(return_val, f":S{axis_Id_numeric}"):
return SmaractChannelStatus(int(return_val.split(",")[1]))
@retry_once
@axis_checked
def is_axis_moving(self, axis_Id_numeric: int) -> bool:
"""Check if axis is moving. Returns true upon open loop move, scanning, closed loop move or reference mark search.
Args:
axis_Id_numeric (int): Axis number.
Returns:
bool: True if axis is moving.
"""
axis_status = self.get_status(axis_Id_numeric)
return axis_status in [
SmaractChannelStatus.STEPPING,
SmaractChannelStatus.SCANNING,
SmaractChannelStatus.TARGETING,
SmaractChannelStatus.FINDING_REFERENCE_MARK,
]
@retry_once
def stop_all_axes(self):
return [
self.socket_put_and_receive(f"S{ax.axis_Id_numeric}", raise_if_not_status=True)
for ax in self._axis
if ax is not None
]
@retry_once
@axis_checked
def axis_is_referenced(self, axis_Id_numeric: int) -> bool:
return_val = self.socket_put_and_receive(f"GPPK{axis_Id_numeric}")
if self._message_starts_with(return_val, f":PPK{axis_Id_numeric}"):
return bool(int(return_val.split(",")[1]))
def all_axes_referenced(self) -> bool:
return all(
self.axis_is_referenced(ax.axis_Id_numeric) for ax in self._axis if ax is not None
)
@retry_once
@axis_checked
def get_position(self, axis_Id_numeric: int) -> float:
"""Returns the current position of a positioner.
Args:
axis_Id_numeric (int): Axis number.
Returns:
float: Position in mm
"""
return_val = self.socket_put_and_receive(f"GP{axis_Id_numeric}")
if self._message_starts_with(return_val, f":P{axis_Id_numeric}"):
return float(return_val.split(",")[1]) / 1e6
@retry_once
@axis_checked
@typechecked
def move_axis_to_absolute_position(
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
) -> None:
"""Instructs a positioner to move to a specific position.
Args:
axis_Id_numeric (int): Axis number.
target_val (float): Target position in mm.
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
"""
self.socket_put_and_receive(
f"MPA{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
raise_if_not_status=True,
)
@retry_once
@axis_checked
@typechecked
def move_axis_to_relative_position(
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
) -> None:
"""Instructs a positioner to move to a position relative to its current position.
Args:
axis_Id_numeric (int): Axis number.
target_val (float): Relative position to move to in mm.
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
"""
self.socket_put_and_receive(
f"MPR{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
raise_if_not_status=True,
)
@retry_once
@axis_checked
@typechecked
def move_open_loop_steps(
self, axis_Id_numeric: int, steps: int, amplitude: int = 4000, frequency: int = 2000
) -> None:
"""Move open loop steps. It performs a burst of steps with the given parameters.
Args:
axis_Id_numeric (int): Axis number.
steps (int): Number and direction of steps to perform. The valid range is -30,000..30,000. A value of 0 stops the positioner, but see S command. A value of 30,000 or -30,000 performs an unbounded move. This should be used with caution since the positioner will only stop on an S command.
amplitude (int): Amplitude that the steps are performed with. Lower amplitude values result in a smaller step width. The parameter must be given as a 12bit value (range 0..4,095). 0 corresponds to 0V, 4,095 to 100V. Default: 4000
frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000.
"""
self.socket_put_and_receive(
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True
)
@retry_once
def get_communication_mode(self) -> SmaractCommunicationMode:
return_val = self.socket_put_and_receive("GCM")
if self._message_starts_with(return_val, f":CM"):
return SmaractCommunicationMode(int(return_val.strip(":CM")))
@retry_once
@axis_checked
def get_channel_type(self, axis_Id_numeric) -> str:
return_val = self.socket_put_and_receive(f"GCT{axis_Id_numeric}")
if self._message_starts_with(return_val, f":CT{axis_Id_numeric}"):
return return_val.split(",")[1]
@retry_once
def get_interface_version(self) -> str:
"""This command may be used to retrieve the interface version of the system. It is useful to check if changes
have been made to the software interface. An application may check the version in order to ensure that the
system behaves as the application expects it to do.
Returns:
str: interface version
"""
return_val = self.socket_put_and_receive("GIV")
if self._message_starts_with(return_val, f":IV"):
return return_val.strip(":IV")
@retry_once
def get_number_of_channels(self) -> int:
"""This command may be used to determine how many control channels are available on a system. This
includes positioner channels and end effector channels. Each channel is of a specific type. Use the GCT
command to determine the types of the channels.
Note that the number of channels does not represent the number positioners and/or end effectors that are
currently connected to the system.
The channel indexes throughout the interface are zero based. If your system has N channels then the valid
range for a channel index is 0.. N-1.
Returns:
int: number of channels
"""
return_val = self.socket_put_and_receive("GNC")
if self._message_starts_with(return_val, f":N"):
return int(return_val.strip(":N"))
@retry_once
def get_system_id(self) -> str:
"""This command may be used to physically identify a system connected to the PC. Each system has a unique
ID which makes it possible to distinguish one from another.
The ID returned is a generic decimal number that uniquely identifies the system.
"""
return_val = self.socket_put_and_receive("GSI")
if self._message_starts_with(return_val, f":ID"):
return return_val.strip(":ID")
@retry_once
def reset(self) -> None:
"""When this command is sent the system will perform a reset. It has the same effect as a power down/power
up cycle. The system replies with an acknowledge string before resetting itself.
"""
self.socket_put_and_receive("R", raise_if_not_status=True)
@retry_once
def set_hcm_mode(self, mode: int):
"""If a Hand Control Module (HCM) is connected to the system, this command may be used to enable or
disable it in order to avoid interference while the software is in control of the system. There are three possible
modes to set:
0: In this mode the Hand Control Module is disabled. It may not be used to control positioners.
1: This is the default setting where the Hand Control Module may be used to control the positioners.
2: In this mode the Hand Control Module cannot be used to control the positioners. However, if there
are positioners with sensors attached, their position data will still be displayed.
Args:
mode (int): HCM mode
"""
if mode not in range(3):
raise ValueError(f"HCM mode must be 0, 1 or 2. Received: {mode}.")
self.socket_put_and_receive(f"SHE{mode}", raise_if_not_status=True)
@retry_once
@axis_checked
def get_position_limits(self, axis_Id_numeric: int) -> list:
"""May be used to read out the travel range limit that is currently
configured for a linear channel.
Args:
axis_Id_numeric (int): Axis
Returns:
list: [low_limit, high_limit] in mm
"""
return_val = self.socket_put_and_receive(f"GPL{axis_Id_numeric}")
if self._message_starts_with(return_val, f":GPL{axis_Id_numeric}"):
return [
float(limit) / 1e6
for limit in return_val.strip(f":GPL{axis_Id_numeric},").split(",")
]
@retry_once
@axis_checked
def set_position_limits(
self, axis_Id_numeric: int, low_limit: float, high_limit: float
) -> None:
"""For positioners with integrated sensors this command may be used to limit the travel range of a linear
positioner by software. By default there is no limit set. If defined the
positioner will not move beyond the limit. This affects open-loop as well as closed-loop movements.
Args:
axis_Id_numeric (int): Axis
low_limit (float): low limit in mm
high_limit (float): high limit in mm
"""
self.socket_put_and_receive(
f"SPL{axis_Id_numeric},{np.round(low_limit*1e6)},{np.round(high_limit*1e6)}",
raise_if_not_status=True,
)
@retry_once
@axis_checked
def get_sensor_type(self, axis_Id_numeric: int) -> SmaractSensorDefinition:
return_val = self.socket_put_and_receive(f"GST{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},")))
@retry_once
@axis_checked
def find_reference_mark(
self, axis_Id_numeric: int, direction: int, holdTime: int, autoZero: int
) -> None:
return_val = self.socket_put_and_receive(
f"FRM{axis_Id_numeric},{direction},{holdTime},{autoZero}"
)
@retry_once
@axis_checked
def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None:
"""This command configures the speed control feature of a channel for closed-loop commands move_axis_to_absolute_position. By default the speed control is inactive. In this state the behavior of closed-loop commands is influenced by the maximum driving frequency. If a movement speed is configured, all following closed-loop commands will be executed with the new speed.
Args:
axis_Id_numeric (int): Axis number.
move_speed (float): Movement speed given in mm/s for linear positioners. The valid range is 0 .. 100. A value of 0 (default) deactivates the speed control feature.
"""
move_speed_in_nm_per_s = int(round(move_speed * 1e6))
if move_speed_in_nm_per_s > 100e6 or move_speed_in_nm_per_s < 0:
raise ValueError("Move speed must be within 0 to 100 mm/s.")
self.socket_put_and_receive(
f"SCLS{axis_Id_numeric},{move_speed_in_nm_per_s}", raise_if_not_status=True
)
@retry_once
@axis_checked
def get_closed_loop_move_speed(self, axis_Id_numeric: int) -> float:
"""Returns the currently configured movement speed that is used for closed-loop commands for a channel.
Args:
axis_Id_numeric (int): Axis number.
Returns:
float: move speed in mm/s. A return value of 0 means that the speed control feature is disabled.
"""
return_val = self.socket_put_and_receive(f"GCLS{axis_Id_numeric}")
if self._message_starts_with(return_val, f":CLS{axis_Id_numeric}"):
return float(return_val.strip(f":CLS{axis_Id_numeric},")) * 1e6
def describe(self) -> None:
t = PrettyTable()
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"]
for ax in range(self._axes_per_controller):
axis = self._axis[ax]
if axis is not None:
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
axis.name,
axis.connected,
self.axis_is_referenced(axis.axis_Id_numeric),
self.get_closed_loop_move_speed(axis.axis_Id_numeric),
axis.readback.read().get(axis.name).get("value"),
]
)
else:
t.add_row([None for t in t.field_names])
print(t)
@axis_checked
def _error_str(self, axis_Id_numeric: int, error_number: int):
return f":E{axis_Id_numeric},{error_number}"
def _get_error_code_from_msg(self, msg: str) -> int:
if msg.startswith(":E"):
return int(msg.split(",")[-1])
else:
return -1
def _get_axis_from_error_code(self, msg: str) -> int:
if msg.startswith(":E"):
try:
return int(msg.strip(":E").split(",")[0])
except ValueError:
return None
else:
return None
def _check_for_error(self, msg: str, axis_Id_numeric: int = None, raise_if_not_status=False):
if msg.startswith(":E"):
if axis_Id_numeric is None:
axis_Id_numeric = self._get_axis_from_error_code(msg)
if axis_Id_numeric is None:
raise SmaractCommunicationError(
"Could not retrieve axis number from error message."
)
if msg != self._error_str(axis_Id_numeric, 0):
error_code = self._get_error_code_from_msg(msg)
if error_code != 0:
raise SmaractErrorCode(error_code)
else:
if raise_if_not_status:
raise SmaractCommunicationError(
"Expected error / status message but failed to parse it."
)
def _remove_trailing_characters(self, var: str) -> str:
if len(var) > 1:
return var.split("\n")[0]
return var
def _message_starts_with(self, msg: str, leading_chars: str) -> bool:
if msg.startswith(leading_chars):
return True
raise SmaractCommunicationError(
f"Expected to receive a return message starting with {leading_chars} but instead"
f" received '{msg}'"
)

View File

@@ -1,47 +0,0 @@
SMARACT_ERRORS = {
1: "Syntax Error: The command could not be processed due to a syntactical error.",
2: "Invalid Command Error: The command given is not known to the system.",
3: "Overflow Error: This error occurs if a parameter given is too large and therefore cannot be processed.",
4: "Parse Error: The command could not be processed due to a parse error.",
5: "Too Few Parameters Error: The specified command requires more parameters in order to be executed.",
6: "Too Many Parameters Error: There were too many parameters given for the specified command.",
7: "Invalid Parameter Error: A parameter given exceeds the valid range. Please see the command description for valid ranges of the parameters.",
8: "Wrong Mode Error: This error is generated if the specified command is not available in the current communication mode. For example, the SRC command is not executable in synchronous mode.",
129: "No Sensor Present Error: This error occurs if a command was given that requires sensor feedback, but the addressed positioner has none attached.",
140: "Sensor Disabled Error: This error occurs if a command was given that requires sensor feedback, but the sensor of the addressed positioner is disabled (see SSE command).",
141: "Command Overridden Error: This error is only generated in the asynchronous communication mode. When the software commands a movement which is then interrupted by the Hand Control Module, an error of this type is generated.",
142: "End Stop Reached Error: This error is generated in asynchronous mode if the target position of a closed-loop command could not be reached, because a mechanical end stop was detected. After this error the positioner will have a movement status code of 0 (stopped).",
143: "Wrong Sensor Type Error: This error occurs if a closed-loop command does not match the sensor type that is currently configured for the addressed channel. For example, issuing a GP command while the targeted channel is configured as rotary will lead to this error.",
144: "Could Not Find Reference Mark Error: This error is generated in asynchronous mode (see SCM) if the search for a reference mark was aborted.",
145: "Wrong End Effector Type Error: This error occurs if a command does not match the end effector type that is currently configured for the addressed channel. For example, sending GF while the targeted channel is configured for a gripper will lead to this error.",
146: "Movement Locked Error: This error occurs if a movement command is issued while the system is in the locked state. ",
147: "Range Limit Reached Error: If a range limit is defined (SPL or SAL) and the positioner is about to move beyond this limit, then the positioner will stop and report this error (only in asynchronous mode, see SCM). After this error the positioner will have status code of 0 (stopped).",
148: "Physical Position Unknown Error: A range limit is only allowed to be defined if the positioner “knows” its physical position. If this is not the case, the commands SPL and SAL will return this error code.",
150: "Command Not Processable Error: This error is generated if a command is sent to a channel when it is in a state where the command cannot be processed. For example, to change the sensor type of a channel the addressed channel must be completely stopped. In this case send a stop command before changing the type.",
151: "Waiting For Trigger Error: If there is at least one command queued in the command queue then you may only append more commands (if the queue is not full), but you may not issue movement commands for immediate execution. Doing so will generate this error. See section 2.4.5 “Command Queues“.",
152: "Command Not Triggerable Error: After sending a ATC command you are required to issue a movement command that is to be triggered by the given event source. Commands that cannot be triggered will generate this error.",
153: "Command Queue Full Error: This error is generated if you attempt to append more commands to the command queue, but the queue cannot hold anymore commands. The queue capacity may be read out with a get channel property command (GCP on p.30).",
154: "Invalid Component Error: Indicates that a component (e.g. SCP) was selected that does not exist.",
155: "Invalid Sub Component Error: Indicates that a sub component (e.g. SCP) was selected that does not exist.",
156: "Invalid Property Error: Indicates that a property (e.g. SCP) was selected that does not exist.",
157: "Permission Denied Error: This error is generated when you call a functionality which is not unlocked for the system (e.g. Low Vibration Mode).",
}
class SmaractError(Exception):
pass
class SmaractCommunicationError(SmaractError):
pass
class SmaractErrorCode(SmaractError):
def __init__(self, error_code: int, message=""):
self.error_code = error_code
self.error_code_message = SMARACT_ERRORS.get(error_code, "UNKNOWN ERROR")
self.message = message
super().__init__(self.message)
def __str__(self):
return f"{self.error_code} / {self.error_code_message}. {self.message}"

View File

@@ -1,321 +0,0 @@
import logging
import threading
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError, ReadOnlyError
from ophyd_devices.utils.controller import threadlocked
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
from csaxs_bec.devices.smaract.smaract_controller import SmaractController
from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError
logger = bec_logger.logger
class SmaractSignalBase(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 SmaractSignalRO(SmaractSignalBase):
def __init__(self, signal_name, **kwargs):
super().__init__(signal_name, **kwargs)
self._metadata["write_access"] = False
@threadlocked
def _socket_set(self, val):
raise ReadOnlyError("Read-only signals cannot be set")
class SmaractReadbackSignal(SmaractSignalRO):
@threadlocked
def _socket_get(self):
return self.controller.get_position(self.parent.axis_Id_numeric) * self.parent.sign
class SmaractSetpointSignal(SmaractSignalBase):
setpoint = 0
@threadlocked
def _socket_get(self):
return self.setpoint
@threadlocked
def _socket_set(self, val):
target_val = val * self.parent.sign
self.setpoint = target_val
if self.controller.axis_is_referenced(self.parent.axis_Id_numeric):
self.controller.move_axis_to_absolute_position(self.parent.axis_Id_numeric, target_val)
# parameters are axis_no,pos_mm*1e6, hold_time_sec*1e3
else:
raise SmaractError(f"Axis {self.parent.axis_Id_numeric} is not referenced.")
class SmaractMotorIsMoving(SmaractSignalRO):
@threadlocked
def _socket_get(self):
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
class SmaractAxisReferenced(SmaractSignalRO):
@threadlocked
def _socket_get(self):
return self.parent.controller.axis_is_referenced(self.parent.axis_Id_numeric)
class SmaractAxisLimits(SmaractSignalBase):
@threadlocked
def _socket_get(self):
limits_msg = self.controller.socket_put_and_receive(f"GPL{self.parent.axis_Id_numeric}")
if limits_msg.startswith(":PL"):
limits = [
float(limit)
for limit in limits_msg.strip(f":PL{self.parent.axis_Id_numeric},").split(",")
]
else:
raise SmaractCommunicationError("Expected to receive message starting with :PL.")
return limits
# def _socket_set(self, val):
class SmaractMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
readback = Cpt(SmaractReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(SmaractSetpointSignal, signal_name="setpoint")
# motor_resolution = Cpt(
# SmaractMotorResolution, signal_name="resolution", kind="config"
# )
motor_is_moving = Cpt(SmaractMotorIsMoving, 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")
# all_axes_referenced = Cpt(
# SmaractAxesReferenced, signal_name="all_axes_referenced", kind="config"
# )
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=8085,
limits=None,
sign=1,
socket_cls=SocketIO,
**kwargs,
):
self.controller = SmaractController(
socket_cls=socket_cls, socket_host=host, socket_port=port
)
self.axis_Id = axis_Id
self.sign = sign
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
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()
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
@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", 4)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)
def move_and_finish():
while self.motor_is_moving.get():
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(0.1)
val = self.readback.read()
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
self._done_moving(success=success)
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)}")
@property
def egu(self):
"""The engineering units (EGU) for positions"""
return "mm"
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:
lsmarA = SmaractMotor("A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1)
lsmarB = SmaractMotor("B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1)
lsmarA.stage()
lsmarB.stage()
# status = leyey.move(2, wait=True)
# status = leyey.move(2, wait=True)
lsmarA.read()
lsmarB.read()
lsmarA.get()
lsmarB.get()
lsmarA.describe()
lsmarA.unstage()
lsmarA.controller.off()
# status = leyey.move(10, wait=False)
# print(lSmaract_controller)
else:
from ophyd_devices.utils.socket import SocketMock
lsmarA = SmaractMotor(
"A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
)
lsmarB = SmaractMotor(
"B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
)
lsmarA.stage()
lsmarB.stage()
lsmarA.read()
lsmarB.read()
lsmarA.get()
lsmarB.get()
lsmarA.describe()
lsmarA.unstage()
lsmarA.controller.off()
# status = leyey.move(10, wait=False)
# print(lSmaract_controller)

View File

@@ -1,303 +0,0 @@
[
{
"symbol": "S",
"type_code": 1,
"positioner_series": "SLCxxxxs",
"comment": "linear positioner with nano sensor",
"reference_type": "mark"
},
{
"symbol": "SR",
"type_code": 2,
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s, SR7012s, SR4513s, SR5018s",
"comment": "rotary positioner with nano sensor",
"reference_type": "mark"
},
{
"symbol": "SP",
"type_code": 5,
"positioner_series": "SLCxxxxrs",
"comment": "linear positioner with nano sensor, large actuator",
"reference_type": "mark"
},
{
"symbol": "SC",
"type_code": 6,
"positioner_series": "SLCxxxxsc",
"comment": "linear positioner with nano sensor, distance coded reference marks",
"reference_type": "mark*"
},
{
"symbol": "SR20",
"type_code": 8,
"positioner_series": "SR2013s, SR1612s",
"comment": "rotary positioner with nano sensor",
"reference_type": "mark"
},
{
"symbol": "M",
"type_code": 9,
"positioner_series": "SLCxxxxm",
"comment": "linear positioner with micro sensor",
"reference_type": "end stop"
},
{
"symbol": "GD",
"type_code": 11,
"positioner_series": "SGO60.5m",
"comment": "goniometer with micro sensor (60.5mm radius)",
"reference_type": "end stop"
},
{
"symbol": "GE",
"type_code": 12,
"positioner_series": "SGO77.5m",
"comment": "goniometer with micro sensor (77.5mm radius)",
"reference_type": "end stop"
},
{
"symbol": "GF",
"type_code": 14,
"positioner_series": "SR1209m",
"comment": "rotary positioner with micro sensor",
"reference_type": "end stop"
},
{
"symbol": "G605S",
"type_code": 16,
"positioner_series": "SGO60.5s",
"comment": "goniometer with nano sensor (60.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "G775S",
"type_code": 17,
"positioner_series": "SGO77.5s",
"comment": "goniometer with nano sensor (77.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "SC500",
"type_code": 18,
"positioner_series": "SLLxxsc",
"comment": "linear positioner with nano sensor, distance coded reference marks",
"reference_type": "mark*"
},
{
"symbol": "G955S",
"type_code": 19,
"positioner_series": "SGO95.5s",
"comment": "goniometer with nano sensor (95.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "SR77",
"type_code": 20,
"positioner_series": "SR77xxs",
"comment": "rotary positioner with nano sensor",
"reference_type": "mark"
},
{
"symbol": "SD",
"type_code": 21,
"positioner_series": "SLCxxxxds, SLLxxs",
"comment": "like S, but with extended scanning Range",
"reference_type": "mark"
},
{
"symbol": "R20ME",
"type_code": 22,
"positioner_series": "SR2013sx, SR1410sx",
"comment": "rotary positioner with MicroE sensor",
"reference_type": "mark"
},
{
"symbol": "SR2",
"type_code": 23,
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s",
"comment": "like SR, for high applied masses",
"reference_type": "mark"
},
{
"symbol": "SCD",
"type_code": 24,
"positioner_series": "SLCxxxxdsc",
"comment": "like SP, but with distance coded reference marks",
"reference_type": "mark*"
},
{
"symbol": "SRC",
"type_code": 25,
"positioner_series": "SR7021sc",
"comment": "like SR, but with distance coded reference marks",
"reference_type": "mark*"
},
{
"symbol": "SR36M",
"type_code": 26,
"positioner_series": "SR3610m",
"comment": "rotary positioner, no end stops",
"reference_type": "none"
},
{
"symbol": "SR36ME",
"type_code": 27,
"positioner_series": "SR3610m",
"comment": "rotary positioner with end stops",
"reference_type": "end stop"
},
{
"symbol": "SR50M",
"type_code": 28,
"positioner_series": "SR5018m",
"comment": "rotary positioner, no end stops",
"reference_type": "none"
},
{
"symbol": "SR50ME",
"type_code": 29,
"positioner_series": "SR5018m",
"comment": "rotary positioner with end stops",
"reference_type": "end stop"
},
{
"symbol": "G1045S",
"type_code": 30,
"positioner_series": "SGO104.5s",
"comment": "goniometer with nano sensor (104.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "G1395S",
"type_code": 31,
"positioner_series": "SGO139.5s",
"comment": "goniometer with nano sensor (139.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "MD",
"type_code": 32,
"positioner_series": "SLCxxxxdme",
"comment": "like M, but with large actuator",
"reference_type": "end stop"
},
{
"symbol": "G935M",
"type_code": 33,
"positioner_series": "SGO93.5me",
"comment": "goniometer with micro sensor (93.5mm radius)",
"reference_type": "end stop"
},
{
"symbol": "SHL20",
"type_code": 34,
"positioner_series": "SHL-20",
"comment": "high load vertical positioner",
"reference_type": "mark"
},
{
"symbol": "SCT",
"type_code": 35,
"positioner_series": "SLCxxxxscu",
"comment": "like SCD, but with even larger actuator",
"reference_type": "mark*"
},
{
"symbol": "SR77T",
"type_code": 36,
"positioner_series": "SR7021s",
"comment": "like SR77, but with larger actuator",
"reference_type": "mark"
},
{
"symbol": "SR120",
"type_code": 37,
"positioner_series": "SR120xxs",
"comment": "large rotary positioner",
"reference_type": "mark"
},
{
"symbol": "LC",
"type_code": 38,
"positioner_series": "SLCxxxxl",
"comment": "linear positioner with improved micro sensor",
"reference_type": "mark*"
},
{
"symbol": "LR",
"type_code": 39,
"positioner_series": "SRxxxxl",
"comment": "rotary positioner with improved micro sensor",
"reference_type": "mark"
},
{
"symbol": "LCD",
"type_code": 40,
"positioner_series": "SLCxxxxdl",
"comment": "like LC, but with large actuator",
"reference_type": "mark*"
},
{
"symbol": "L",
"type_code": 41,
"positioner_series": "SLCxxxxl",
"comment": "linear positioner with improved micro sensor",
"reference_type": "mark"
},
{
"symbol": "LD",
"type_code": 42,
"positioner_series": "SLCxxxxdl",
"comment": "like L, but with large actuator",
"reference_type": "mark"
},
{
"symbol": "LE",
"type_code": 43,
"positioner_series": "SLCxxxxl",
"comment": "Linear positioner with improved micro sensor",
"reference_type": "end stop"
},
{
"symbol": "LED",
"type_code": 44,
"positioner_series": "SLCxxxxdl",
"comment": "Like L, but with large actuator",
"reference_type": "end stop"
},
{
"symbol": "GDD",
"type_code": 45,
"positioner_series": "SGO60.5md",
"comment": "goniometer with micro sensor (60.5mm radius), large actuator",
"reference_type": "end stop"
},
{
"symbol": "GED",
"type_code": 46,
"positioner_series": "SGO77.5md",
"comment": "goniometer with micro sensor (77.5mm radius), large actuator",
"reference_type": "end stop"
},
{
"symbol": "G935S",
"type_code": 47,
"positioner_series": "SGO96.5s",
"comment": "goniometer with nano sensor (93.5mm radius)",
"reference_type": "mark"
},
{
"symbol": "G605DS",
"type_code": 48,
"positioner_series": "SGO60.5ds",
"comment": "goniometer with nano sensor (60.5mm radius), large actuator",
"reference_type": "mark"
},
{
"symbol": "G775DS",
"type_code": 49,
"positioner_series": "SGO77.5ds",
"comment": "goniometer with nano sensor (77.5mm radius), large actuator",
"reference_type": "mark"
}
]

Some files were not shown because too many files have changed in this diff Show More