Compare commits

..

188 Commits

Author SHA1 Message Date
7d63be7e04 refactor(ids): move to PSIDeviceBase and preview signal 2025-06-16 16:40:47 +02:00
63ee870c89 fix(test-x-ray-eye): update DeviceBase inits with proper config as needed due to PR #506 in BEC 2025-06-11 16:26:52 +02:00
36a2e55474 fix: update upstream repo for plugin template 2025-06-11 16:04:58 +02:00
19d117619a refactor: replace deprecated consumer/producer with connector 2025-06-04 19:38:30 +02:00
447a9c3924 refactor: upgrade to copier version v1 2025-05-24 11:37:51 +02:00
Holler Mirko
17e525689c initial version of omny alignment gui 2025-04-23 13:04:13 +02:00
4907c5db27 fix: add get_config template to pre_startup 2025-01-22 09:49:45 +01:00
0dfcedc47f build: add jjf_client to dependencies 2025-01-15 08:20:59 +01:00
8f2335396b test: fix mock due to refactoring in ophyd devices 2025-01-14 17:53:32 +01:00
8a7a005b56 feat: add JungFrauJoch client draf and test 2025-01-14 16:46:58 +01:00
e19ce733a2 refactor: initial commit for updates on ddg from csaxs 2025-01-14 16:46:58 +01:00
gac-x12sa
ccaf996512 refactor: refactored ddg csaxs with new base class in ophyd devices 2025-01-14 16:46:58 +01:00
Holler Mirko
83de8b75db added show all methods 2025-01-09 14:22:23 +01:00
Holler Mirko
829bdc11c1 adjusted to new gui manager 2025-01-09 14:20:29 +01:00
Holler Mirko
cbb3f9e8bc added example table for temperatures 2025-01-08 13:22:19 +01:00
Holler Mirko
5f819d9835 further fix omny doc 2025-01-08 11:51:02 +01:00
Holler Mirko
8f55b6dbcb Fixes in omny documentation plus added show all for all relevant omny components 2025-01-08 11:46:49 +01:00
b2095f8d7b Merge branch 'main' into 'lamnidoc'
# Conflicts:
#   docs/user/ptychography/omny.md
2024-12-16 17:03:19 +01:00
Mirko Holler
9186853faf lamni doc 2024-12-16 16:50:53 +01:00
4d241d26f3 Update omny.md values of interferometer SSI 2024-12-13 13:19:19 +01:00
f47ca6e174 Update lamni.md 2024-12-02 13:56:07 +01:00
e86e6ba63c Update ptychography.md 2024-12-02 13:56:07 +01:00
7ff2f7dc1b Update omny.md 2024-12-02 13:56:07 +01:00
ef1ab749a9 Update flomni.md 2024-12-02 13:56:07 +01:00
Mirko Holler
ec53bb7000 fixes in flomni doc and completion of first version of omny doc 2024-12-02 13:44:18 +01:00
Mirko Holler
5f1449365d some mods omny doc 2024-11-29 17:28:28 +01:00
Mirko Holler
9699b39a7e continued with omny documentation 2024-11-29 13:20:39 +01:00
4fa81ddc5c fix: fixes related to BEC v3 2024-11-18 13:51:12 +01:00
Mirko Holler
79f63b84fc completed initial version of flomni docs 2024-11-18 13:14:21 +01:00
Holler Mirko
5f9f2c8762 started editing flomni doc 2024-11-15 16:36:44 +01:00
Holler Mirko
58960944f8 init version 2024-11-15 15:37:41 +01:00
Holler Mirko
dfb4fa75d9 initial version 2024-11-15 15:37:04 +01:00
70b196403b docs: cleanup to support rtd autosummary 2024-11-14 17:43:05 +01:00
f14a5f220e docs: cleanup for rtd 2024-11-14 16:37:23 +01:00
a4077be379 build: added bec_widgets as dependency 2024-11-14 16:37:23 +01:00
cf96658625 docs: fixed install path 2024-11-14 16:27:55 +01:00
c21d6cf3c6 docs: install repo for rtd 2024-11-14 16:23:33 +01:00
4f22de66f6 docs: fixed config path 2024-11-14 16:16:32 +01:00
dadee3efc1 docs: fixed install for rtd 2024-11-14 16:10:17 +01:00
Holler Mirko
19ac208f73 config fix ocsx ocsy 2024-11-12 15:42:35 +01:00
a327d0ccaa refactor(gui_tools): cleanup of unused imports 2024-11-12 15:36:17 +01:00
Holler Mirko
9107b1f14b fixed camera init
minor adjustments progress gui
2024-11-12 15:35:45 +01:00
Holler Mirko
bb8bf3a072 req key args 2024-11-12 15:34:40 +01:00
9c331c13e0 refactor(gui_tools): minor cleanup 2024-11-12 15:29:48 +01:00
Holler Mirko
8a1951766b fixes 2024-11-07 12:09:14 +01:00
Holler Mirko
a70c4588a0 added textbox to gui 2024-11-07 11:40:49 +01:00
Holler Mirko
55aa7128e2 work on omny gui, initial status progress bar 2024-11-06 16:56:53 +01:00
b31a1ea996 docs: fixed api reference 2024-11-05 22:22:52 +01:00
820b673144 docs: fixed landing page 2024-11-05 22:15:08 +01:00
805d4b5a13 docs: add readthedocs configuration 2024-11-05 22:08:49 +01:00
ci_update_bot
011955ca38 docs: Update device list 2024-11-05 14:12:11 +00:00
04f123ab2b fix(ids_camera): safe-guarded the import of ids libs 2024-11-05 15:06:35 +01:00
372205957b refactor: formatting 2024-11-05 15:06:35 +01:00
Holler Mirko
41d574253e added cameras plus some fixes 2024-11-05 15:06:35 +01:00
Holler Mirko
c9536d9380 various fixes 2024-11-05 15:06:35 +01:00
831ace2533 fix: added missing pyueye lib 2024-11-05 15:06:35 +01:00
Holler Mirko
20a4f4d0bc fixes. ready for first merge. 2024-11-05 15:06:35 +01:00
Holler Mirko
3ae62c8ff9 fix 2024-11-05 15:06:35 +01:00
Holler Mirko
40c460d120 first scanning via tomo_scan_projection, added mirror corrections 2024-11-05 15:06:35 +01:00
Holler Mirko
0d66d2d364 added tracker check 2024-11-05 15:06:35 +01:00
Holler Mirko
2e3ed9f6f0 cleanup and move mirror parameters to server and transfer to client 2024-11-05 15:06:35 +01:00
Holler Mirko
fff0b8ce41 moved auto alignment methods from client to rt server 2024-11-05 15:06:35 +01:00
Holler Mirko
d7308a8b81 omny fermat scan 2024-11-05 15:06:35 +01:00
Holler Mirko
86e71ca255 first adjustments towards scanning 2024-11-05 15:06:35 +01:00
Holler Mirko
d3c86b07a0 introduce 25 deg offset in osamroy to have 0 to 360
initial work on omny class
2024-11-05 15:06:35 +01:00
Holler Mirko
af2a2df55f rt movement possible. added slew rate limiter querry to rt communication 2024-11-05 15:06:35 +01:00
Holler Mirko
d2e4a8c703 axis not stopping 2024-11-05 15:06:35 +01:00
Holler Mirko
a8cc2bcbe0 interferometer feedback running 2024-11-05 15:06:35 +01:00
Holler Mirko
cb16dcfb4c multiple cameras and fix cam id 2024-11-05 15:06:35 +01:00
Holler Mirko
4282d4ead6 first version ids 2024-11-05 15:06:35 +01:00
Holler Mirko
a07a56822f continued with auto alignment and laser tracker 2024-11-05 15:06:35 +01:00
Holler Mirko
07543755d9 first commissioning of interferometer auto alignment 2024-11-05 15:06:35 +01:00
Holler Mirko
793eedb3f4 continued with mirror tweaking. started with laser tracker. implemented tweak curser function. 2024-11-05 15:06:35 +01:00
Holler Mirko
dbae426a87 started with implementation of RT omny 2024-11-05 15:06:35 +01:00
Holler Mirko
9e84c1570c omny vacuum control system device 2024-11-05 15:06:35 +01:00
Holler Mirko
2002f2fabf added device for dewar monitoring 2024-11-05 15:06:35 +01:00
Holler Mirko
fb49295a92 fixes temperature device 2024-11-05 15:06:35 +01:00
Holler Mirko
85ecf8b55f added show all cryo 2024-11-05 15:06:35 +01:00
Holler Mirko
a1eacdf54b continued devel temperature device 2024-11-05 15:06:35 +01:00
Holler Mirko
c9a1fd1cd6 initial version of temperature device 2024-11-05 15:06:35 +01:00
Holler Mirko
54b9a5004a completed sample transfer commissioning 2024-11-05 15:06:35 +01:00
Holler Mirko
972d010954 gripper move to and from sample stage commissioned 2024-11-05 15:06:35 +01:00
Holler Mirko
b1f4207738 sample transfer put and get at shuttle positions commissioned 2024-11-05 15:06:35 +01:00
Holler Mirko
45dfc1305e further sample transfer. first gripper movements 2024-11-05 15:06:35 +01:00
Holler Mirko
5f90b32210 started commissioning of sample transfer. shuttle align, open and close 2024-11-05 15:06:35 +01:00
Holler Mirko
29a8e1b281 optics movements 2024-11-05 15:06:35 +01:00
Holler Mirko
78b838f516 first init of omny stages. adding color printing, moving methods to tools and further auto init 2024-11-05 15:06:35 +01:00
Holler Mirko
17d4fcb8db messages to client 2024-11-05 15:06:35 +01:00
Holler Mirko
e03b3b84e0 started commissioning with hardware. first movements. added smaract to config 2024-11-05 15:06:35 +01:00
Holler Mirko
dd2f20fba4 interferometer tuning added to rt client 2024-11-05 15:06:35 +01:00
Holler Mirko
d62f398351 started tweak cursor for laser mirrors 2024-11-05 15:06:35 +01:00
Holler Mirko
c17a836858 continued rt ophyd. requires testing now. rt client will contain interferometer mirror tweaking 2024-11-05 15:06:35 +01:00
Holler Mirko
f359e32610 continue rt and laser 2024-11-05 15:06:35 +01:00
Holler Mirko
b28649e6af started with rt 2024-11-05 15:06:35 +01:00
Holler Mirko
a0f2269da3 started with rt 2024-11-05 15:06:35 +01:00
Holler Mirko
143c4dbd65 first test of transfer functions (setting and getting samples, without hw, just epics level) 2024-11-05 15:06:35 +01:00
Holler Mirko
347c63d500 fixes after first load in bec 2024-11-05 15:06:35 +01:00
Holler Mirko
f0df487131 added new files for alignment, tools and laser 2024-11-05 15:06:35 +01:00
Holler Mirko
90449d22ee initial draft of sample transfer completed 2024-11-05 15:06:35 +01:00
Holler Mirko
3a06ca484b continue with sample transfer 2024-11-05 15:06:35 +01:00
Holler Mirko
8ba32172ca continued sample transfer 2024-11-05 15:06:35 +01:00
Holler Mirko
9652545440 continue sample transfer 2024-11-05 15:06:35 +01:00
Holler Mirko
55f7015834 continued with sample transfer 2024-11-05 15:06:35 +01:00
Holler Mirko
afd5d5548f continued with optics alignment and started with sample transfer 2024-11-05 15:06:35 +01:00
Holler Mirko
cc04810361 continued 2024-11-05 15:06:35 +01:00
Holler Mirko
fb6ec1c350 continue omny init and started optics alignment 2024-11-05 15:06:35 +01:00
Holler Mirko
d32d7401a3 continue with init 2024-11-05 15:06:35 +01:00
Holler Mirko
c7f9d37c12 continue init script of omny, various mods in galil 2024-11-05 15:06:35 +01:00
Holler Mirko
f3d2100b03 starting omny client 2024-11-05 15:06:35 +01:00
Holler Mirko
79d69c9492 omny galil 2024-11-05 15:06:35 +01:00
Holler Mirko
e4a2d01488 omny galil 2024-11-05 15:06:35 +01:00
5648b4dbd6 fix: added find_reference and folerr public methods 2024-11-05 15:06:35 +01:00
Holler Mirko
977a2beb4c omny galil 2024-11-05 15:06:35 +01:00
Holler Mirko
8f9c43ad99 omny galil 2024-11-05 15:06:35 +01:00
Holler Mirko
8bb89cbf76 omny galil 2024-11-05 15:06:35 +01:00
Holler Mirko
294d3b0d4e omny galil first commit 2024-11-05 15:06:35 +01:00
87f002a68b added license 2024-11-04 21:13:02 +01:00
ci_update_bot
b5a5082919 docs: Update device list 2024-10-03 13:42:26 +00:00
3a40b4a37e refactor(npoint): minor improvements to the npoint config template 2024-10-02 18:31:53 +02:00
baafa982e3 refactor(npoint): cleanup 2024-10-02 18:27:02 +02:00
8c2d705a89 docs: improved npoint test docs 2024-10-02 18:21:11 +02:00
59db1c067b test: fixed npoint tests after axis refactoring 2024-10-02 18:11:51 +02:00
gac-x12sa
15bdbe2e03 feat: added npoint support to ophyd 2024-10-02 11:50:39 +02:00
0d2b4c4423 refactor(npoint): cleanup 2024-09-18 21:49:49 +02:00
59e0755e14 test: fixed test for unique rids 2024-06-25 10:06:26 +02:00
ci_update_bot
2e6d1ad343 docs: Update device list 2024-06-23 09:04:52 +00:00
fdfb6db84b refactor: publish file location with successful and done 2024-06-06 15:43:32 +02:00
4e4ca325ab fix: use on_complete instead of on_unstage and adapt changes for base class 2024-06-04 09:02:06 +02:00
ci_update_bot
319771e2aa docs: Update device list 2024-05-27 10:48:14 +00:00
26b8ac997d refactor: cleanup imports, tests and classes 2024-05-27 08:49:53 +02:00
3f21090441 refactor: moved patch_dual_pvs to devices.tests_utils 2024-05-27 08:34:11 +02:00
34fdc39ebc refactor: moved patch pvs to conftest.py 2024-05-27 08:34:11 +02:00
f3d7f1ba64 feat: adapt detector classes and test for psi_detector_base refactoring 2024-05-27 08:34:11 +02:00
ci_update_bot
fad8b516a6 docs: Update device list 2024-05-23 12:28:05 +00:00
a9ff092a66 fix: commented outdated epics devices 2024-05-23 14:26:05 +02:00
37d80e9a7b fix: added backward compatible fix 2024-05-22 11:52:32 +02:00
ed335dc308 fix: renamed move_and_wait 2024-05-22 11:46:02 +02:00
78d2dd2436 fix: fixed scan_progress after renaming in bec 2024-05-21 12:51:05 +02:00
ci_update_bot
bf2e6baba5 docs: Update device list 2024-05-17 15:45:17 +00:00
fcf9ee4545 refactor: restructured device module 2024-05-17 17:40:26 +02:00
ci_update_bot
53a200928f docs: Update device list 2024-05-16 17:57:58 +00:00
ci_update_bot
1572dd4484 docs: Update device list 2024-05-16 17:55:57 +00:00
ci_update_bot
f72be7ab70 docs: Update device list 2024-05-16 17:53:55 +00:00
ci_update_bot
8aee3e3c0d docs: Update device list 2024-05-16 17:51:51 +00:00
ci_update_bot
6a517d3ac5 docs: Update device list 2024-05-16 17:49:48 +00:00
ci_update_bot
b6b2850853 docs: Update device list 2024-05-16 17:47:44 +00:00
ci_update_bot
5841a56255 docs: Update device list 2024-05-16 17:45:47 +00:00
ci_update_bot
333b4f1dd2 docs: Update device list 2024-05-16 17:43:45 +00:00
ci_update_bot
97e09cf622 docs: Update device list 2024-05-16 17:41:44 +00:00
ci_update_bot
174c0689bc docs: Update device list 2024-05-16 15:39:19 +00:00
ci_update_bot
a3899bf7a5 docs: Update device list 2024-05-16 15:35:03 +00:00
ci_update_bot
d4c12a3c9c docs: Update device list 2024-05-16 15:33:00 +00:00
ci_update_bot
8f0ed4e250 docs: Update device list 2024-05-16 15:29:28 +00:00
ci_update_bot
0359f1f431 docs: Update device list 2024-05-16 15:27:23 +00:00
ci_update_bot
b45070332d docs: Update device list 2024-05-16 15:25:21 +00:00
ci_update_bot
35e4ea0916 docs: Update device list 2024-05-16 15:23:14 +00:00
ae86fbd329 fix: fixed lgalil init 2024-05-16 16:15:07 +02:00
a195be64e7 test: support for repeated runs 2024-05-16 15:38:41 +02:00
e980bf2ee6 fix: fixed import 2024-05-16 15:07:53 +02:00
ce876f58d6 fix: fixed config after refactoring 2024-05-16 15:07:53 +02:00
Holler Mirko
b0a1c32b47 conig_files 2024-05-16 15:07:53 +02:00
11ed6e112f test: ensure to reset the controller instances 2024-05-16 15:07:53 +02:00
8a24692e82 fix: fixed import after refactoring 2024-05-16 15:07:53 +02:00
1d266c5da9 refactor: added galil base class and moved lamni-specific galil components to subclass 2024-05-16 15:07:53 +02:00
7d93154761 refactor: renamed rt_lamni module to rt 2024-05-16 15:07:53 +02:00
c85c6ec5ab refactor: cleanup for rt 2024-05-16 15:07:53 +02:00
Holler Mirko
d567e49b53 fixes at beamline 2024-05-16 15:07:53 +02:00
73ce61dd96 refactor: merged controller methods 2024-05-16 15:07:53 +02:00
8410b3ceec fix: disabled detector_table_theta class 2024-05-16 13:19:37 +02:00
477567e61a fix: fixed bec_lib imports 2024-05-15 18:41:17 +02:00
7da1bddef8 fix: apply black and isort 2024-05-15 14:47:48 +02:00
5007b182ca fix: change imports according to latest bec_lib 2024-05-15 14:47:48 +02:00
063308042d ci: added child pipeline branch 2024-05-13 16:10:21 +02:00
0180e4a3f9 fix: update to new ophyd_devices structure 2024-05-08 17:37:56 +02:00
fc37892ea1 fix: fixed import for new ophyd_devices repo structure 2024-05-08 16:59:04 +02:00
Holler Mirko
d6f88b599a improve show all 2024-05-07 15:24:55 +02:00
Holler Mirko
ab2270683f feature_lamni_init 2024-05-07 15:21:06 +02:00
Holler Mirko
7b6652f867 feature_lamni_init 2024-05-07 15:21:06 +02:00
Holler Mirko
cb525e8778 feature: initialization of lamni stages 2024-05-07 15:21:06 +02:00
21c3e3cab4 Merge branch 'wakonig_k-main-patch-ef88' into 'main'
moved gitlab ci to awi-utils

See merge request bec/csaxs_bec!16
2024-05-07 09:30:43 +02:00
adfdb6fe5d ci: moved to awi utils template 2024-05-07 09:27:55 +02:00
73d9a967d6 Merge branch 'test/async_readback_fix' into 'main'
test: fixed async readback dummy data

See merge request bec/csaxs_bec!17
2024-05-07 09:25:40 +02:00
787e0afcec test: fixed async readback dummy data 2024-05-07 09:23:04 +02:00
74d941c249 Merge branch 'backlashcommandfix' into 'main'
backlashcommandfix

See merge request bec/csaxs_bec!13
2024-04-26 10:32:17 +02:00
f601593c77 test(galil): fixed wrong backlash assert 2024-04-26 10:29:57 +02:00
Holler Mirko
e0d93cd84e backlashcommandfix 2024-04-26 10:00:44 +02:00
803dffb9cd Merge branch 'fix' into 'main'
fix

See merge request bec/csaxs_bec!11
2024-04-25 23:32:09 +02:00
Holler Mirko
3cbafa51be fix 2024-04-25 15:40:53 +02:00
154 changed files with 17090 additions and 4404 deletions

9
.copier-answers.yml Normal file
View File

@@ -0,0 +1,9 @@
# Do not edit this file!
# It is needed to track the repo template version, and editing may break things.
# This file will be overwritten by copier on template updates.
_commit: v1.0.0
_src_path: https://github.com/bec-project/plugin_copier_template.git
make_commit: false
project_name: csaxs_bec
widget_plugins_input: []

View File

@@ -1,105 +1,20 @@
# 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: Jobs/Secret-Detection.gitlab-ci.yml
- project: bec/awi_utils
file: /templates/plugin-repo-template.yml
inputs:
name: "csaxs"
target: "csaxs_bec"
branch: $CHILD_PIPELINE_BRANCH
#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
pages:
stage: Deploy
needs: []
allow_failure: true
before_script:
- pip install pylint pylint-exit anybadge
- apt-get update
- apt-get install -y bc
variables:
TARGET_BRANCH: $CI_COMMIT_REF_NAME
rules:
- if: "$CI_COMMIT_TAG != null"
variables:
TARGET_BRANCH: $CI_COMMIT_TAG
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/csaxs_bec"'
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
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/sls-csaxs/270162/

29
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,29 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
tools:
python: "3.10"
jobs:
pre_install:
- pip install .
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
# - pdf
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2025, Paul Scherrer Institute
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
bin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
# Add anything you don't want to check in to git, e.g. very large files

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
import os
import subprocess
from bec_lib import MessageEndpoints, RedisConnector, bec_logger, messages
from bec_lib.redis_connector import MessageObject
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import MessageObject, RedisConnector
logger = bec_logger.logger
@@ -12,7 +13,6 @@ logger = bec_logger.logger
class PilatusConverter:
def __init__(self, host: str, port: int) -> None:
self._connector = RedisConnector(f"{host}:{port}")
self._producer = self._connector.producer()
def start(self) -> None:
"""start the consumer"""
@@ -27,7 +27,7 @@ class PilatusConverter:
message (MessageObject): Message object
parent (PilatusConverter): Parent object
"""
msg = messages.MessageReader.loads(message.value)
msg = message.value
print(msg)
if not msg:
return
@@ -56,10 +56,9 @@ class PilatusConverter:
"""
Start the consumer.
"""
file_consumer = self._connector.consumer(
self._connector.register(
MessageEndpoints.file_event("pilatus_2"), cb=self.on_new_message, parent=self
)
file_consumer.start()
if __name__ == "__main__":

View File

@@ -13,6 +13,166 @@ umv = builtins.__dict__.get("umv")
bec = builtins.__dict__.get("bec")
class LamNIInitError(Exception):
pass
class LaMNIInitStagesMixin:
def lamni_init_stages(self):
user_input = input("Starting initialization of LamNI stages. OK? [y/n]")
if user_input == "y":
print("staring...")
dev.lsamrot.enabled = True
else:
return
if self.check_all_axes_of_lamni_referenced():
user_input = input("Continue anyways? [y/n]")
if user_input == "y":
print("ok then...")
else:
return
axis_id_lsamrot = dev.lsamrot._config["deviceConfig"].get("axis_Id")
if dev.lsamrot.controller.get_motor_limit_switch(axis_id_lsamrot)[1] == False:
user_input = input("The rotation stage will be moved to one limit [y/n]")
if user_input == "y":
print("starting...")
else:
return
self.drive_axis_to_limit(dev.lsamrot, "forward")
dev.lsamrot.enabled = False
print("Now hard reboot the controller and run the initialization routine again.")
print("The controller will be disabled in bec. To enable dev.lsamrot.enabled=True")
return
user_input = input(
"Init of loptz. Can the stage move to the upstream limit without collision?? [y/n]"
)
if user_input == "y":
print("ok then...")
else:
return
print("Referencing loptz")
self.drive_axis_to_limit(dev.loptz, "forward")
self.find_reference_mark(dev.loptz)
print("Referencing loptx")
self.drive_axis_to_limit(dev.loptx, "reverse")
self.find_reference_mark(dev.loptx)
print("Referencing lopty")
self.drive_axis_to_limit(dev.lopty, "forward")
self.find_reference_mark(dev.lopty)
print("Referencing lsamx")
self.drive_axis_to_limit(dev.lsamx, "forward")
self.find_reference_mark(dev.lsamx)
print("Referencing lsamy")
self.drive_axis_to_limit(dev.lsamy, "reverse")
self.find_reference_mark(dev.lsamy)
# the dual encoder requires the reference mark to pass on both encoders
print("Referencing lsamrot")
self.drive_axis_to_limit(dev.lsamrot, "reverse")
time.sleep(0.1)
self.find_reference_mark(dev.lsamrot)
user_input = input("Init of leye. Can the stage move to -x limit without collision? [y/n]")
if user_input == "y":
print("starting...")
else:
return
print("Referencing leyex")
self.drive_axis_to_limit(dev.leyex, "forward")
print("Referencing leyey")
self.drive_axis_to_limit(dev.leyey, "forward")
# set_lm lsamx 6 14
# set_lm lsamy 6 14
# set_lm lsamrot -3 362
# set_lm loptx -1 -0.2
# set_lm lopty 3.0 3.6
# set_lm loptz 82 87
# set_lm leyex 0 25
# set_lm leyey 0.5 50
print("Init of Smaract stages")
dev.losax.controller.find_reference_mark(2, 0, 1000, 1)
time.sleep(1)
dev.losax.controller.find_reference_mark(0, 0, 1000, 1)
time.sleep(1)
dev.losax.controller.find_reference_mark(1, 0, 1000, 1)
time.sleep(1)
# dev.losax.controller.find_reference_mark(3, 1, 1000, 1)
# time.sleep(1)
# dev.losax.controller.find_reference_mark(4, 1, 1000, 1)
# time.sleep(1)
# set_lm losax -1.5 0.25
# set_lm losay -2.5 4.1
# set_lm losaz -4.1 -0.5
# set_lm lcsy -1.5 5
self._align_setup()
def find_reference_mark(self, device):
axis_id = device._config["deviceConfig"].get("axis_Id")
axis_id_numeric = self.axis_id_to_numeric(axis_id)
device.controller.find_reference(axis_id_numeric)
def drive_axis_to_limit(self, device, direction):
axis_id = device._config["deviceConfig"].get("axis_Id")
axis_id_numeric = self.axis_id_to_numeric(axis_id)
device.controller.drive_axis_to_limit(axis_id_numeric, direction)
def axis_id_to_numeric(self, axis_id) -> int:
return ord(axis_id.lower()) - 97
def _align_setup(self):
user_input = input("Start moving stages to default initial positions? [y/n]")
if user_input == "y":
print("Start moving stages...")
else:
print("Stopping.")
return
lsamx_center = dev.lsamx.user_parameter.get("center")
if lsamx_center is None:
raise LamNIInitError(
"Could not find a lsamx center position. Please check your device config."
)
lsamy_center = dev.lsamy.user_parameter.get("center")
if lsamy_center is None:
raise LamNIInitError(
"Could not find a lsamy center position. Please check your device config."
)
umv(dev.lsamx, lsamx_center, dev.lsamy, lsamy_center, dev.loptx, -0.3, dev.lopty, 0)
umv(dev.losax, -1)
umv(dev.loptz, 82.25)
umv(dev.lsamrot, -1)
umv(dev.lsamrot, 0)
time.sleep(2)
dev.rtx.controller.feedback_disable_and_even_reset_lamni_angle_interferometer()
def check_all_axes_of_lamni_referenced(self):
if (
dev.losax.controller.axis_is_referenced(0)
& dev.losax.controller.axis_is_referenced(1)
& dev.losax.controller.axis_is_referenced(2)
& dev.lsamx.controller.all_axes_referenced()
):
print("All axes of LamNI are referenced.")
return True
else:
return False
class LamNIOpticsMixin:
@staticmethod
def _get_user_param_safe(device, var):
@@ -120,14 +280,22 @@ class LamNIOpticsMixin:
umv(dev.losaz, losaz_out)
umv(dev.losay, losay_out)
def lfzp_info(self):
def lfzp_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
loptz_val = dev.loptz.read()["loptz"]["value"]
distance = -loptz_val + 85.6 + 52
print(f"The sample is in a distance of {distance:.1f} mm from the FZP.")
diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6]
mokev_val = dev.mokev.read()["mokev"]["value"]
console = Console()
table = Table(
title=f"At the current energy of {mokev_val:.4f} keV we have following options:",

View File

@@ -16,7 +16,7 @@ from typeguard import typechecked
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
from .lamni_optics_mixin import LamNIOpticsMixin
from .lamni_optics_mixin import LaMNIInitStagesMixin, LamNIOpticsMixin
logger = bec_logger.logger
bec = builtins.__dict__.get("bec")
@@ -32,7 +32,7 @@ class XrayEyeAlign:
self.lamni = lamni
self.device_manager = client.device_manager
self.scans = client.scans
self.xeye = self.device_manager.devices.xeye
# self.xeye = self.device_manager.devices.xeye
self.alignment_values = defaultdict(list)
self._reset_init_values()
self.corr_pos_x = []
@@ -511,7 +511,7 @@ class LamNI(LamNIOpticsMixin):
super().__init__()
self.client = client
self.align = XrayEyeAlign(client, self)
self.init = LaMNIInitStagesMixin()
self.check_shutter = True
self.check_light_available = True
self.check_fofb = True
@@ -586,6 +586,27 @@ class LamNI(LamNIOpticsMixin):
def tomo_circfov(self, val: float):
self.client.set_global_var("tomo_circfov", val)
@property
def tomo_type(self):
val = self.client.get_global_var("tomo_type")
if val is None:
return 1
return val
@tomo_type.setter
def tomo_type(self, val: float):
if val == 1:
# equally spaced tomography with 8 sub tomograms
self.client.set_global_var("tomo_type", val)
# elif val == 2:
# # golden ratio tomography (sorted bunches)
# self.client.set_global_var("tomo_type", val)
# elif val == 3:
# # equally spaced tomography with starting angles shifted by golden ratio
# self.client.set_global_var("tomo_type", val)
else:
raise ValueError("Unknown tomo_type.")
@property
def tomo_countingtime(self):
val = self.client.get_global_var("tomo_countingtime")
@@ -948,25 +969,63 @@ class LamNI(LamNIOpticsMixin):
# _tomo_shift_angles (potential global variable)
_tomo_shift_angles = 0
angle_end = start_angle + 360
for angle in np.linspace(
angles = np.linspace(
start_angle + _tomo_shift_angles,
angle_end,
num=int(360 / self.tomo_angle_stepsize) + 1,
endpoint=True,
):
successful = False
error_caught = False
if 0 <= angle < 360.05:
print(f"Starting LamNI scan for angle {angle}")
while not successful:
self._start_beam_check()
if not self.special_angles:
self._current_special_angles = []
if self._current_special_angles:
next_special_angle = self._current_special_angles[0]
if np.isclose(angle, next_special_angle, atol=0.5):
self._current_special_angles.pop(0)
num_repeats = self.special_angle_repeats
)
# reverse even sub-tomograms
if not (subtomo_number % 2):
angles = np.flip(angles)
for angle in angles:
self.progress["subtomo"] = subtomo_number
self.progress["subtomo_projection"] = angles.index(angle)
self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize
self.progress["projection"] = (subtomo_number - 1) * self.progress[
"subtomo_total_projections"
] + self.progress["subtomo_projection"]
self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8
self.progress["angle"] = angle
self._tomo_scan_at_angle(angle, subtomo_number)
def _print_progress(self):
print("\x1b[95mProgress report:")
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
print(f"Projection: ...................... {self.progress['projection']}")
print(f"Total projections expected ....... {self.progress['total_projections']}")
print(f"Angle: ........................... {self.progress['angle']}")
print(f"Current subtomo: ................. {self.progress['subtomo']}")
print(f"Current projection within subtomo: {self.progress['subtomo_projection']}\x1b[0m")
def _tomo_scan_at_angle(self, angle, subtomo_number):
successful = False
error_caught = False
if 0 <= angle < 360.05:
print(f"Starting LamNI scan for angle {angle} in subtomo {subtomo_number}")
self._print_progress()
while not successful:
self._start_beam_check()
if not self.special_angles:
self._current_special_angles = []
if self._current_special_angles:
next_special_angle = self._current_special_angles[0]
if np.isclose(angle, next_special_angle, atol=0.5):
self._current_special_angles.pop(0)
num_repeats = self.special_angle_repeats
else:
num_repeats = 1
try:
start_scan_number = bec.queue.next_scan_number
for i in range(num_repeats):
self._at_each_angle(angle)
error_caught = False
except AlarmBase as exc:
if exc.alarm_type == "TimeoutError":
bec.queue.request_queue_reset()
time.sleep(2)
error_caught = True
else:
num_repeats = 1
try:
@@ -1006,7 +1065,7 @@ class LamNI(LamNIOpticsMixin):
scans = builtins.__dict__.get("scans")
self._current_special_angles = self.special_angles.copy()
if subtomo_start == 1 and start_angle is None:
if self.tomo_type == 1 and subtomo_start == 1 and start_angle is None:
# pylint: disable=undefined-variable
self.tomo_id = self.add_sample_database(
self.sample_name,

View File

@@ -0,0 +1,351 @@
corr_elements = 175
corr_angle[0] = 0.100000
corr_angle[1] = 1.001000
corr_angle[2] = 1.902000
corr_angle[3] = 2.798000
corr_angle[4] = 7.305000
corr_angle[5] = 8.204000
corr_angle[6] = 9.104000
corr_angle[7] = 10.005000
corr_angle[8] = 14.504000
corr_angle[9] = 15.404000
corr_angle[10] = 16.304000
corr_angle[11] = 17.204000
corr_angle[12] = 21.704000
corr_angle[13] = 22.604000
corr_angle[14] = 23.504000
corr_angle[15] = 24.404000
corr_angle[16] = 28.904000
corr_angle[17] = 29.804000
corr_angle[18] = 30.704000
corr_angle[19] = 31.604000
corr_angle[20] = 36.104000
corr_angle[21] = 37.004000
corr_angle[22] = 37.904000
corr_angle[23] = 38.804000
corr_angle[24] = 43.305000
corr_angle[25] = 44.205000
corr_angle[26] = 45.104000
corr_angle[27] = 46.005000
corr_angle[28] = 50.504000
corr_angle[29] = 51.404000
corr_angle[30] = 52.305000
corr_angle[31] = 53.205000
corr_angle[32] = 57.705000
corr_angle[33] = 58.605000
corr_angle[34] = 59.505000
corr_angle[35] = 60.405000
corr_angle[36] = 64.904000
corr_angle[37] = 65.804000
corr_angle[38] = 66.704000
corr_angle[39] = 67.604000
corr_angle[40] = 72.104000
corr_angle[41] = 73.004000
corr_angle[42] = 73.904000
corr_angle[43] = 74.804000
corr_angle[44] = 79.304000
corr_angle[45] = 80.204000
corr_angle[46] = 81.104000
corr_angle[47] = 82.005000
corr_angle[48] = 86.505000
corr_angle[49] = 87.404000
corr_angle[50] = 88.304000
corr_angle[51] = 89.205000
corr_angle[52] = 93.704000
corr_angle[53] = 94.604000
corr_angle[54] = 95.505000
corr_angle[55] = 96.404000
corr_angle[56] = 100.904000
corr_angle[57] = 101.804000
corr_angle[58] = 102.704000
corr_angle[59] = 103.604000
corr_angle[60] = 108.104000
corr_angle[61] = 109.004000
corr_angle[62] = 109.904000
corr_angle[63] = 110.804000
corr_angle[64] = 115.304000
corr_angle[65] = 116.204000
corr_angle[66] = 117.104000
corr_angle[67] = 118.004000
corr_angle[68] = 122.504000
corr_angle[69] = 123.404000
corr_angle[70] = 124.304000
corr_angle[71] = 125.204000
corr_angle[72] = 129.704000
corr_angle[73] = 130.604000
corr_angle[74] = 131.504000
corr_angle[75] = 132.404000
corr_angle[76] = 136.904000
corr_angle[77] = 137.804000
corr_angle[78] = 138.701000
corr_angle[79] = 139.604000
corr_angle[80] = 144.104000
corr_angle[81] = 145.004000
corr_angle[82] = 145.904000
corr_angle[83] = 146.804000
corr_angle[84] = 151.304000
corr_angle[85] = 152.204000
corr_angle[86] = 153.104000
corr_angle[87] = 154.004000
corr_angle[88] = 158.504000
corr_angle[89] = 159.404000
corr_angle[90] = 160.304000
corr_angle[91] = 161.204000
corr_angle[92] = 165.704000
corr_angle[93] = 166.604000
corr_angle[94] = 167.504000
corr_angle[95] = 168.404000
corr_angle[96] = 172.904000
corr_angle[97] = 173.805000
corr_angle[98] = 174.704000
corr_angle[99] = 180.104000
corr_angle[100] = 183.704000
corr_angle[101] = 184.603000
corr_angle[102] = 185.504000
corr_angle[103] = 190.904000
corr_angle[104] = 191.805000
corr_angle[105] = 192.704000
corr_angle[106] = 198.104000
corr_angle[107] = 199.004000
corr_angle[108] = 199.904000
corr_angle[109] = 205.304000
corr_angle[110] = 206.204000
corr_angle[111] = 207.104000
corr_angle[112] = 212.504000
corr_angle[113] = 213.404000
corr_angle[114] = 214.304000
corr_angle[115] = 219.704000
corr_angle[116] = 220.604000
corr_angle[117] = 221.504000
corr_angle[118] = 226.904000
corr_angle[119] = 227.804000
corr_angle[120] = 228.704000
corr_angle[121] = 234.104000
corr_angle[122] = 235.004000
corr_angle[123] = 235.904000
corr_angle[124] = 241.304000
corr_angle[125] = 242.204000
corr_angle[126] = 243.104000
corr_angle[127] = 248.504000
corr_angle[128] = 249.404000
corr_angle[129] = 250.304000
corr_angle[130] = 255.704000
corr_angle[131] = 256.604000
corr_angle[132] = 257.504000
corr_angle[133] = 262.904000
corr_angle[134] = 263.804000
corr_angle[135] = 264.704000
corr_angle[136] = 270.104000
corr_angle[137] = 271.004000
corr_angle[138] = 271.904000
corr_angle[139] = 277.304000
corr_angle[140] = 278.205000
corr_angle[141] = 279.104000
corr_angle[142] = 284.504000
corr_angle[143] = 285.405000
corr_angle[144] = 286.304000
corr_angle[145] = 291.703000
corr_angle[146] = 292.604000
corr_angle[147] = 293.504000
corr_angle[148] = 298.904000
corr_angle[149] = 299.804000
corr_angle[150] = 300.704000
corr_angle[151] = 306.104000
corr_angle[152] = 307.004000
corr_angle[153] = 307.904000
corr_angle[154] = 313.304000
corr_angle[155] = 314.204000
corr_angle[156] = 315.104000
corr_angle[157] = 320.504000
corr_angle[158] = 321.404000
corr_angle[159] = 322.304000
corr_angle[160] = 327.704000
corr_angle[161] = 328.605000
corr_angle[162] = 329.504000
corr_angle[163] = 334.904000
corr_angle[164] = 335.804000
corr_angle[165] = 336.705000
corr_angle[166] = 342.104000
corr_angle[167] = 343.004000
corr_angle[168] = 343.904000
corr_angle[169] = 349.304000
corr_angle[170] = 350.204000
corr_angle[171] = 351.104000
corr_angle[172] = 356.504000
corr_angle[173] = 357.404000
corr_angle[174] = 358.304000
corr_pos[0] = 0.012330
corr_pos[1] = 0.024870
corr_pos[2] = 0.037262
corr_pos[3] = 0.049438
corr_pos[4] = 0.108462
corr_pos[5] = 0.119791
corr_pos[6] = 0.130986
corr_pos[7] = 0.142044
corr_pos[8] = 0.195045
corr_pos[9] = 0.205834
corr_pos[10] = 0.216589
corr_pos[11] = 0.226402
corr_pos[12] = 0.271931
corr_pos[13] = 0.281684
corr_pos[14] = 0.290769
corr_pos[15] = 0.299392
corr_pos[16] = 0.337986
corr_pos[17] = 0.345178
corr_pos[18] = 0.352253
corr_pos[19] = 0.358528
corr_pos[20] = 0.379520
corr_pos[21] = 0.382947
corr_pos[22] = 0.386110
corr_pos[23] = 0.388887
corr_pos[24] = 0.395645
corr_pos[25] = 0.396154
corr_pos[26] = 0.396679
corr_pos[27] = 0.396367
corr_pos[28] = 0.392400
corr_pos[29] = 0.392530
corr_pos[30] = 0.391826
corr_pos[31] = 0.391325
corr_pos[32] = 0.379188
corr_pos[33] = 0.376449
corr_pos[34] = 0.373195
corr_pos[35] = 0.369588
corr_pos[36] = 0.343021
corr_pos[37] = 0.336754
corr_pos[38] = 0.330310
corr_pos[39] = 0.323447
corr_pos[40] = 0.281171
corr_pos[41] = 0.272735
corr_pos[42] = 0.263888
corr_pos[43] = 0.254469
corr_pos[44] = 0.203024
corr_pos[45] = 0.192980
corr_pos[46] = 0.182359
corr_pos[47] = 0.170780
corr_pos[48] = 0.111580
corr_pos[49] = 0.101222
corr_pos[50] = 0.090051
corr_pos[51] = 0.077918
corr_pos[52] = 0.015225
corr_pos[53] = 0.003876
corr_pos[54] = -0.007756
corr_pos[55] = -0.020383
corr_pos[56] = -0.084910
corr_pos[57] = -0.096848
corr_pos[58] = -0.108732
corr_pos[59] = -0.121432
corr_pos[60] = -0.184349
corr_pos[61] = -0.195904
corr_pos[62] = -0.207113
corr_pos[63] = -0.219319
corr_pos[64] = -0.274799
corr_pos[65] = -0.284942
corr_pos[66] = -0.294809
corr_pos[67] = -0.305437
corr_pos[68] = -0.352114
corr_pos[69] = -0.359689
corr_pos[70] = -0.367451
corr_pos[71] = -0.375629
corr_pos[72] = -0.404347
corr_pos[73] = -0.407683
corr_pos[74] = -0.410857
corr_pos[75] = -0.414904
corr_pos[76] = -0.424625
corr_pos[77] = -0.424357
corr_pos[78] = -0.424411
corr_pos[79] = -0.424615
corr_pos[80] = -0.421392
corr_pos[81] = -0.419977
corr_pos[82] = -0.417860
corr_pos[83] = -0.416400
corr_pos[84] = -0.398108
corr_pos[85] = -0.392613
corr_pos[86] = -0.386455
corr_pos[87] = -0.380338
corr_pos[88] = -0.341288
corr_pos[89] = -0.332298
corr_pos[90] = -0.322767
corr_pos[91] = -0.311630
corr_pos[92] = -0.256798
corr_pos[93] = -0.244808
corr_pos[94] = -0.232932
corr_pos[95] = -0.219785
corr_pos[96] = -0.158685
corr_pos[97] = -0.146198
corr_pos[98] = -0.130743
corr_pos[99] = -0.054066
corr_pos[100] = -0.001498
corr_pos[101] = 0.012010
corr_pos[102] = 0.025195
corr_pos[103] = 0.094982
corr_pos[104] = 0.109235
corr_pos[105] = 0.120813
corr_pos[106] = 0.179893
corr_pos[107] = 0.192147
corr_pos[108] = 0.201902
corr_pos[109] = 0.245092
corr_pos[110] = 0.250501
corr_pos[111] = 0.255536
corr_pos[112] = 0.280598
corr_pos[113] = 0.280673
corr_pos[114] = 0.282529
corr_pos[115] = 0.293286
corr_pos[116] = 0.292713
corr_pos[117] = 0.291123
corr_pos[118] = 0.288721
corr_pos[119] = 0.288260
corr_pos[120] = 0.286480
corr_pos[121] = 0.271630
corr_pos[122] = 0.268004
corr_pos[123] = 0.265418
corr_pos[124] = 0.239187
corr_pos[125] = 0.233224
corr_pos[126] = 0.226652
corr_pos[127] = 0.189034
corr_pos[128] = 0.180302
corr_pos[129] = 0.170931
corr_pos[130] = 0.125760
corr_pos[131] = 0.116433
corr_pos[132] = 0.106845
corr_pos[133] = 0.057551
corr_pos[134] = 0.048531
corr_pos[135] = 0.038276
corr_pos[136] = -0.012292
corr_pos[137] = -0.021223
corr_pos[138] = -0.030046
corr_pos[139] = -0.077647
corr_pos[140] = -0.085863
corr_pos[141] = -0.093816
corr_pos[142] = -0.138532
corr_pos[143] = -0.145584
corr_pos[144] = -0.152699
corr_pos[145] = -0.192594
corr_pos[146] = -0.200980
corr_pos[147] = -0.208816
corr_pos[148] = -0.241983
corr_pos[149] = -0.248319
corr_pos[150] = -0.253789
corr_pos[151] = -0.276869
corr_pos[152] = -0.279240
corr_pos[153] = -0.281538
corr_pos[154] = -0.294317
corr_pos[155] = -0.295586
corr_pos[156] = -0.296029
corr_pos[157] = -0.297443
corr_pos[158] = -0.297185
corr_pos[159] = -0.295946
corr_pos[160] = -0.284463
corr_pos[161] = -0.282598
corr_pos[162] = -0.281260
corr_pos[163] = -0.256509
corr_pos[164] = -0.250326
corr_pos[165] = -0.244372
corr_pos[166] = -0.204043
corr_pos[167] = -0.196147
corr_pos[168] = -0.187928
corr_pos[169] = -0.131831
corr_pos[170] = -0.121351
corr_pos[171] = -0.110548
corr_pos[172] = -0.038951
corr_pos[173] = -0.025887
corr_pos[174] = -0.012501

View File

@@ -309,6 +309,7 @@ class FlomniInitStagesMixin:
else:
print("Stopping.")
return
dev.rtx.controller.feedback_disable()
# positions for optics out and 50 mm distance to sample
umv(dev.ftrackz, 4.73, dev.ftracky, 2.5170, dev.foptx, -14.3, dev.fopty, 3.87)
@@ -423,18 +424,18 @@ class FlomniSampleTransferMixin:
def show_signal_strength_interferometer(self):
dev.rtx.controller.show_signal_strength_interferometer()
def rt_feedback_disable(self):
def feedback_disable(self):
self.device_manager.devices.rtx.controller.feedback_disable()
def rt_feedback_enable_with_reset(self):
def feedback_enable_with_reset(self):
self.device_manager.devices.rtx.controller.feedback_enable_with_reset()
self.rt_feedback_status()
def rt_feedback_enable_without_reset(self):
def feedback_enable_without_reset(self):
self.device_manager.devices.rtx.controller.feedback_enable_without_reset()
self.rt_feedback_status()
def rt_feedback_status(self):
def feedback_status(self):
feedback_status = self.device_manager.devices.rtx.controller.feedback_is_running()
if feedback_status == True:
print("The rt feedback is \x1b[92mrunning\x1b[0m.")
@@ -454,7 +455,7 @@ class FlomniSampleTransferMixin:
umv(dev.fsamroy, 0)
self.rt_feedback_disable()
self.feedback_disable()
self.ensure_fheater_up()
@@ -1050,7 +1051,7 @@ class FlomniAlignmentMixin:
value = line.split(" ")[2]
name = line.split(" ")[0].split("[")[0]
if name == "corr_pos":
corr_pos.append(float(value) / 1000)
corr_pos.append(float(value))
elif name == "corr_angle":
corr_angle.append(float(value))
print(
@@ -1141,7 +1142,7 @@ class Flomni(
fsamx_in = self._get_user_param_safe(dev.fsamx, "in")
if np.isclose(fsamx_in, dev.fsamx.readback.get(), 0.5):
print("Stopping alignment. Returning to fsamx in position.")
self.rt_feedback_disable()
self.feedback_disable()
umv(dev.fsamx, fsamx_in)
raise exc
@@ -1520,7 +1521,7 @@ class Flomni(
angles = np.flip(angles)
for angle in angles:
self.progress["subtomo"] = subtomo_number
self.progress["subtomo_projection"] = angles.index(angle)
self.progress["subtomo_projection"] = np.where(angles == angle)[0][0]
self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize
self.progress["projection"] = (subtomo_number - 1) * self.progress[
"subtomo_total_projections"
@@ -1700,7 +1701,7 @@ class Flomni(
def _print_progress(self):
print("\x1b[95mProgress report:")
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
print(f"Projection: ...................... {self.progress['projection']}")
print(f"Projection: ...................... {self.progress['projection']:.0f}")
print(f"Total projections expected ....... {self.progress['total_projections']}")
print(f"Angle: ........................... {self.progress['angle']}")
print(f"Current subtomo: ................. {self.progress['subtomo']}")

View File

@@ -100,7 +100,7 @@ class XrayEyeAlign:
self.flomni.laser_tracker_on()
self.flomni.rt_feedback_enable_with_reset()
self.flomni.feedback_enable_with_reset()
# disable movement buttons
self.movement_buttons_enabled = False
@@ -109,7 +109,7 @@ class XrayEyeAlign:
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
# this makes sure we are in a defined state
self.flomni.rt_feedback_disable()
self.flomni.feedback_disable()
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
@@ -143,13 +143,13 @@ class XrayEyeAlign:
self.movement_buttons_enabled = False
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
self.flomni.rt_feedback_disable()
self.flomni.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()
self.flomni.feedback_disable()
umv(dev.fsamx, fsamx_in - 0.25)
self.update_frame()
@@ -160,7 +160,7 @@ class XrayEyeAlign:
umv(dev.fsamx, fsamx_in)
time.sleep(0.5)
self.flomni.rt_feedback_enable_with_reset()
self.flomni.feedback_enable_with_reset()
self.update_frame()
self.send_message("Adjust sample height and submit center")
@@ -205,11 +205,11 @@ class XrayEyeAlign:
# allow movements, store movements to calculate center
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
if _xrayeyalignmvy != 0:
self.flomni.rt_feedback_disable()
self.flomni.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.flomni.feedback_enable_with_reset()
self.update_frame()
time.sleep(0.2)

View File

@@ -0,0 +1 @@
from .omny import OMNY

View File

@@ -0,0 +1,715 @@
corr_elements = 357
corr_angle[0] = 0.097400
corr_angle[1] = 0.603500
corr_angle[2] = 1.134200
corr_angle[3] = 1.625000
corr_angle[4] = 2.162200
corr_angle[5] = 2.700100
corr_angle[6] = 3.191600
corr_angle[7] = 3.714300
corr_angle[8] = 4.223200
corr_angle[9] = 4.730900
corr_angle[10] = 5.253300
corr_angle[11] = 5.743300
corr_angle[12] = 6.279200
corr_angle[13] = 6.782900
corr_angle[14] = 7.301200
corr_angle[15] = 7.808100
corr_angle[16] = 8.325300
corr_angle[17] = 8.859400
corr_angle[18] = 9.359400
corr_angle[19] = 9.887900
corr_angle[20] = 10.395400
corr_angle[21] = 10.930700
corr_angle[22] = 11.415400
corr_angle[23] = 11.928900
corr_angle[24] = 12.456900
corr_angle[25] = 12.955900
corr_angle[26] = 13.479000
corr_angle[27] = 13.982700
corr_angle[28] = 14.500900
corr_angle[29] = 15.016200
corr_angle[30] = 15.528000
corr_angle[31] = 16.053800
corr_angle[32] = 16.562800
corr_angle[33] = 17.076600
corr_angle[34] = 17.592400
corr_angle[35] = 18.094100
corr_angle[36] = 18.623800
corr_angle[37] = 19.118400
corr_angle[38] = 19.655000
corr_angle[39] = 20.143900
corr_angle[40] = 20.672200
corr_angle[41] = 21.171500
corr_angle[42] = 21.696300
corr_angle[43] = 22.215900
corr_angle[44] = 22.728900
corr_angle[45] = 23.233100
corr_angle[46] = 23.760000
corr_angle[47] = 24.267700
corr_angle[48] = 24.792100
corr_angle[49] = 25.287900
corr_angle[50] = 25.824600
corr_angle[51] = 26.321900
corr_angle[52] = 26.842600
corr_angle[53] = 27.337200
corr_angle[54] = 27.873500
corr_angle[55] = 28.373900
corr_angle[56] = 28.895900
corr_angle[57] = 29.404900
corr_angle[58] = 29.926700
corr_angle[59] = 30.439300
corr_angle[60] = 30.963700
corr_angle[61] = 31.460000
corr_angle[62] = 31.989500
corr_angle[63] = 32.489700
corr_angle[64] = 33.026400
corr_angle[65] = 33.516500
corr_angle[66] = 34.044000
corr_angle[67] = 34.547700
corr_angle[68] = 35.070800
corr_angle[69] = 35.578400
corr_angle[70] = 36.103200
corr_angle[71] = 36.603200
corr_angle[72] = 37.128200
corr_angle[73] = 37.645200
corr_angle[74] = 38.164300
corr_angle[75] = 38.662100
corr_angle[76] = 39.191000
corr_angle[77] = 39.693400
corr_angle[78] = 40.222500
corr_angle[79] = 40.719500
corr_angle[80] = 41.247000
corr_angle[81] = 41.755700
corr_angle[82] = 42.271300
corr_angle[83] = 42.767600
corr_angle[84] = 43.302200
corr_angle[85] = 43.818700
corr_angle[86] = 44.330000
corr_angle[87] = 44.835300
corr_angle[88] = 45.359800
corr_angle[89] = 45.867200
corr_angle[90] = 46.396200
corr_angle[91] = 46.895000
corr_angle[92] = 47.411500
corr_angle[93] = 47.915200
corr_angle[94] = 48.436600
corr_angle[95] = 48.946100
corr_angle[96] = 49.472300
corr_angle[97] = 49.979800
corr_angle[98] = 50.503300
corr_angle[99] = 51.008700
corr_angle[100] = 51.535000
corr_angle[101] = 52.036700
corr_angle[102] = 52.563700
corr_angle[103] = 53.063600
corr_angle[104] = 53.591700
corr_angle[105] = 54.091200
corr_angle[106] = 54.618900
corr_angle[107] = 55.116900
corr_angle[108] = 55.636100
corr_angle[109] = 56.143100
corr_angle[110] = 56.672200
corr_angle[111] = 57.172900
corr_angle[112] = 57.704600
corr_angle[113] = 58.204800
corr_angle[114] = 58.728600
corr_angle[115] = 59.239500
corr_angle[116] = 59.768400
corr_angle[117] = 60.268300
corr_angle[118] = 60.788800
corr_angle[119] = 61.289300
corr_angle[120] = 61.813300
corr_angle[121] = 62.310800
corr_angle[122] = 62.836700
corr_angle[123] = 63.353700
corr_angle[124] = 63.866600
corr_angle[125] = 64.377200
corr_angle[126] = 64.906600
corr_angle[127] = 65.414000
corr_angle[128] = 65.937100
corr_angle[129] = 66.429400
corr_angle[130] = 66.970000
corr_angle[131] = 67.459200
corr_angle[132] = 67.996400
corr_angle[133] = 68.499800
corr_angle[134] = 69.014500
corr_angle[135] = 69.509500
corr_angle[136] = 70.044000
corr_angle[137] = 70.543200
corr_angle[138] = 71.079400
corr_angle[139] = 71.579300
corr_angle[140] = 72.103500
corr_angle[141] = 72.607000
corr_angle[142] = 73.137100
corr_angle[143] = 73.633000
corr_angle[144] = 74.164500
corr_angle[145] = 74.660200
corr_angle[146] = 75.180600
corr_angle[147] = 75.674200
corr_angle[148] = 76.215400
corr_angle[149] = 76.718900
corr_angle[150] = 77.242300
corr_angle[151] = 77.752000
corr_angle[152] = 78.279300
corr_angle[153] = 78.780500
corr_angle[154] = 79.314900
corr_angle[155] = 79.424500
corr_angle[156] = 79.807000
corr_angle[157] = 80.336500
corr_angle[158] = 80.338700
corr_angle[159] = 80.835300
corr_angle[160] = 81.367100
corr_angle[161] = 81.376100
corr_angle[162] = 81.859000
corr_angle[163] = 82.382300
corr_angle[164] = 82.384000
corr_angle[165] = 82.881100
corr_angle[166] = 83.415000
corr_angle[167] = 83.421600
corr_angle[168] = 83.917100
corr_angle[169] = 84.439200
corr_angle[170] = 84.439300
corr_angle[171] = 84.947400
corr_angle[172] = 85.472700
corr_angle[173] = 85.987700
corr_angle[174] = 86.512400
corr_angle[175] = 87.009500
corr_angle[176] = 87.536500
corr_angle[177] = 88.035600
corr_angle[178] = 88.560100
corr_angle[179] = 89.057600
corr_angle[180] = 89.583200
corr_angle[181] = 90.090500
corr_angle[182] = 90.614600
corr_angle[183] = 91.119900
corr_angle[184] = 91.638300
corr_angle[185] = 92.153300
corr_angle[186] = 92.681600
corr_angle[187] = 93.181800
corr_angle[188] = 93.710900
corr_angle[189] = 94.206100
corr_angle[190] = 94.732700
corr_angle[191] = 95.226600
corr_angle[192] = 95.766400
corr_angle[193] = 96.259000
corr_angle[194] = 96.783400
corr_angle[195] = 97.283100
corr_angle[196] = 97.811500
corr_angle[197] = 98.323600
corr_angle[198] = 98.839400
corr_angle[199] = 99.350700
corr_angle[200] = 99.880200
corr_angle[201] = 100.378100
corr_angle[202] = 100.913300
corr_angle[203] = 101.405000
corr_angle[204] = 101.935700
corr_angle[205] = 102.426300
corr_angle[206] = 102.957300
corr_angle[207] = 103.456500
corr_angle[208] = 103.985100
corr_angle[209] = 104.490300
corr_angle[210] = 105.015100
corr_angle[211] = 105.518400
corr_angle[212] = 106.047400
corr_angle[213] = 106.551100
corr_angle[214] = 107.077100
corr_angle[215] = 107.575800
corr_angle[216] = 108.115200
corr_angle[217] = 108.598800
corr_angle[218] = 109.129100
corr_angle[219] = 109.626200
corr_angle[220] = 110.158400
corr_angle[221] = 110.661700
corr_angle[222] = 111.188600
corr_angle[223] = 111.695000
corr_angle[224] = 112.216400
corr_angle[225] = 112.720000
corr_angle[226] = 113.246200
corr_angle[227] = 113.757100
corr_angle[228] = 114.281900
corr_angle[229] = 114.780000
corr_angle[230] = 115.306800
corr_angle[231] = 115.799800
corr_angle[232] = 116.328400
corr_angle[233] = 116.829900
corr_angle[234] = 117.355400
corr_angle[235] = 117.858200
corr_angle[236] = 118.380600
corr_angle[237] = 118.893200
corr_angle[238] = 119.418200
corr_angle[239] = 119.921000
corr_angle[240] = 120.446100
corr_angle[241] = 120.953800
corr_angle[242] = 121.480300
corr_angle[243] = 121.977600
corr_angle[244] = 122.507100
corr_angle[245] = 122.996200
corr_angle[246] = 123.529600
corr_angle[247] = 124.032200
corr_angle[248] = 124.554800
corr_angle[249] = 125.057800
corr_angle[250] = 125.580800
corr_angle[251] = 126.090000
corr_angle[252] = 126.611600
corr_angle[253] = 127.127000
corr_angle[254] = 127.650900
corr_angle[255] = 128.153700
corr_angle[256] = 128.677200
corr_angle[257] = 129.170800
corr_angle[258] = 129.703000
corr_angle[259] = 130.194900
corr_angle[260] = 130.733600
corr_angle[261] = 131.228300
corr_angle[262] = 131.756900
corr_angle[263] = 132.263200
corr_angle[264] = 132.784700
corr_angle[265] = 133.296300
corr_angle[266] = 133.818800
corr_angle[267] = 134.318900
corr_angle[268] = 134.846800
corr_angle[269] = 135.344800
corr_angle[270] = 135.876000
corr_angle[271] = 136.372600
corr_angle[272] = 136.902700
corr_angle[273] = 137.397800
corr_angle[274] = 137.926800
corr_angle[275] = 138.428000
corr_angle[276] = 138.953700
corr_angle[277] = 139.461300
corr_angle[278] = 139.987800
corr_angle[279] = 140.492600
corr_angle[280] = 141.017900
corr_angle[281] = 141.524000
corr_angle[282] = 142.061200
corr_angle[283] = 142.545400
corr_angle[284] = 143.069900
corr_angle[285] = 143.568700
corr_angle[286] = 144.102200
corr_angle[287] = 144.596200
corr_angle[288] = 145.126400
corr_angle[289] = 145.626900
corr_angle[290] = 146.158700
corr_angle[291] = 146.662200
corr_angle[292] = 147.184700
corr_angle[293] = 147.692300
corr_angle[294] = 148.225900
corr_angle[295] = 148.716000
corr_angle[296] = 149.243400
corr_angle[297] = 149.739400
corr_angle[298] = 150.272400
corr_angle[299] = 150.762100
corr_angle[300] = 151.301800
corr_angle[301] = 151.796300
corr_angle[302] = 152.333500
corr_angle[303] = 152.833100
corr_angle[304] = 153.363600
corr_angle[305] = 153.862900
corr_angle[306] = 154.396100
corr_angle[307] = 154.895400
corr_angle[308] = 155.417700
corr_angle[309] = 155.916900
corr_angle[310] = 156.451600
corr_angle[311] = 156.942700
corr_angle[312] = 157.467600
corr_angle[313] = 157.970900
corr_angle[314] = 158.501100
corr_angle[315] = 158.999600
corr_angle[316] = 159.526100
corr_angle[317] = 160.028100
corr_angle[318] = 160.555800
corr_angle[319] = 161.067700
corr_angle[320] = 161.597300
corr_angle[321] = 162.091300
corr_angle[322] = 162.616600
corr_angle[323] = 163.107400
corr_angle[324] = 163.639100
corr_angle[325] = 164.139700
corr_angle[326] = 164.672500
corr_angle[327] = 165.176800
corr_angle[328] = 165.698600
corr_angle[329] = 166.205400
corr_angle[330] = 166.727300
corr_angle[331] = 167.238600
corr_angle[332] = 167.771500
corr_angle[333] = 168.265000
corr_angle[334] = 168.794800
corr_angle[335] = 169.293800
corr_angle[336] = 169.813300
corr_angle[337] = 170.320200
corr_angle[338] = 170.845100
corr_angle[339] = 171.344000
corr_angle[340] = 171.866200
corr_angle[341] = 172.375300
corr_angle[342] = 172.899400
corr_angle[343] = 173.404400
corr_angle[344] = 173.928500
corr_angle[345] = 174.430800
corr_angle[346] = 174.827800
corr_angle[347] = 175.460400
corr_angle[348] = 175.993100
corr_angle[349] = 176.492100
corr_angle[350] = 177.014200
corr_angle[351] = 177.512100
corr_angle[352] = 178.044200
corr_angle[353] = 178.643800
corr_angle[354] = 179.067200
corr_angle[355] = 179.571600
corr_angle[356] = 180.093700
corr_pos[0] = -0.814519
corr_pos[1] = -0.810675
corr_pos[2] = -0.806338
corr_pos[3] = -0.802047
corr_pos[4] = -0.797044
corr_pos[5] = -0.791712
corr_pos[6] = -0.786558
corr_pos[7] = -0.780781
corr_pos[8] = -0.774864
corr_pos[9] = -0.768674
corr_pos[10] = -0.762005
corr_pos[11] = -0.755474
corr_pos[12] = -0.748024
corr_pos[13] = -0.740730
corr_pos[14] = -0.732929
corr_pos[15] = -0.725011
corr_pos[16] = -0.716637
corr_pos[17] = -0.707676
corr_pos[18] = -0.698861
corr_pos[19] = -0.686245
corr_pos[20] = -0.676596
corr_pos[21] = -0.663643
corr_pos[22] = -0.655664
corr_pos[23] = -0.642249
corr_pos[24] = -0.633516
corr_pos[25] = -0.621402
corr_pos[26] = -0.612846
corr_pos[27] = -0.599978
corr_pos[28] = -0.592135
corr_pos[29] = -0.577949
corr_pos[30] = -0.570695
corr_pos[31] = -0.555560
corr_pos[32] = -0.549015
corr_pos[33] = -0.532162
corr_pos[34] = -0.524479
corr_pos[35] = -0.507848
corr_pos[36] = -0.500301
corr_pos[37] = -0.484361
corr_pos[38] = -0.477365
corr_pos[39] = -0.461751
corr_pos[40] = -0.455963
corr_pos[41] = -0.439438
corr_pos[42] = -0.436103
corr_pos[43] = -0.420587
corr_pos[44] = -0.417361
corr_pos[45] = -0.402270
corr_pos[46] = -0.400459
corr_pos[47] = -0.384745
corr_pos[48] = -0.385280
corr_pos[49] = -0.371217
corr_pos[50] = -0.373612
corr_pos[51] = -0.361838
corr_pos[52] = -0.365771
corr_pos[53] = -0.354641
corr_pos[54] = -0.360647
corr_pos[55] = -0.351446
corr_pos[56] = -0.359147
corr_pos[57] = -0.348696
corr_pos[58] = -0.359348
corr_pos[59] = -0.349962
corr_pos[60] = -0.360782
corr_pos[61] = -0.355040
corr_pos[62] = -0.367441
corr_pos[63] = -0.360511
corr_pos[64] = -0.372617
corr_pos[65] = -0.366120
corr_pos[66] = -0.377568
corr_pos[67] = -0.370362
corr_pos[68] = -0.380918
corr_pos[69] = -0.372553
corr_pos[70] = -0.381460
corr_pos[71] = -0.371828
corr_pos[72] = -0.379428
corr_pos[73] = -0.368465
corr_pos[74] = -0.374297
corr_pos[75] = -0.360340
corr_pos[76] = -0.365429
corr_pos[77] = -0.349168
corr_pos[78] = -0.351544
corr_pos[79] = -0.332514
corr_pos[80] = -0.333505
corr_pos[81] = -0.313316
corr_pos[82] = -0.313014
corr_pos[83] = -0.291107
corr_pos[84] = -0.287807
corr_pos[85] = -0.264483
corr_pos[86] = -0.260891
corr_pos[87] = -0.236646
corr_pos[88] = -0.231362
corr_pos[89] = -0.205833
corr_pos[90] = -0.200663
corr_pos[91] = -0.174834
corr_pos[92] = -0.171662
corr_pos[93] = -0.146200
corr_pos[94] = -0.142890
corr_pos[95] = -0.118389
corr_pos[96] = -0.112895
corr_pos[97] = -0.088879
corr_pos[98] = -0.084146
corr_pos[99] = -0.061625
corr_pos[100] = -0.054742
corr_pos[101] = -0.032741
corr_pos[102] = -0.026725
corr_pos[103] = -0.004827
corr_pos[104] = 0.002678
corr_pos[105] = 0.024418
corr_pos[106] = 0.031105
corr_pos[107] = 0.052334
corr_pos[108] = 0.060072
corr_pos[109] = 0.081470
corr_pos[110] = 0.088550
corr_pos[111] = 0.109685
corr_pos[112] = 0.116675
corr_pos[113] = 0.136298
corr_pos[114] = 0.142152
corr_pos[115] = 0.161986
corr_pos[116] = 0.166996
corr_pos[117] = 0.186466
corr_pos[118] = 0.190874
corr_pos[119] = 0.208729
corr_pos[120] = 0.212714
corr_pos[121] = 0.229992
corr_pos[122] = 0.232685
corr_pos[123] = 0.248748
corr_pos[124] = 0.249688
corr_pos[125] = 0.264768
corr_pos[126] = 0.264432
corr_pos[127] = 0.278801
corr_pos[128] = 0.278424
corr_pos[129] = 0.289656
corr_pos[130] = 0.289612
corr_pos[131] = 0.299899
corr_pos[132] = 0.299782
corr_pos[133] = 0.307473
corr_pos[134] = 0.309489
corr_pos[135] = 0.315999
corr_pos[136] = 0.317413
corr_pos[137] = 0.325060
corr_pos[138] = 0.325931
corr_pos[139] = 0.332544
corr_pos[140] = 0.332095
corr_pos[141] = 0.338738
corr_pos[142] = 0.337864
corr_pos[143] = 0.343909
corr_pos[144] = 0.342973
corr_pos[145] = 0.348449
corr_pos[146] = 0.346060
corr_pos[147] = 0.351949
corr_pos[148] = 0.349873
corr_pos[149] = 0.354969
corr_pos[150] = 0.352375
corr_pos[151] = 0.358388
corr_pos[152] = 0.354736
corr_pos[153] = 0.359539
corr_pos[154] = 0.354369
corr_pos[155] = 0.358549
corr_pos[156] = 0.353217
corr_pos[157] = 0.355795
corr_pos[158] = 0.351299
corr_pos[159] = 0.352922
corr_pos[160] = 0.346933
corr_pos[161] = 0.350201
corr_pos[162] = 0.343937
corr_pos[163] = 0.349304
corr_pos[164] = 0.344857
corr_pos[165] = 0.347471
corr_pos[166] = 0.335793
corr_pos[167] = 0.341109
corr_pos[168] = 0.330453
corr_pos[169] = 0.326576
corr_pos[170] = 0.320299
corr_pos[171] = 0.315348
corr_pos[172] = 0.300744
corr_pos[173] = 0.294366
corr_pos[174] = 0.280438
corr_pos[175] = 0.273618
corr_pos[176] = 0.259335
corr_pos[177] = 0.251464
corr_pos[178] = 0.237247
corr_pos[179] = 0.229092
corr_pos[180] = 0.214882
corr_pos[181] = 0.207363
corr_pos[182] = 0.193246
corr_pos[183] = 0.185868
corr_pos[184] = 0.173256
corr_pos[185] = 0.165378
corr_pos[186] = 0.153853
corr_pos[187] = 0.147031
corr_pos[188] = 0.135535
corr_pos[189] = 0.129435
corr_pos[190] = 0.119843
corr_pos[191] = 0.115062
corr_pos[192] = 0.107115
corr_pos[193] = 0.105182
corr_pos[194] = 0.099647
corr_pos[195] = 0.098162
corr_pos[196] = 0.094628
corr_pos[197] = 0.095257
corr_pos[198] = 0.084490
corr_pos[199] = 0.085389
corr_pos[200] = 0.074302
corr_pos[201] = 0.075265
corr_pos[202] = 0.063623
corr_pos[203] = 0.064487
corr_pos[204] = 0.052841
corr_pos[205] = 0.053559
corr_pos[206] = 0.042022
corr_pos[207] = 0.043321
corr_pos[208] = 0.032826
corr_pos[209] = 0.032277
corr_pos[210] = 0.022169
corr_pos[211] = 0.020740
corr_pos[212] = 0.010378
corr_pos[213] = 0.008668
corr_pos[214] = -0.001930
corr_pos[215] = -0.003882
corr_pos[216] = -0.014694
corr_pos[217] = -0.016629
corr_pos[218] = -0.026380
corr_pos[219] = -0.028069
corr_pos[220] = -0.037125
corr_pos[221] = -0.039429
corr_pos[222] = -0.047773
corr_pos[223] = -0.049261
corr_pos[224] = -0.056872
corr_pos[225] = -0.058756
corr_pos[226] = -0.065279
corr_pos[227] = -0.066859
corr_pos[228] = -0.073027
corr_pos[229] = -0.074253
corr_pos[230] = -0.079062
corr_pos[231] = -0.079800
corr_pos[232] = -0.084350
corr_pos[233] = -0.084805
corr_pos[234] = -0.088621
corr_pos[235] = -0.088437
corr_pos[236] = -0.092166
corr_pos[237] = -0.090769
corr_pos[238] = -0.093820
corr_pos[239] = -0.091724
corr_pos[240] = -0.094152
corr_pos[241] = -0.090866
corr_pos[242] = -0.092575
corr_pos[243] = -0.089239
corr_pos[244] = -0.089666
corr_pos[245] = -0.086864
corr_pos[246] = -0.087183
corr_pos[247] = -0.083974
corr_pos[248] = -0.083709
corr_pos[249] = -0.079848
corr_pos[250] = -0.078494
corr_pos[251] = -0.072782
corr_pos[252] = -0.069638
corr_pos[253] = -0.063004
corr_pos[254] = -0.058599
corr_pos[255] = -0.051737
corr_pos[256] = -0.046451
corr_pos[257] = -0.040238
corr_pos[258] = -0.034071
corr_pos[259] = -0.028578
corr_pos[260] = -0.021481
corr_pos[261] = -0.016346
corr_pos[262] = -0.009475
corr_pos[263] = -0.004141
corr_pos[264] = 0.002780
corr_pos[265] = 0.008455
corr_pos[266] = 0.015507
corr_pos[267] = 0.021672
corr_pos[268] = 0.029704
corr_pos[269] = 0.036662
corr_pos[270] = 0.044794
corr_pos[271] = 0.052031
corr_pos[272] = 0.061478
corr_pos[273] = 0.069150
corr_pos[274] = 0.078715
corr_pos[275] = 0.087785
corr_pos[276] = 0.098593
corr_pos[277] = 0.107863
corr_pos[278] = 0.118256
corr_pos[279] = 0.127631
corr_pos[280] = 0.139011
corr_pos[281] = 0.150077
corr_pos[282] = 0.162154
corr_pos[283] = 0.173758
corr_pos[284] = 0.186998
corr_pos[285] = 0.200111
corr_pos[286] = 0.214116
corr_pos[287] = 0.227291
corr_pos[288] = 0.240662
corr_pos[289] = 0.252955
corr_pos[290] = 0.265359
corr_pos[291] = 0.275995
corr_pos[292] = 0.287613
corr_pos[293] = 0.295789
corr_pos[294] = 0.306424
corr_pos[295] = 0.313027
corr_pos[296] = 0.322181
corr_pos[297] = 0.329313
corr_pos[298] = 0.338191
corr_pos[299] = 0.345243
corr_pos[300] = 0.353316
corr_pos[301] = 0.360491
corr_pos[302] = 0.367891
corr_pos[303] = 0.375027
corr_pos[304] = 0.380865
corr_pos[305] = 0.388354
corr_pos[306] = 0.395257
corr_pos[307] = 0.401478
corr_pos[308] = 0.408087
corr_pos[309] = 0.414083
corr_pos[310] = 0.420361
corr_pos[311] = 0.424284
corr_pos[312] = 0.429614
corr_pos[313] = 0.433888
corr_pos[314] = 0.438529
corr_pos[315] = 0.442302
corr_pos[316] = 0.445899
corr_pos[317] = 0.449014
corr_pos[318] = 0.451693
corr_pos[319] = 0.453795
corr_pos[320] = 0.455132
corr_pos[321] = 0.455438
corr_pos[322] = 0.455334
corr_pos[323] = 0.454055
corr_pos[324] = 0.451146
corr_pos[325] = 0.447259
corr_pos[326] = 0.442478
corr_pos[327] = 0.437520
corr_pos[328] = 0.432297
corr_pos[329] = 0.426442
corr_pos[330] = 0.418918
corr_pos[331] = 0.411040
corr_pos[332] = 0.402610
corr_pos[333] = 0.394491
corr_pos[334] = 0.383925
corr_pos[335] = 0.374590
corr_pos[336] = 0.363650
corr_pos[337] = 0.353143
corr_pos[338] = 0.339756
corr_pos[339] = 0.328074
corr_pos[340] = 0.315463
corr_pos[341] = 0.302641
corr_pos[342] = 0.288898
corr_pos[343] = 0.275134
corr_pos[344] = 0.260308
corr_pos[345] = 0.245582
corr_pos[346] = 0.233584
corr_pos[347] = 0.213812
corr_pos[348] = 0.196540
corr_pos[349] = 0.179844
corr_pos[350] = 0.161839
corr_pos[351] = 0.144160
corr_pos[352] = 0.124715
corr_pos[353] = 0.102123
corr_pos[354] = 0.085736
corr_pos[355] = 0.065743
corr_pos[356] = 0.044511

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,171 @@
import builtins
from bec_widgets.cli.client import BECDockArea
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
if builtins.__dict__.get("bec") is not None:
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
umvr = builtins.__dict__.get("umvr")
class OMNYGuiToolsError(Exception):
pass
class OMNYGuiTools:
def __init__(self, client):
self.gui = getattr(client, "gui", None)
self.gui_window = self.gui.windows['main'].widget
self.fig200 = None
self.fig201 = None
self.fig202 = None
self.fig203 = None
self.progressbar = None
self.text_box = None
self.idle_text_box = None
def omnygui_show_gui(self):
self.gui_window.show()
def omnygui_stop_gui(self):
self.gui_window.hide()
def _omnycam_parking(self):
self.omnygui_show_omnycam_parking()
def omnygui_show_omnycam_parking(self):
self.omnygui_show_gui()
if self.fig200 is None:
self._omnycam_clear()
self.fig200 = self.gui_window.add_dock(name="omnycam200").add_widget("BECImageWidget")
if self._omnycam_check_device_exists(dev.cam200):
fig = self.fig200.image("cam200")
fig.set_rotation(deg_90=3)
self.fig200.lock_aspect_ratio(True)
else:
print("Cannot open cam200. Device does not exist.")
self.fig203 = self.gui_window.add_dock(name="omnycam203").add_widget("BECImageWidget")
if self._omnycam_check_device_exists(dev.cam203):
fig = self.fig203.image("cam203")
fig.set_rotation(deg_90=3)
self.fig203.lock_aspect_ratio(True)
else:
print("Cannot open cam203. Device does not exist.")
try:
self.gui_window.remove_dock(name="default_figure")
except:
pass
def omnygui_remove_all_docks(self):
self.gui_window.clear_all()
self.fig200 = None
self.fig201 = None
self.fig202 = None
self.fig203 = None
self.progressbar = None
self.text_box = None
self.idle_text_box = None
def omnygui_idle(self):
self.omnygui_show_gui()
if self.idle_text_box is None:
self.omnygui_remove_all_docks()
self.idle_text_box = self.gui_window.add_dock(name="idle_text").add_widget("TextBox")
try:
self.gui_window.remove_dock(name="default_figure")
except:
pass
text = (
"<pre>"
+ " ,o888888o. ,8. ,8. b. 8 `8.`8888. ,8' \n"
+ " . 8888 `88. ,888. ,888. 888o. 8 `8.`8888. ,8' \n"
+ ",8 8888 `8b .`8888. .`8888. Y88888o. 8 `8.`8888. ,8' \n"
+ "88 8888 `8b ,8.`8888. ,8.`8888. .`Y888888o. 8 `8.`8888.,8' \n"
+ "88 8888 88 ,8'8.`8888,8^8.`8888. 8o. `Y888888o. 8 `8.`88888' \n"
+ "88 8888 88 ,8' `8.`8888' `8.`8888. 8`Y8o. `Y88888o8 `8. 8888 \n"
+ "88 8888 ,8P ,8' `8.`88' `8.`8888. 8 `Y8o. `Y8888 `8 8888 \n"
+ "`8 8888 ,8P ,8' `8.`' `8.`8888. 8 `Y8o. `Y8 8 8888 \n"
+ " ` 8888 ,88' ,8' `8 `8.`8888. 8 `Y8o.` 8 8888 \n"
+ " `8888888P' ,8' ` `8.`8888. 8 `Yo 8 8888 \n"
+ "</pre>"
)
self.idle_text_box.set_html_text(text)
def _omnycam_clear(self):
self.omnygui_remove_all_docks()
def _omnycam_check_device_exists(self, device):
try:
device
except:
return False
else:
return True
def _omnycam_samplestage(self):
self.omnygui_show_omnycam_samplestage()
def omnygui_show_omnycam_samplestage(self):
self.omnygui_show_gui()
if self.fig201 is None:
self.omnygui_remove_all_docks()
self.fig201 = self.gui_window.add_dock(name="omnycam201").add_widget("BECImageWidget")
if self._omnycam_check_device_exists(dev.cam201):
fig = self.fig201.image("cam201")
fig.set_rotation(deg_90=3)
self.fig201.lock_aspect_ratio(True)
else:
print("Cannot open cam201. Device does not exist.")
self.fig202 = self.gui_window.add_dock(name="omnycam202").add_widget("BECImageWidget")
if self._omnycam_check_device_exists(dev.cam202):
fig = self.fig202.image("cam202")
fig.set_rotation(deg_90=3)
self.fig202.lock_aspect_ratio(True)
else:
print("Cannot open cam202. Device does not exist.")
try:
self.gui_window.remove_dock(name="default_figure")
except:
pass
def omnygui_show_progress(self):
self.omnygui_show_gui()
if self.progressbar is None:
self.omnygui_remove_all_docks()
# Add a new dock with a RingProgressBar widget
self.progressbar = self.gui_window.add_dock(name="progress").add_widget("RingProgressBar")
# Customize the size of the progress ring
self.progressbar.set_line_widths(20)
# Disable automatic updates and manually set the self.progressbar value
self.progressbar.enable_auto_updates(False)
# Set precision for the self.progressbar display
self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
# Setting multiple rigns with different values
self.progressbar.set_number_of_bars(3)
self.progressbar.rings[2].set_update("scan")
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
# self.progressbar.set_value([50, 75])
# Add a new dock with a TextBox widget
self.text_box = self.gui_window.add_dock(name="progress_text").add_widget("TextBox")
try:
self.gui_window.remove_dock(name="default_figure")
except:
pass
self._omnygui_update_progress()
def _omnygui_update_progress(self):
if self.progressbar is not None:
progress = self.progress["projection"] / self.progress["total_projections"] * 100
subotmo_progress = (
self.progress["subtomo_projection"]
/ self.progress["subtomo_total_projections"]
* 100
)
self.progressbar.set_value([progress, subotmo_progress])
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
self.text_box.set_plain_text(text)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
import time
import numpy as np
import os
from rich import box
from rich.console import Console
from rich.table import Table
from typeguard import typechecked
from bec_lib import bec_logger
logger = bec_logger.logger
class OMNYAlignmentError(Exception):
pass
class OMNYAlignmentMixin:
default_correction_file = "correction_omny_202204.txt"
default_correction_file_x = "correction_omny_202204_x.txt"
def reset_correction(self, use_default_correction=True):
"""
Reset the correction to the default values.
If use_default_correction is False, the correction will be set to empty values.
Otherwise the default values will be loaded.
Args:
use_default_correction (bool, optional): If set to true, a call reset the correction to the default values. Defaults to True.
"""
self.corr_pos_x = []
self.corr_angle_x = []
self.corr_pos_y = []
self.corr_angle_y = []
self.corr_pos_y_2 = []
self.corr_angle_y_2 = []
if use_default_correction:
try:
self.read_additional_correction_x(self.default_correction_file_x)
logger.info(f"Applying default x correction from {self.default_correction_file_x}")
except FileNotFoundError:
logger.warning(
f"Could not find default correction file {self.default_correction_file_x}."
)
logger.warning("Not applying any correction.")
try:
self.read_additional_correction_y(self.default_correction_file)
logger.info(f"Applying default y correction from {self.default_correction_file}")
except FileNotFoundError:
logger.warning(
f"Could not find default correction file {self.default_correction_file}."
)
logger.warning("Not applying any correction.")
def reset_tomo_alignment_fit(self):
self.client.delete_global_var("tomo_alignment_fit")
def read_alignment_offset(
self,
dir_path=os.path.expanduser("~/Data10/specES1/internal/"),
setup="omny",
use_vertical_default_values=True,
):
"""
Read the alignment offset from the given directory and set the global parameter
tomo_alignment_fit.
Args:
dir_path (str, optional): The directory to read the alignment offset from. Defaults to os.path.expanduser("~/Data10/specES1/internal/").
"""
tomo_alignment_fit = np.zeros((2, 5))
with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
tomo_alignment_fit[0][0] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
tomo_alignment_fit[0][1] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
tomo_alignment_fit[0][2] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
tomo_alignment_fit[1][0] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
tomo_alignment_fit[1][1] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
tomo_alignment_fit[1][2] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
tomo_alignment_fit[1][3] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
tomo_alignment_fit[1][4] = file.readline()
print("New alignment parameters loaded:")
print(
f"X Amplitude {tomo_alignment_fit[0][0]}, "
f"X Phase {tomo_alignment_fit[0][1]}, "
f"X Offset {tomo_alignment_fit[0][2]}, "
f"Y Amplitude {tomo_alignment_fit[1][0]}, "
f"Y Phase {tomo_alignment_fit[1][1]}, "
f"Y Offset {tomo_alignment_fit[1][2]}, "
f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, "
f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ."
)
if use_vertical_default_values:
print(
f"Using default values for vertical alignment for setup {setup}. Optional: use_vertical_default_values=False"
)
if setup == "flomni":
tomo_alignment_fit[1][0] = 0
tomo_alignment_fit[1][1] = 0
tomo_alignment_fit[1][2] = 0
tomo_alignment_fit[1][3] = 0
tomo_alignment_fit[1][4] = 0
elif setup == "omny":
tomo_alignment_fit[1][0] = 2.588628
tomo_alignment_fit[1][1] = -2.385422
tomo_alignment_fit[1][2] = 0
tomo_alignment_fit[1][3] = 1.010583
tomo_alignment_fit[1][4] = -1.359157
print("Follwing parameters will be used:")
print(
f"X Amplitude {tomo_alignment_fit[0][0]}, "
f"X Phase {tomo_alignment_fit[0][1]}, "
f"X Offset {tomo_alignment_fit[0][2]}, "
f"Y Amplitude {tomo_alignment_fit[1][0]}, "
f"Y Phase {tomo_alignment_fit[1][1]}, "
f"Y Offset {tomo_alignment_fit[1][2]}, "
f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, "
f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ."
)
self.client.set_global_var("tomo_alignment_fit", tomo_alignment_fit.tolist())
# x amp, phase, offset, y amp, phase, offset, 3rd order amp, 3rd order phase
# 0 0 0 1 0 2 1 0 1 1 1 2 1 3 1 4
def get_alignment_offset(self, angle: float):
"""
Compute the alignment offset for the given angle.
Args:
angle (float): The angle to compute the alignment offset for.
Returns:
tuple: The alignment offset in x, y and z direction.
"""
tomo_alignment_fit = self.client.get_global_var("tomo_alignment_fit")
if tomo_alignment_fit is None:
print("Not applying any alignment offsets. No tomo alignment fit data available.\n")
return (0, 0, 0)
# x amp, phase, offset, y amp, phase, offset
# 0 0 0 1 0 2 1 0 1 1 1 2
correction_x = (
tomo_alignment_fit[0][0] * np.sin(np.radians(angle) + tomo_alignment_fit[0][1])
+ tomo_alignment_fit[0][2]
)
correction_y = (
tomo_alignment_fit[1][0] * np.sin(np.radians(angle) + tomo_alignment_fit[1][1])
+ tomo_alignment_fit[1][2]
+ tomo_alignment_fit[1][3] * np.sin(3 * np.radians(angle) + tomo_alignment_fit[1][4])
)
correction_z = tomo_alignment_fit[0][0] * np.sin(
np.radians(angle + 90) + tomo_alignment_fit[0][1]
)
print(
f"Alignment offset x {correction_x}, y {correction_y}, z {correction_z} for angle"
f" {angle}\n"
)
return (correction_x, correction_y, correction_z)
def _read_correction_file(self, correction_file: str):
with open(correction_file, "r") as f:
num_elements = f.readline()
int_num_elements = int(num_elements.split(" ")[2])
corr_pos = []
corr_angle = []
for j in range(int_num_elements * 2):
line = f.readline()
value = line.split(" ")[2]
name = line.split(" ")[0].split("[")[0]
if name == "corr_pos":
corr_pos.append(float(value))
elif name == "corr_angle":
corr_angle.append(float(value))
print(
f"Loading default mirror correction from file {correction_file} containing {int_num_elements} elements."
)
# print(corr_pos)
return corr_pos, corr_angle
def read_additional_correction_x(self, correction_file: str):
self.corr_pos_x, self.corr_angle_x = self._read_correction_file(correction_file)
def read_additional_correction_y(self, correction_file: str):
self.corr_pos_y, self.corr_angle_y = self._read_correction_file(correction_file)
def read_additional_correction_y_2(self, correction_file: str):
self.corr_pos_y_2, self.corr_angle_y_2 = self._read_correction_file(correction_file)
def compute_additional_correction_x(self, angle):
return self._compute_additional_correction(angle, iteration="x1")
def compute_additional_correction_y(self, angle):
return self._compute_additional_correction(angle, iteration="y1")
def compute_additional_correction_y_2(self, angle):
return self._compute_additional_correction(angle, iteration="y2")
def _compute_additional_correction(self, angle, iteration="y1"):
if iteration == "x1":
corr_pos = self.corr_pos_x
corr_angle = self.corr_angle_x
elif iteration == "y1":
corr_pos = self.corr_pos_y
corr_angle = self.corr_angle_y
elif iteration == "y2":
corr_pos = self.corr_pos_y_2
corr_angle = self.corr_angle_y_2
if not corr_pos:
print(f"Not applying any additional correction {iteration}. No data available.\n")
return 0
# find index of closest angle
for j, _ in enumerate(corr_pos):
newangledelta = np.fabs(corr_angle[j] - angle)
if j == 0:
angledelta = newangledelta
additional_correction_shift = corr_pos[j]
continue
if newangledelta < angledelta:
additional_correction_shift = corr_pos[j]
angledelta = newangledelta
if additional_correction_shift == 0 and angle > corr_angle[-1]:
additional_correction_shift = corr_pos[-1]
print(f"Additional correction shift {iteration}: {additional_correction_shift}")
return additional_correction_shift

View File

@@ -0,0 +1,153 @@
import time
import numpy as np
import sys
import termios
import tty
import fcntl
import os
import builtins
from rich import box
from rich.console import Console
from rich.table import Table
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
if builtins.__dict__.get("bec") is not None:
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
umvr = builtins.__dict__.get("umvr")
class OMNYToolsError(Exception):
pass
class OMNYTools:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKCYAN = "\033[96m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
def __init__(self, client) -> None:
self.client = client
@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 printgreen(self, string: str):
print(self.OKGREEN + string + self.ENDC)
def printgreenbold(self, string: str):
print(self.BOLD + self.OKGREEN + string + self.ENDC)
def yesno(self, message: str, default="none", autoconfirm=0) -> bool:
if autoconfirm and default == "y":
self.printgreen(message + " Automatically confirming default: yes")
return True
elif autoconfirm and default == "n":
self.printgreen(message + " Automatically confirming default: no")
return False
if default == "y":
message_ending = " [Y]/n? "
elif default == "n":
message_ending = " y/[N]? "
else:
message_ending = " y/n? "
while True:
user_input = input(self.OKBLUE + message + message_ending + self.ENDC)
if (
user_input == "Y" or user_input == "y" or user_input == "yes" or user_input == "Yes"
) or (default == "y" and user_input == ""):
return True
if (
user_input == "N" or user_input == "n" or user_input == "no" or user_input == "No"
) or (default == "n" and user_input == ""):
return False
else:
print("Please expicitely confirm y or n.")
def tweak_cursor(
self, dev1, step1: float, dev2="none", step2: float = "0", special_command="none"
):
if dev1 not in dev.enabled_devices:
print(f"Device 1 {dev} is not in enabled devices.")
return
if dev2 not in dev.enabled_devices and dev2 != "none":
print(f"Device 2 {dev} is not in enabled devices.")
return
# Save the current terminal settings
fd = sys.stdin.fileno()
old_term = termios.tcgetattr(fd)
try:
# Set the terminal to raw mode to capture single key presses
tty.setraw(fd)
# Set stdin to non-blocking mode
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
print("Tweak Cursor." + self.BOLD + self.OKBLUE + "Press (q) to quit!\r" + self.ENDC)
while True:
try:
# Read single character input
key = sys.stdin.read(1)
if key == "q":
print("\n\rExiting tweak mode\r")
break
elif key == "\x1b": # Escape sequences for arrow keys
next1, next2 = sys.stdin.read(2)
if next1 == "[":
if next2 == "A":
# print("up")
if dev2 != "none":
umvr(dev2, step2)
if special_command != "none":
special_command()
elif next2 == "B":
# print(" down")
if dev2 != "none":
umvr(dev2, -step2)
if special_command != "none":
special_command()
elif next2 == "C":
# print("right")
umvr(dev1, step1)
if special_command != "none":
special_command()
elif next2 == "D":
# print("left")
umvr(dev1, -step1)
if special_command != "none":
special_command()
elif key == "+":
step1 = step1 * 2
if dev2 != "none":
step2 = step2 * 2
print(f"\rDouble step size. New step size: {step1}, {step2}\r")
elif key == "-":
step1 = step1 / 2
if dev2 != "none":
step2 = step2 / 2
print(f"\rHalf step size. New step size: {step1}, {step2}\r")
except IOError:
# No input available, keep looping
pass
# Sleep for a short period to avoid high CPU usage
time.sleep(0.02)
finally:
# Restore the terminal to its original state
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)

View File

@@ -0,0 +1,384 @@
import time
import numpy as np
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 OMNYError(Exception):
pass
class OMNYOpticsMixin:
@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 ooptics_in(self):
self.ofzp_in()
# ocs_in
self.oosa_in()
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_enable()
self.align.update_frame()
user_input = input(
"Is the direct beam gone on the xray eye? Do you see the cone of the FZP?"
)
if user_input == "y":
printf("Next oeye_out...\n")
else:
raise OMNYError("Failed to properly move in the Xray optics")
def _oeyey_mv(self, position):
# direction dependent speeds
if dev.oeyez.get().readback < position:
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=15000")
else:
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
umv(dev.oeyey, position)
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
def oeye_out(self):
fshclose()
if self.OMNYTools.yesno("Did you move in the optics?"):
umv(dev.oeyez, -2)
self._oeyey_mv(-60.3)
# free camera
epics_put("XOMNYI-XEYE-ACQ:0", 2)
else:
raise OMNYError("The optics were not moved in. Please do so prior to eyey_out")
self.OMNYTools.printgreen("Oeye is out.")
def oeye_cam_in(self):
if dev.oeyez.get().readback < -80:
umv(dev.oeyez, -50)
if np.fabs(dev.oeyey.get().readback + 4.8) > 0.1:
self._oeyey_mv(-4.8)
if np.fabs(dev.oeyez.get().readback + 2) > 0.1 or np.fabs(dev.oeyex.get().readback) > 0.1:
umv(dev.oeyez, -2, dev.oeyex, 0)
# if still too close in z -- safety check
if np.fabs(dev.oeyez.get().readback + 2) > 0.1:
raise OMNYError("The oeye is too close in z for transfer. ERROR! Aborting.")
self.OMNYTools.printgreen("Oeye is at cam position.")
def _oeye_xray_is_in(self) -> bool:
omny_oeye_xray_inx = self._get_user_param_safe("oeyex", "xray_in")
omny_oeye_xray_iny = self._get_user_param_safe("oeyey", "xray_in")
omny_oeye_currentx = dev.oeyex.get().readback
omny_oeye_currenty = dev.oeyey.get().readback
if (
np.fabs(omny_oeye_currentx - omny_oeye_xray_inx) < 0.1
and np.fabs(omny_oeye_currenty - omny_oeye_xray_iny) < 0.1
):
return True
else:
return False
def oeye_xray_in(self):
if self._oeye_xray_is_in():
pass
else:
# todo
# self._otransfer_gripper_safe_xray_in_operation()
# if(!_oshield_is_ST_closed())
# {
# printf("The shield of the sample stage is not closed. Aborting.\n")
# exit
# }
omny_oeye_xray_inx = self._get_user_param_safe("oeyex", "xray_in")
omny_oeye_xray_iny = self._get_user_param_safe("oeyey", "xray_in")
omny_oeye_xray_inz = self._get_user_param_safe("oeyez", "xray_in")
self._oeyey_mv(omny_oeye_xray_iny)
omny_oeye_currenty = dev.oeyey.get().readback
if np.fabs(omny_oeye_currenty - omny_oeye_xray_iny) > 0.1:
raise OMNYError("The oeye did not move up.\n")
umv(dev.oeyex, omny_oeye_xray_inx, dev.oeyez, omny_oeye_xray_inz)
self.OMNYTools.printgreen("Oeye is at X-ray position.")
# some notes for the vis microscope:
# initial position for the vis light microscope
# do not open the shield when the microscope is at the vis mic position
# found eoeyx -45.13, z -84.9, y 0.64
# for a samy position of 2.8 with delta off
# the osa position should be in z around 7.4. in x it seems better
# around -0.6, where potentially xrays dont pass anymore
#
def _oosa_check_y(self):
omny_oosa_currenty = dev.oosay.get().readback
if np.fabs(omny_oosa_currenty - 0.9) > 0.05:
umv(dev.oosay, 0.9)
omny_oosa_currenty = dev.oosay.get().readback
if np.fabs(omny_oosa_currenty - 0.9) > 0.05:
raise OMNYError("oosay is not around 0.9. Aborting.")
def _oosa_to_move_corridor(self):
self._oosa_check_y()
dev.oosax.limits = [-3, 3.7] # risk collision with shield
umv(dev.oosax, -2)
dev.oosax.read(cached=False)
omny_oosa_currentx = dev.oosax.get().readback
if np.fabs(omny_oosa_currentx + 2) > 0.1:
raise OMNYError("oosax did not reach target position. Not moving in z.\n")
def oosa_in(self):
self._oosa_check_y()
dev.oshield.read(cached=False)
omny_oshield_current = dev.oshield.get().readback
if omny_oshield_current < 15:
self._oshield_ST_close()
if self.near_field == False:
x_in_pos = self._get_user_param_safe("oosax", "far_field_in")
y_in_pos = self._get_user_param_safe("oosay", "far_field_in")
z_in_pos = self._get_user_param_safe("oosaz", "far_field_in")
print("OSA movement in far-field mode.")
dev.oosaz.read(cached=False)
omny_oosa_currentz = dev.oosaz.get().readback
if omny_oosa_currentz < 6.4:
self._oosa_to_move_corridor()
dev.oosaz.limits = [6.4, 6.6]
umv(dev.oosaz, z_in_pos)
umv(dev.oosax, x_in_pos)
umv(dev.oosay, y_in_pos)
#### For the 30 nm FZP 220 um we use this part
# umv oosaz 6.5
# umv oosax 3.2453
# umv oosay 0.386015
if self.near_field == True:
x_in_pos = self._get_user_param_safe("oosax", "near_field_in")
y_in_pos = self._get_user_param_safe("oosay", "near_field_in")
z_in_pos = self._get_user_param_safe("oosaz", "near_field_in")
print("OSA movement in near-field mode.")
dev.oosaz.read(cached=False)
omny_oosa_currentz = dev.oosaz.get().readback
if omny_oosa_currentz > 0:
self._oosa_to_move_corridor()
dev.oosaz.limits = [-0.4, -0.6]
umv(dev.oosaz, z_in_pos)
umv(dev.oosax, x_in_pos)
omny_osamy_current = dev.osamy.get().readback
if omny_osamy_current < 3.25:
umv(dev.oosay, y_in_pos)
else:
raise OMNYError("Failed to move oosa in. osamy position is too large.")
self.OMNYTools.printgreen("OSA is in.")
# todo
# _omny_interferometer_align_tracking
# rt_feedback_enable
def oosa_out(self):
self._oosa_check_y()
dev.oshield.read(cached=False)
omny_oshield_current = dev.oshield.get().readback
if omny_oshield_current < 15:
self._oshield_ST_close()
omny_oosaz_current = dev.oosaz.get().readback
if self.near_field == False:
print("OSA movement in far-field mode.")
if omny_oosaz_current < 6.4:
self._oosa_to_move_corridor()
dev.oosaz.limits = [6.4, 6.6]
umv(dev.oosaz, 6.5)
umv(dev.oosax, -2)
if self.near_field == True:
print("OSA movement in near-field mode.")
if omny_oosaz_current > 0:
self._oosa_to_move_corridor()
dev.oosaz.limits = [-0.4, -0.6]
umv(dev.oosaz, -0.45)
umv(dev.oosax, -2)
# todo _omny_interferometer_align_tracking
self.OMNYTools.printgreen("OSA is out.")
def oosa_move_out_of_shield(self):
# todo: _omnycam_samplestage
self._oosa_check_y()
self._oosa_to_move_corridor()
omny_osamx_current = dev.osamx.get().readback
if np.fabs(omny_osamx_current) > 0.2:
umv(dev.osamx, 0)
omny_oosaz_current = dev.oosaz.get().readback
if omny_oosaz_current > 0.1:
dev.oosaz.limits = [-0.1, 0.1]
umv(dev.oosaz, 0)
self.OMNYTools.printgreen("OSA is out of shield.")
def ofzp_out(self):
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
y_out_pos = self._get_user_param_safe("ofzpy", "out")
if np.fabs(dev.ofzpy.get().readback - y_out_pos) > 0.02:
umv(dev.ofzpy, y_out_pos)
self.OMNYTools.printgreen("FZP at out position")
def ofzp_in(self):
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
x_in_pos = self._get_user_param_safe("ofzpx", "in")
y_in_pos = self._get_user_param_safe("ofzpy", "in")
if np.fabs(dev.ofzpy.get().readback - y_in_pos) > 0.02:
umv(dev.ofzpy, y_in_pos)
if np.fabs(dev.ofzpx.get().readback - x_in_pos) > 0.02:
umv(dev.ofzpx, x_in_pos)
self.OMNYTools.printgreen("FZP at in position")
# 220 mu FZP at ofzpz 31.8025 for eiger probe (about 2.4 mm propagation after focus)
# umv(dev.ofzpy, 0.7944)
# if np.fabs(dev.ofzpx.get().readback+0.4317)>0.05:
# umv(dev.ofzpx, -0.4317)
# note the 220 fzp also works for near field 6.2 kev by just moving back osa and fzp
# ofzpz 24.8 leads to a 9.5 mm propagation distance.
# With the 220 mu FZP this gives 100 nm pixel recons
# for the oosa macro set near_field=1
# 170 mu FZP at 6.2 kev for large beam at ofzpz 31.8025 of about 58 mu diameter
# 120 mu FZP at ofzpz 28.1991
# 250 mu FZP 60 nm at 5.65 keV
# ofzpz 29.7 for propagation distance 2.2
# umv ofzpx -0.4457
# umv ofzpy 0.193630
# 150 um fzp, 60 nm, ofzpz 33.8 at 8.9 kev for propagation of 1.7 mm after focus
# umv ofzpx -0.756678
# umv ofzpy 0.193515
# 250 um 30 nm FZP upper right
# small abberrations, seems to give good results in weak objects
# ofzpx -0.609240
# umv ofzpy 0.118265
# 250 um 30 nm FZP lower right very aberated
# ofzpx -0.881935
# umv ofzpy 0.537050
# ofzpz 28.4027
# 5.30 mm prop at 8.9 keV, 45 nm pixel in near field
# ofzpz 33.103
# 0.6 mm prop at 8.9 kev far field 7 m flight tube at foptz
# ofzpz 49.4 is reachable just without interferometer swap
# which at 6.2 keV and 250 um diam, 30 nm should gives a propagation of 0.8 after focus
# and a beam size of 6 microns diamter
###coordinates 30 nm FZP for comparing them
# not sure if that is really correct
# FZP 1 - FZP 2
# FZP 5
# FZP 4 - FZP 1
# FZP
##upper right
# umv ofzpx -0.6154 ofzpy 0.1183
# umv ocsx -0.6070 ocsy 0.0540
# lower right
# umv ofzpx -0.8341 ofzpy 0.5683
# umv ocsx -0.3880 ocsy -0.3960
# lower left
# umv ofzpx -0.3876 ofzpy 0.7902
# umv ocsx -0.8380 ocsy -0.6180
# upper left
# umv ofzpx -0.1678 ofzpy 0.3403
# umv ocsx -1.0550 ocsy -0.1680
def ofzp_info(self, mokev_val=-1, ofzpz_val=-1):
print(f"{ofzpz_val}")
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
if ofzpz_val == -1:
ofzpz_val = dev.ofzpz.readback.get()
distance = 66 + 2.4 + 31.8025 - ofzpz_val
print(
f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the 60 nm 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)
# 30 nm with additional spacer
distance = 53.84 + 0.6 + 33.1 - ofzpz_val
print(
f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the 30 nm FZP.\n"
)
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)
print(
"This function can be called with explicit energy and ofzpz position.\n Example: omny.ffzp_info(mokev_val=6.2, ofzpz_val=33.2)"
)
# from flomni
# oosaz_val = dev.oosaz.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

@@ -0,0 +1,241 @@
import time
import numpy as np
import sys
import termios
import tty
import fcntl
import os
from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
class OMNY_rt_clientError(Exception):
pass
class OMNY_rt_client:
def __init__(self):
self.mirror_channel = -1
self.mirror_amplitutde_increase = 0
self.mirror_parameters = {}
for j in range(1, 9):
self.mirror_parameters[j] = dev.rtx.controller.get_mirror_parameters(j)
@staticmethod
def _get_user_param_safe(device, var):
param = dev[device].user_parameter
if not param or param.get(var) is None:
raise OMNY_rt_clientError(
f"Device {device} has no user parameter definition for {var}."
)
return param.get(var)
def _omny_interferometer_openloop_steps(self, channel, steps, amplitude):
dev.rtx.controller._omny_interferometer_openloop_steps(channel, steps, amplitude)
def interferometer_tweaking(self):
self._tweak_interferometer()
def _tweak_interferometer(self):
self.mirror_channel = -1
# Save the current terminal settings
fd = sys.stdin.fileno()
old_term = termios.tcgetattr(fd)
print("Ready to tweak the interferometer. Press q to quit.")
print("The arrows adjust directions.")
print("Numbers select the mirror aligner.")
try:
# Set the terminal to raw mode to capture single key presses
tty.setraw(fd)
# Set stdin to non-blocking mode
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
opt_mirrorname = "none"
max = 0
while True:
try:
# Read single character input
key = sys.stdin.read(1)
if key == "q":
self.mirror_amplitutde_increase = 0
self.mirror_channel = -1
print("\n\rExiting tweak mode\r")
break
elif key == "\x1b": # Escape sequences for arrow keys
next1, next2 = sys.stdin.read(2)
if next1 == "[":
printit = True
if next2 == "A":
# print("up")
if self.mirror_channel != -1:
self._omny_interferometer_openloop_steps(
4,
-self.mirror_parameters[self.mirror_channel][
"opt_steps2_neg"
],
self.mirror_parameters[self.mirror_channel][
"opt_amplitude2_neg"
]
+ self.mirror_amplitutde_increase,
)
elif next2 == "B":
# print(" down")
if self.mirror_channel != -1:
self._omny_interferometer_openloop_steps(
4,
self.mirror_parameters[self.mirror_channel][
"opt_steps2_pos"
],
self.mirror_parameters[self.mirror_channel][
"opt_amplitude2_pos"
]
+ self.mirror_amplitutde_increase,
)
elif next2 == "C":
# print("right")
if self.mirror_channel != -1:
self._omny_interferometer_openloop_steps(
3,
-self.mirror_parameters[self.mirror_channel][
"opt_steps1_neg"
],
self.mirror_parameters[self.mirror_channel][
"opt_amplitude1_neg"
]
+ self.mirror_amplitutde_increase,
)
elif next2 == "D":
# print("left")
if self.mirror_channel != -1:
self._omny_interferometer_openloop_steps(
3,
self.mirror_parameters[self.mirror_channel][
"opt_steps1_pos"
],
self.mirror_parameters[self.mirror_channel][
"opt_amplitude1_pos"
]
+ self.mirror_amplitutde_increase,
)
elif key.isdigit() and 1 <= int(key) <= 8:
self.mirror_channel = int(key)
opt_mirrorname = self.mirror_parameters[self.mirror_channel][
"opt_mirrorname"
]
autostop = self.mirror_parameters[self.mirror_channel]["opt_signal_stop"]
averaging_time = self.mirror_parameters[self.mirror_channel][
"opt_averaging_time"
]
print(
f"\nSelected mirror channel {self.mirror_channel}: {opt_mirrorname}. Autostop {autostop}. Signal averaging time: {averaging_time}\r"
)
if int(key) == 6:
dev.rtx.controller.laser_tracker_on()
dev.rtx.controller._omny_interferometer_switch_channel(self.mirror_channel)
max = 0
printit = True
elif key == "+":
print("\nIncreasing voltage amplitudes by 100.\r")
self.mirror_amplitutde_increase += 100
elif key == "-":
print("\nDecreasing voltage amplitudes by 100.\r")
self.mirror_amplitutde_increase -= 100
elif key == "a":
if self.mirror_channel != -1:
dev.rtx.controller._omny_interferometer_optimize(
mirror_channel=self.mirror_channel, channel=3
)
dev.rtx.controller._omny_interferometer_optimize(
mirror_channel=self.mirror_channel, channel=4
)
dev.rtx.controller._omny_interferometer_optimize(
mirror_channel=self.mirror_channel, channel=3
)
dev.rtx.controller._omny_interferometer_optimize(
mirror_channel=self.mirror_channel, channel=4
)
if self.mirror_channel != -1 and printit:
printit = False
signal = dev.rtx.controller._omny_interferometer_get_signalsample(
self.mirror_parameters[self.mirror_channel]["opt_signalchannel"],
self.mirror_parameters[self.mirror_channel]["opt_averaging_time"],
)
if signal > max:
max = signal
info_str = f"Channel {self.mirror_channel}, {opt_mirrorname}, Current signal: {signal:.0f}"
filling = " " * (50 - len(info_str))
# Calculate the number of filled and unfilled segments
length = 30
percentage = signal / max
filled_length = int(length * percentage)
unfilled_length = length - filled_length
bar = "#" * filled_length + "-" * unfilled_length
print(info_str + filling + "0 " + bar + f" {max:.0f} (q)uit\r", end="")
except IOError:
# No input available, keep looping
pass
# Sleep for a short period to avoid high CPU usage
time.sleep(0.02)
finally:
# Restore the terminal to its original state
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
dev.rtx.controller._omny_interferometer_switch_alloff()
self.mirror_channel = -1
self.mirror_amplitutde_increase = 0
dev.rtx.controller.show_signal_strength_interferometer()
def show_signal_strength_interferometer(self):
dev.rtx.controller.show_signal_strength_interferometer()
def omny_interferometer_align_incoupling_angle(self):
dev.rtx.controller.omny_interferometer_align_incoupling_angle()
def interferometer_tweak_otrack(self):
self.OMNYTools.tweak_cursor(
dev.otrackz,
0.1,
dev.otracky,
0.1,
special_command=dev.rtx.controller.laser_tracker_print_intensity_for_otrack_tweaking,
)
def feedback_enable_with_reset(self):
dev.rtx.controller.feedback_enable_with_reset()
def feedback_disable(self):
dev.rtx.controller.feedback_disable()
def feedback_status(self):
if dev.rtx.controller.feedback_is_running():
print("Feedback is running.")
else:
print("Feedback is NOT running.")
def laser_tracker_on(self):
dev.rtx.controller.laser_tracker_on()
def laser_tracker_off(self):
dev.rtx.controller.laser_tracker_off()
def laser_tracker_show_all(self):
dev.rtx.controller.laser_tracker_show_all()
def omny_interferometer_align_tracking(self):
dev.rtx.controller.omny_interferometer_align_tracking()
def laser_tracker_check_and_wait_for_signalstrength(self):
dev.rtx.controller.laser_tracker_check_and_wait_for_signalstrength()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,235 @@
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.omny import OMNY
class XrayEyeAlign:
# pixel calibration, multiply to get mm
PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning
def __init__(self, client, omny: OMNY) -> None:
self.client = client
self.omny = omny
self.device_manager = client.device_manager
self.scans = client.scans
self.alignment_values = {}
self.omny.reset_correction()
self.omny.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.osamroy, val)
def get_tomo_angle(self):
return self.device_manager.devices.osamroy.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.tomo_rotate(0)
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
self.omny.oeye_xray_in()
self.omny.feedback_enable_with_reset()
# disable movement buttons
self.movement_buttons_enabled = False
sample_name = dev.omny_samples.get_sample_name_in_samplestage()
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
# this makes sure we are in a defined state
self.omny.feedback_disable()
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
umv(dev.osamx, osamx_in - 0.35)
self.omny.ofzp_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.omny.feedback_disable()
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
umv(dev.osamx, osamx_in)
self.omny.ofzp_out()
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.osamx, osamx_in)
time.sleep(0.5)
self.omny.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.omny.feedback_disable()
umvr(dev.osamy, _xrayeyalignmvy / 1000)
time.sleep(2)
epics_put("XOMNYI-XEYE-MVY:0", 0)
self.omny.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 omny.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

@@ -62,7 +62,6 @@ bec._beamline_mixin._bl_info_register(OperatorInfo)
bec._ip.prompts.username = _session_name
bec._ip.prompts.status = 1
# REGISTER BEAMLINE CHECKS
from bec_lib.bl_conditions import (
FastOrbitFeedbackCondition,

View File

@@ -12,3 +12,10 @@ def extend_command_line_args(parser):
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
return parser
# def get_config() -> ServiceConfig:
# """
# Create and return the service configuration.
# """
# return ServiceConfig(redis={"host": "localhost", "port": 6379})

View File

@@ -0,0 +1,63 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
if TYPE_CHECKING: # pragma: no cover
from bec_lib.messages import ScanStatusMessage
class cSAXSUpdate(AutoUpdates):
#######################################################################
################# GUI Callbacks #######################################
#######################################################################
def on_start(self) -> None:
"""
Procedure to run when the auto updates are enabled.
"""
self.start_default_dock()
def on_stop(self) -> None:
"""
Procedure to run when the auto updates are disabled.
"""
def on_scan_open(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan starts.
Args:
msg (ScanStatusMessage): The scan status message.
"""
if msg.scan_name == "line_scan" and msg.scan_report_devices:
return self.simple_line_scan(msg)
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
return self.simple_grid_scan(msg)
if msg.scan_report_devices:
return self.best_effort(msg)
return None
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan ends.
Args:
msg (ScanStatusMessage): The scan status message.
"""
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan is aborted.
Args:
msg (ScanStatusMessage): The scan status message.
"""
class cSAXSUpdateAlignment(AutoUpdates): ...
class cSAXSUpdateScan(AutoUpdates): ...

View File

@@ -0,0 +1,75 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
logger = bec_logger.logger
# pylint: skip-file
_Widgets = {
"OmnyAlignment": "OmnyAlignment",
}
class OmnyAlignment(RPCBase):
@property
@rpc_call
def enable_live_view(self):
"""
None
"""
@enable_live_view.setter
@rpc_call
def enable_live_view(self):
"""
None
"""
@property
@rpc_call
def user_message(self):
"""
None
"""
@user_message.setter
@rpc_call
def user_message(self):
"""
None
"""
@property
@rpc_call
def sample_name(self):
"""
None
"""
@sample_name.setter
@rpc_call
def sample_name(self):
"""
None
"""
@property
@rpc_call
def enable_move_buttons(self):
"""
None
"""
@enable_move_buttons.setter
@rpc_call
def enable_move_buttons(self):
"""
None
"""

View File

@@ -0,0 +1,139 @@
from typing import TypedDict
from bec_widgets.utils.error_popups import SafeSlot
import os
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.ui_loader import UILoader
from qtpy.QtWidgets import QWidget, QPushButton, QLineEdit, QLabel, QVBoxLayout
from bec_qthemes import material_icon
from bec_lib.logger import bec_logger
logger = bec_logger.logger
# class OmnyAlignmentUIComponents(TypedDict):
# moveRightButton: QPushButton
# moveLeftButton: QPushButton
# moveUpButton: QPushButton
# moveDownButton: QPushButton
# image: Image
class OmnyAlignment(BECWidget, QWidget):
USER_ACCESS = ["enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter","sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
PLUGIN = True
ui_file = "./omny_alignment.ui"
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
self._load_ui()
def _load_ui(self):
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
layout = QVBoxLayout()
layout.addWidget(self.ui)
self.setLayout(layout)
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
self.ui.moveRightButton.setText("")
self.ui.moveRightButton.setIcon(
material_icon(icon_name="keyboard_arrow_right", **icon_options)
)
self.ui.moveLeftButton.setText("")
self.ui.moveLeftButton.setIcon(
material_icon(icon_name="keyboard_arrow_left", **icon_options)
)
self.ui.moveUpButton.setText("")
self.ui.moveUpButton.setIcon(
material_icon(icon_name="keyboard_arrow_up", **icon_options)
)
self.ui.moveDownButton.setText("")
self.ui.moveDownButton.setIcon(
material_icon(icon_name="keyboard_arrow_down", **icon_options)
)
self.ui.confirmButton.setText("OK")
self.ui.liveViewSwitch.enabled.connect(self.on_live_view_enabled)
@property
def enable_live_view(self):
return self.ui.liveViewSwitch.checked
@enable_live_view.setter
def enable_live_view(self, enable:bool):
self.ui.liveViewSwitch.checked = enable
@property
def user_message(self):
return self.ui.messageLineEdit.text()
@user_message.setter
def user_message(self, message:str):
self.ui.messageLineEdit.setText(message)
@property
def sample_name(self):
return self.ui.sampleLineEdit.text()
@sample_name.setter
def sample_name(self, message:str):
self.ui.sampleLineEdit.setText(message)
@SafeSlot(bool)
def on_live_view_enabled(self, enabled:bool):
from bec_widgets.widgets.plots.image.image import Image
logger.info(f"Live view is enabled: {enabled}")
image: Image = self.ui.image
if enabled:
image.image("cam200")
return
image.disconnect_monitor("cam200")
@property
def enable_move_buttons(self):
move_up:QPushButton = self.ui.moveUpButton
move_down:QPushButton = self.ui.moveDownButton
move_left:QPushButton = self.ui.moveLeftButton
move_right:QPushButton = self.ui.moveRightButton
return move_up.isEnabled() and move_down.isEnabled() and move_left.isEnabled() and move_right.isEnabled()
@enable_move_buttons.setter
def enable_move_buttons(self, enabled:bool):
move_up:QPushButton = self.ui.moveUpButton
move_down:QPushButton = self.ui.moveDownButton
move_left:QPushButton = self.ui.moveLeftButton
move_right:QPushButton = self.ui.moveRightButton
move_up.setEnabled(enabled)
move_down.setEnabled(enabled)
move_left.setEnabled(enabled)
move_right.setEnabled(enabled)
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
widget = OmnyAlignment()
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1 @@
{'files': ['omny_alignment.py']}

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>988</width>
<height>821</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="2">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QPushButton" name="moveRightButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="moveLeftButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="moveUpButton">
<property name="text">
<string>Up</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="moveDownButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="confirmButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="sampleLineEdit"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="messageLineEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Sample</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Message</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="3">
<widget class="Image" name="image">
<property name="enable_toolbar" stdset="0">
<bool>false</bool>
</property>
<property name="inner_axes" stdset="0">
<bool>false</bool>
</property>
<property name="monitor" stdset="0">
<string>cam200</string>
</property>
<property name="rotation" stdset="0">
<number>3</number>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="ToggleSwitch" name="liveViewSwitch"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Live View</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Image</class>
<extends>QWidget</extends>
<header>image</header>
</customwidget>
<customwidget>
<class>ToggleSwitch</class>
<extends>QWidget</extends>
<header>toggle_switch</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment import OmnyAlignment
DOM_XML = """
<ui language='c++'>
<widget class='OmnyAlignment' name='omny_alignment'>
</widget>
</ui>
"""
class OmnyAlignmentPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = OmnyAlignment(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return designer_material_icon(OmnyAlignment.ICON_NAME)
def includeFile(self):
return "omny_alignment"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "OmnyAlignment"
def toolTip(self):
return "OmnyAlignment"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment_plugin import OmnyAlignmentPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(OmnyAlignmentPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,11 +0,0 @@
import os
def setup_epics_ca():
os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
os.environ["PYTHONIOENCODING"] = "latin1"
def run():
setup_epics_ca()

View File

View File

@@ -0,0 +1,11 @@
import os
def setup_epics_ca():
# os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
# os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
os.environ["PYTHONIOENCODING"] = "latin1"
def run():
setup_epics_ca()

30
csaxs_bec/device_configs/bec_device_config_sastt.yaml Executable file → Normal file
View File

@@ -17,7 +17,7 @@ bpm4i:
softwareTrigger: false
mokev:
description: Monochromator energy in keV
deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev
deviceClass: csaxs_bec.devices.epics.specMotors.EnergyKev
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
@@ -29,7 +29,7 @@ mokev:
softwareTrigger: false
mcs:
description: Mcs scalar card for transmission readout
deviceClass: csaxs_bec.devices.epics.devices.MCScSAXS
deviceClass: csaxs_bec.devices.epics.mcs_csaxs.MCScSAXS
deviceConfig:
prefix: 'X12SA-MCS:'
mcs_config:
@@ -43,7 +43,7 @@ mcs:
softwareTrigger: false
eiger9m:
description: Eiger9m HPC area detector 9M
deviceClass: csaxs_bec.devices.epics.devices.Eiger9McSAXS
deviceClass: csaxs_bec.devices.epics.eiger9m_csaxs.Eiger9McSAXS
deviceConfig:
prefix: 'X12SA-ES-EIGER9M:'
deviceTags:
@@ -55,7 +55,7 @@ eiger9m:
softwareTrigger: false
ddg_detectors:
description: DelayGenerator for detector triggering
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG1:'
ddg_config:
@@ -82,7 +82,7 @@ ddg_detectors:
softwareTrigger: false
ddg_mcs:
description: DelayGenerator for mcs triggering
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG2:'
ddg_config:
@@ -111,7 +111,7 @@ ddg_mcs:
softwareTrigger: false
ddg_fsh:
description: DelayGenerator for fast shutter control
deviceClass: csaxs_bec.devices.epics.devices.DelayGeneratorcSAXS
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
deviceConfig:
prefix: 'delaygen:DG3:'
ddg_config:
@@ -138,7 +138,7 @@ ddg_fsh:
softwareTrigger: false
falcon:
description: Falcon detector x-ray fluoresence
deviceClass: csaxs_bec.devices.epics.devices.FalconcSAXS
deviceClass: csaxs_bec.devices.epics.falcon_csaxs.FalconcSAXS
deviceConfig:
prefix: 'X12SA-SITORO:'
deviceTags:
@@ -150,7 +150,7 @@ falcon:
softwareTrigger: false
pilatus_2:
description: Pilatus2 HPC area detector 300k
deviceClass: csaxs_bec.devices.epics.devices.PilatuscSAXS
deviceClass: csaxs_bec.devices.epics.pilatus_csaxs.PilatuscSAXS
deviceConfig:
prefix: 'X12SA-ES-PILATUS300K:'
deviceTags:
@@ -162,7 +162,7 @@ pilatus_2:
softwareTrigger: false
samx:
description: SGalil motor stage
deviceClass: csaxs_bec.devices.galil.SGalilMotor
deviceClass: csaxs_bec.devices.omny.galil.SGalilMotor
deviceConfig:
axis_Id: "E"
host: '129.129.122.26'
@@ -180,7 +180,7 @@ samx:
softwareTrigger: false
samy:
description: SGalil motor stage
deviceClass: csaxs_bec.devices.galil.SGalilMotor
deviceClass: csaxs_bec.devices.omny.galil.SGalilMotor
deviceConfig:
axis_Id: "C"
host: '129.129.122.26'
@@ -198,7 +198,7 @@ samy:
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES06
motor_resolution: 0.00125
@@ -216,7 +216,7 @@ micfoc:
softwareTrigger: false
owis_samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
@@ -234,7 +234,7 @@ owis_samx:
softwareTrigger: false
owis_samy:
description: Owis motor stage samx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
@@ -252,7 +252,7 @@ owis_samy:
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.0025
@@ -273,7 +273,7 @@ rotx:
softwareTrigger: false
roty:
description: Rotation stage rotx
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025

View File

@@ -3,7 +3,7 @@
############################################################
leyex:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: G
host: mpc2680.psi.ch
@@ -22,7 +22,7 @@ leyex:
in: 14.117
leyey:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: H
host: mpc2680.psi.ch
@@ -42,7 +42,7 @@ leyey:
out: 0.5
loptx:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2680.psi.ch
@@ -62,7 +62,7 @@ loptx:
out: -0.699
lopty:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2680.psi.ch
@@ -82,7 +82,7 @@ lopty:
out: 3.53
loptz:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2680.psi.ch
@@ -99,7 +99,7 @@ loptz:
readoutPriority: baseline
lsamrot:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2680.psi.ch
@@ -116,7 +116,7 @@ lsamrot:
readoutPriority: baseline
lsamx:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2680.psi.ch
@@ -135,7 +135,7 @@ lsamx:
center: 8.768
lsamy:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.galil_ophyd.GalilMotor
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2680.psi.ch
@@ -162,7 +162,7 @@ lsamy:
############################################################
rtx:
deviceClass: csaxs_bec.devices.rt_lamni.rt_lamni_ophyd.RtLamniMotor
deviceClass: csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniMotor
deviceConfig:
axis_Id: A
device_access: true
@@ -179,7 +179,7 @@ rtx:
enabled: true
readOnly: False
rty:
deviceClass: csaxs_bec.devices.rt_lamni.rt_lamni_ophyd.RtLamniMotor
deviceClass: csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniMotor
deviceConfig:
axis_Id: B
device_access: true
@@ -293,7 +293,7 @@ losaz:
eiger1p5m:
description: Eiger 1.5M in vacuum detector, in-house developed, PSI
deviceClass: csaxs_bec.devices.eiger1p5m_csaxs.eiger1p5m.Eiger1p5MDetector
deviceClass: csaxs_bec.devices.omny.eiger1p5m.Eiger1p5MDetector
deviceConfig:
device_access: true
deviceTags:
@@ -504,7 +504,7 @@ sls_filling_pattern:
readOnly: True
sls_info:
readoutPriority: on_request
deviceClass: ophyd_devices.sls_devices.sls_devices.SLSInfo
deviceClass: ophyd_devices.devices.sls_devices.SLSInfo
deviceConfig:
deviceTags:
- SLS status
@@ -537,7 +537,7 @@ sls_machine_status:
readOnly: True
sls_operator:
readoutPriority: on_request
deviceClass: ophyd_devices.sls_devices.sls_devices.SLSOperatorMessages
deviceClass: ophyd_devices.devices.sls_devices.SLSOperatorMessages
deviceConfig:
auto_monitor: true
read_pv: ARIDI-BPM:OFB-MODE
@@ -802,7 +802,7 @@ bm5try:
softwareTrigger: false
bpm1:
description: 'XBPM 1: Somewhere around mono (VME)'
deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp
deviceClass: csaxs_bec.devices.epics.XbpmBase.XbpmCsaxsOp
deviceConfig:
prefix: 'X12SA-OP-BPM2:'
deviceTags:
@@ -824,7 +824,7 @@ bpm1i:
softwareTrigger: false
bpm2:
description: 'XBPM 2: Somewhere around mono (VME)'
deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp
deviceClass: csaxs_bec.devices.epics.XbpmBase.XbpmCsaxsOp
deviceConfig:
prefix: 'X12SA-OP-BPM2:'
deviceTags:
@@ -1187,7 +1187,7 @@ dtpush:
softwareTrigger: false
dtth:
description: Detector tower tilt rotation
deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmDetectorRotation
deviceClass: csaxs_bec.devices.epics.specMotors.PmDetectorRotation
deviceConfig:
prefix: X12SA-ES1-DETT:ROX1
deviceTags:
@@ -1462,7 +1462,7 @@ fttrz:
softwareTrigger: false
idgap:
description: Undulator gap size [mm]
deviceClass: csaxs_bec.devices.epics.devices.InsertionDevice
deviceClass: csaxs_bec.devices.epics.InsertionDevice.InsertionDevice
deviceConfig:
prefix: X12SA-ID
deviceTags:
@@ -1561,7 +1561,7 @@ mitry3:
softwareTrigger: false
mobd:
description: Monochromator bender virtual motor
deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmMonoBender
deviceClass: csaxs_bec.devices.epics.specMotors.PmMonoBender
deviceConfig:
prefix: 'X12SA-OP-MO:'
deviceTags:
@@ -1616,7 +1616,7 @@ mobddi:
softwareTrigger: false
mokev:
description: Monochromator energy in keV
deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev
deviceClass: csaxs_bec.devices.epics.specMotors.EnergyKev
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
@@ -1671,7 +1671,7 @@ moroll2:
softwareTrigger: false
moth1:
description: Monochromator Theta 1
deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta1
deviceClass: csaxs_bec.devices.epics.specMotors.MonoTheta1
deviceConfig:
read_pv: X12SA-OP-MO:ROX1
deviceTags:
@@ -1693,7 +1693,7 @@ moth1e:
softwareTrigger: false
moth2:
description: Monochromator Theta 2
deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta2
deviceClass: csaxs_bec.devices.epics.specMotors.MonoTheta2
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
@@ -1812,17 +1812,17 @@ sec:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl0h:
description: FrontEnd slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-FE-SH1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl0h:
# description: FrontEnd slit virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-FE-SH1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl0trxi:
description: FrontEnd slit inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1845,17 +1845,17 @@ sl0trxo:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl1h:
description: OpticsHutch slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-OP-SH1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl1h:
# description: OpticsHutch slit virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-OP-SH1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl1trxi:
description: OpticsHutch slit inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1900,28 +1900,28 @@ sl1tryt:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl1v:
description: OpticsHutch slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitV
deviceConfig:
prefix: 'X12SA-OP-SV1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl2h:
description: OpticsHutch slit 2 virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-OP-SH2:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl1v:
# description: OpticsHutch slit virtual movement
# deviceClass: ophyd_devices.devices.SlitV
# deviceConfig:
# prefix: 'X12SA-OP-SV1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sl2h:
# description: OpticsHutch slit 2 virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-OP-SH2:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl2trxi:
description: OpticsHutch slit 2 inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1966,72 +1966,72 @@ sl2tryt:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl2v:
description: OpticsHutch slit 2 virtual movement
deviceClass: ophyd_devices.epics.devices.SlitV
deviceConfig:
prefix: 'X12SA-OP-SV2:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
strox:
description: Girder virtual pitch
deviceClass: ophyd_devices.epics.devices.GirderMotorPITCH
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
stroy:
description: Girder virtual yaw
deviceClass: ophyd_devices.epics.devices.GirderMotorYAW
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
stroz:
description: Girder virtual roll
deviceClass: ophyd_devices.epics.devices.GirderMotorROLL
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sttrx:
description: Girder X translation
deviceClass: ophyd_devices.epics.devices.GirderMotorX1
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sttry:
description: Girder Y translation
deviceClass: ophyd_devices.epics.devices.GirderMotorY1
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl2v:
# description: OpticsHutch slit 2 virtual movement
# deviceClass: ophyd_devices.devices.SlitV
# deviceConfig:
# prefix: 'X12SA-OP-SV2:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# strox:
# description: Girder virtual pitch
# deviceClass: ophyd_devices.devices.GirderMotorPITCH
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# stroy:
# description: Girder virtual yaw
# deviceClass: ophyd_devices.devices.GirderMotorYAW
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# stroz:
# description: Girder virtual roll
# deviceClass: ophyd_devices.devices.GirderMotorROLL
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sttrx:
# description: Girder X translation
# deviceClass: ophyd_devices.devices.GirderMotorX1
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sttry:
# description: Girder Y translation
# deviceClass: ophyd_devices.devices.GirderMotorY1
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
transd:
description: Transmission diode
deviceClass: ophyd.EpicsSignalRO

View File

@@ -0,0 +1,50 @@
ddg_detectors:
description: DelayGenerator for detector triggering
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
deviceConfig:
prefix: 'X12SA-CPCL-DDG3:'
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: True
bpm4i:
readoutPriority: monitored
deviceClass: ophyd_devices.SimMonitor
deviceConfig:
deviceTags:
- beamline
enabled: true
readOnly: false
samx:
readoutPriority: baseline
deviceClass: ophyd_devices.SimPositioner
deviceConfig:
delay: 1
limits:
- -50
- 50
tolerance: 0.01
update_frequency: 400
deviceTags:
- user motors
enabled: true
readOnly: false

View File

@@ -1,6 +1,10 @@
############################################################
#################### flOMNI Galil motors ###################
############################################################
feyex:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
description: Xray eye X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
@@ -17,8 +21,8 @@ feyex:
in: -16.267
out: -1
feyey:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
description: Xray eye Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
@@ -34,8 +38,8 @@ feyey:
userParameter:
in: -10.467
fheater:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
description: Heater Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
@@ -48,17 +52,9 @@ fheater:
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
description: Optics X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
@@ -74,8 +70,8 @@ foptx:
userParameter:
in: -13.761
fopty:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
description: Optics Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
@@ -92,8 +88,8 @@ fopty:
in: 0.552
out: 0.752
foptz:
description: phase plate angle
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
description: Optics Z
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
@@ -108,8 +104,166 @@ foptz:
readoutPriority: baseline
userParameter:
in: 23
fsamroy:
description: Sample rotation
deviceClass: csaxs_bec.devices.omny.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: Sample coarse X
deviceClass: csaxs_bec.devices.omny.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: Sample coarse Y
deviceClass: csaxs_bec.devices.omny.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: Laser Tracker coarse Y
deviceClass: csaxs_bec.devices.omny.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: Laser Tracker coarse Z
deviceClass: csaxs_bec.devices.omny.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: Sample transer X
deviceClass: csaxs_bec.devices.omny.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: Sample transer Y
deviceClass: csaxs_bec.devices.omny.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: Sample transer Z
deviceClass: csaxs_bec.devices.omny.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: Sample transfer tray
deviceClass: csaxs_bec.devices.omny.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
############################################################
#################### flOMNI Sample Names ###################
############################################################
flomni_samples:
description: Sample names and storage
deviceClass: csaxs_bec.devices.omny.flomni_sample_storage.FlomniSampleStorage
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
############################################################
#################### flOMNI Smaract motors #################
############################################################
fosax:
description: phase plate angle
description: OSA X
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
@@ -127,7 +281,7 @@ fosax:
in: 9.124
out: 5.3
fosay:
description: phase plate angle
description: OSA Y
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
@@ -144,7 +298,7 @@ fosay:
userParameter:
in: 0.367
fosaz:
description: phase plate angle
description: OSA Z
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
@@ -161,148 +315,14 @@ fosaz:
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
############################################################
#################### flOMNI RT motors ######################
############################################################
rtx:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
@@ -318,7 +338,7 @@ rtx:
rt_pid_voltage: -0.06219
rty:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
@@ -332,7 +352,7 @@ rty:
tomo_additional_offsety: 0
rtz:
description: flomni rt
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch

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

View File

@@ -0,0 +1,261 @@
############################################################
#################### LamNI Galil motors ####################
############################################################
leyex:
description: Xray eye X
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: G
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 14.117
leyey:
description: Xray eye Y
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: H
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 48.069
out: 0.5
loptx:
description: Optics X
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -0.244
out: -0.699
lopty:
description: Optics Y
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 3.724
out: 3.53
loptz:
description: Optics Z
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
lsamrot:
description: Sample rotation
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
lsamx:
description: Sample coarse X
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
center: 8.768
lsamy:
description: Sample coarse Y
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
center: 10.041
############################################################
################ LamNI Smaract motors ######################
############################################################
losax:
description: OSA X
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8085
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -1.442
losay:
description: OSA Y
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8085
sign: -1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -0.171
out: 3.8
losaz:
description: OSA Z
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: mpc2680.psi.ch
limits:
- 0
- 0
port: 8085
sign: 1
deviceTags:
- lamni
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -1
out: -3
############################################################
#################### flOMNI RT motors ######################
############################################################
rtx:
deviceClass: csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniMotor
deviceConfig:
axis_Id: A
device_access: true
host: mpc2680.psi.ch
labels: rtx
limits:
- 0
- 0
port: 3333
sign: 1
deviceTags:
- lamni
readoutPriority: baseline
enabled: true
readOnly: False
rty:
deviceClass: csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniMotor
deviceConfig:
axis_Id: B
device_access: true
host: mpc2680.psi.ch
labels: rty
limits:
- 0
- 0
port: 3333
sign: 1
deviceTags:
- lamni
readoutPriority: baseline
enabled: true
readOnly: False

View File

@@ -0,0 +1,38 @@
############################################################
#################### npoint motors #########################
############################################################
npx:
description: nPoint x axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: A
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint
npy:
description: nPoint y axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: B
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint

View File

@@ -0,0 +1,610 @@
# ############################################################
# #################### IDS Camera ######################
# ############################################################
cam200:
description: Camera200
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
deviceConfig:
camera_ID: 200
bits_per_pixel: 24
channels: 3
m_n_colormode: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
cam201:
description: Camera201
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
deviceConfig:
camera_ID: 201
bits_per_pixel: 24
channels: 3
m_n_colormode: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
cam202:
description: Camera202
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
deviceConfig:
camera_ID: 202
bits_per_pixel: 24
channels: 3
m_n_colormode: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
cam203:
description: Camera203
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
deviceConfig:
camera_ID: 203
bits_per_pixel: 24
channels: 3
m_n_colormode: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
############################################################
#################### OMNY RT motors ########################
############################################################
rtx:
description: OMNY rt
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
port: 3333
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
low_signal: 8500
min_signal: 8000
rty:
description: OMNY rt
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
deviceConfig:
axis_Id: B
host: mpc3217.psi.ch
port: 3333
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
tomo_additional_offsety: 0
rtz:
description: OMNY rt
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
deviceConfig:
axis_Id: C
host: mpc3217.psi.ch
port: 3333
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
# ############################################################
# ##################### OMNY samples #########################
# ############################################################
omny_samples:
description: OMNYSampleStorage
deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
# ############################################################
# ##################### OMNY vacuum ##########################
# ############################################################
# omny_vcs:
# description: OMNYVCS
# deviceClass: csaxs_bec.devices.omny.omny_vcs.OMNYVCS
# deviceConfig: {}
# enabled: true
# onFailure: buffer
# readOnly: false
# readoutPriority: baseline
# ############################################################
# ##################### OMNY dewar ###########################
# ############################################################
omny_dewar:
description: OMNY Dewar Information
deviceClass: csaxs_bec.devices.omny.omny_dewar.OMNYDewar
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
# ############################################################
# ##################### OMNY temperatures ####################
# ############################################################
omny_temperatures:
description: OMNY Temperatures and pressures
deviceClass: csaxs_bec.devices.omny.omny_temperatures.OMNYTemperatures
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
############################################################
##################### OMNY Galil motors ####################
############################################################
ofzpx:
description: FZP X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -0.4317
ofzpy:
description: FZP Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: B
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0.7944
out: 0.6377
ofzpz:
description: FZP Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: C
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
otransx:
description: Transfer X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: D
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
otransy:
description: Transfer Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: E
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
up_position: -1.2
gripper_sensorvoltagetarget: -2.30
otransz:
description: Transfer Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: F
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
osamx:
description: Sample X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -0.1
osamz:
description: Sample Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: B
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: false
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 0
oosay:
description: OSA Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: C
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
near_field_in: 0.531
far_field_in: 0.4122
oosax:
description: OSA X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: D
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: -1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
near_field_in: 3.2044
far_field_in: 3.022
oosaz:
description: OSA Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: E
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: -1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
near_field_in: -0.4452
far_field_in: 6.5
oparkz:
description: OSA Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: F
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
oshuttleopen:
description: Shuttle opener
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: G
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 0
oshuttlealign:
description: Shuttle aligner
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: H
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8082
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 0
osamy:
description: Sample Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
otracky:
description: Laser Tracker Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: B
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
start_pos: -4.3431
osamroy:
description: Sample rotation
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: C
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
otrackz:
description: Laser Tracker Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: E
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: -1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
start_pos: -0.6948
oeyex:
description: Xray eye X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: F
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
xray_in: -45.7394
oeyez:
description: Xray eye Z
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: G
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
xray_in: -2
oeyey:
description: Xray eye Y
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: H
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8083
sign: 1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
xray_in: 0.0229
############################################################
#################### flOMNI Smaract motors #################
############################################################
ocsx:
description: Central Stop X
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: mpc3217.psi.ch
limits:
- -2
- 2
port: 3334
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
nothing: 0
ocsy:
description: Central Stop Y
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
limits:
- -2
- 2
port: 3334
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
nothing: 0
oshield:
description: Thermal Shield Sample Stage
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: mpc3217.psi.ch
limits:
- -14.5
- 15.8
port: 3334
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
nothing: 0

View File

@@ -220,7 +220,7 @@ bm5try:
softwareTrigger: false
bpm1:
description: 'XBPM 1: Somewhere around mono (VME)'
deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp
deviceClass: csaxs_bec.devices.epics.XbpmBase.XbpmCsaxsOp
deviceConfig:
prefix: 'X12SA-OP-BPM2:'
deviceTags:
@@ -242,7 +242,7 @@ bpm1i:
softwareTrigger: false
bpm2:
description: 'XBPM 2: Somewhere around mono (VME)'
deviceClass: csaxs_bec.devices.epics.devices.XbpmBase.XbpmCsaxsOp
deviceClass: csaxs_bec.devices.epics.XbpmBase.XbpmCsaxsOp
deviceConfig:
prefix: 'X12SA-OP-BPM2:'
deviceTags:
@@ -605,7 +605,7 @@ dtpush:
softwareTrigger: false
dtth:
description: Detector tower tilt rotation
deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmDetectorRotation
deviceClass: csaxs_bec.devices.epics.specMotors.PmDetectorRotation
deviceConfig:
prefix: X12SA-ES1-DETT:ROX1
deviceTags:
@@ -880,7 +880,7 @@ fttrz:
softwareTrigger: false
idgap:
description: Undulator gap size [mm]
deviceClass: csaxs_bec.devices.epics.devices.InsertionDevice.InsertionDevice
deviceClass: csaxs_bec.devices.epics.InsertionDevice.InsertionDevice
deviceConfig:
prefix: X12SA-ID
deviceTags:
@@ -979,7 +979,7 @@ mitry3:
softwareTrigger: false
mobd:
description: Monochromator bender virtual motor
deviceClass: csaxs_bec.devices.epics.devices.specMotors.PmMonoBender
deviceClass: csaxs_bec.devices.epics.specMotors.PmMonoBender
deviceConfig:
prefix: 'X12SA-OP-MO:'
deviceTags:
@@ -1034,7 +1034,7 @@ mobddi:
softwareTrigger: false
mokev:
description: Monochromator energy in keV
deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev
deviceClass: csaxs_bec.devices.epics.specMotors.EnergyKev
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
@@ -1089,7 +1089,7 @@ moroll2:
softwareTrigger: false
moth1:
description: Monochromator Theta 1
deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta1
deviceClass: csaxs_bec.devices.epics.specMotors.MonoTheta1
deviceConfig:
read_pv: X12SA-OP-MO:ROX1
deviceTags:
@@ -1111,7 +1111,7 @@ moth1e:
softwareTrigger: false
moth2:
description: Monochromator Theta 2
deviceClass: csaxs_bec.devices.epics.devices.specMotors.MonoTheta2
deviceClass: csaxs_bec.devices.epics.specMotors.MonoTheta2
deviceConfig:
read_pv: X12SA-OP-MO:ROX2
deviceTags:
@@ -1230,17 +1230,17 @@ sec:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl0h:
description: FrontEnd slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-FE-SH1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl0h:
# description: FrontEnd slit virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-FE-SH1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl0trxi:
description: FrontEnd slit inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1263,17 +1263,17 @@ sl0trxo:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl1h:
description: OpticsHutch slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-OP-SH1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl1h:
# description: OpticsHutch slit virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-OP-SH1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl1trxi:
description: OpticsHutch slit inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1318,28 +1318,28 @@ sl1tryt:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl1v:
description: OpticsHutch slit virtual movement
deviceClass: ophyd_devices.epics.devices.SlitV
deviceConfig:
prefix: 'X12SA-OP-SV1:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl2h:
description: OpticsHutch slit 2 virtual movement
deviceClass: ophyd_devices.epics.devices.SlitH
deviceConfig:
prefix: 'X12SA-OP-SH2:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl1v:
# description: OpticsHutch slit virtual movement
# deviceClass: ophyd_devices.devices.SlitV
# deviceConfig:
# prefix: 'X12SA-OP-SV1:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sl2h:
# description: OpticsHutch slit 2 virtual movement
# deviceClass: ophyd_devices.devices.SlitH
# deviceConfig:
# prefix: 'X12SA-OP-SH2:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
sl2trxi:
description: OpticsHutch slit 2 inner blade movement
deviceClass: ophyd.EpicsMotor
@@ -1384,72 +1384,72 @@ sl2tryt:
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sl2v:
description: OpticsHutch slit 2 virtual movement
deviceClass: ophyd_devices.epics.devices.SlitV
deviceConfig:
prefix: 'X12SA-OP-SV2:'
deviceTags:
- epicsDevice
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
strox:
description: Girder virtual pitch
deviceClass: ophyd_devices.epics.devices.GirderMotorPITCH
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
stroy:
description: Girder virtual yaw
deviceClass: ophyd_devices.epics.devices.GirderMotorYAW
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
stroz:
description: Girder virtual roll
deviceClass: ophyd_devices.epics.devices.GirderMotorROLL
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sttrx:
description: Girder X translation
deviceClass: ophyd_devices.epics.devices.GirderMotorX1
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
sttry:
description: Girder Y translation
deviceClass: ophyd_devices.epics.devices.GirderMotorY1
deviceConfig:
prefix: X12SA-HG
deviceTags:
- beamlineMotor
enabled: true
onFailure: buffer
readoutPriority: baseline
softwareTrigger: false
# sl2v:
# description: OpticsHutch slit 2 virtual movement
# deviceClass: ophyd_devices.devices.SlitV
# deviceConfig:
# prefix: 'X12SA-OP-SV2:'
# deviceTags:
# - epicsDevice
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# strox:
# description: Girder virtual pitch
# deviceClass: ophyd_devices.devices.GirderMotorPITCH
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# stroy:
# description: Girder virtual yaw
# deviceClass: ophyd_devices.devices.GirderMotorYAW
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# stroz:
# description: Girder virtual roll
# deviceClass: ophyd_devices.devices.GirderMotorROLL
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sttrx:
# description: Girder X translation
# deviceClass: ophyd_devices.devices.GirderMotorX1
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
# sttry:
# description: Girder Y translation
# deviceClass: ophyd_devices.devices.GirderMotorY1
# deviceConfig:
# prefix: X12SA-HG
# deviceTags:
# - beamlineMotor
# enabled: true
# onFailure: buffer
# readoutPriority: baseline
# softwareTrigger: false
transd:
description: Transmission diode
deviceClass: ophyd.EpicsSignalRO

View File

@@ -0,0 +1,44 @@
// This file was autogenerated. Do not edit it manually.
## Device List
### csaxs_bec
| Device | Documentation | Module |
| :----- | :------------- | :------ |
| Bpm4i | | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| DelayGeneratorcSAXS | <br> DG645 delay generator at cSAXS (multiple can be in use depending on the setup)<br><br> Default values for setting up DDG.<br> Note: checks of set calues are not (only partially) included, check manual for details on possible settings.<br> https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf<br><br> - delay_burst : (float >=0) Delay between trigger and first pulse in burst mode<br> - delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition<br> - additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line)<br> - polarity : (list of 0/1) polarity for different channels<br> - amplitude : (float) amplitude voltage of TTLs<br> - offset : (float) offset for ampltitude<br> - thres_trig_level : (float) threshold of trigger amplitude<br><br> Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg):<br><br> - set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan.<br> # TODO trigger_width and fixed_ttl could be combined into single list.<br> - fixed_ttl_width : (list of either 1 or 0), one for each channel.<br> - trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value.<br> - set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG.<br> - premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan).<br> - set_high_on_stage : (bool) if True, then TTL signal should go high already on stage.<br> | [csaxs_bec.devices.epics.delay_generator_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/delay_generator_csaxs.py) |
| Eiger1p5MDetector | | [csaxs_bec.devices.omny.eiger1p5m](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/eiger1p5m.py) |
| Eiger9McSAXS | <br> Eiger9M detector for CSAXS<br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> Various EpicsPVs for controlling the detector<br> | [csaxs_bec.devices.epics.eiger9m_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/eiger9m_csaxs.py) |
| EpicsDXPFalcon | <br> DXP parameters for Falcon detector<br><br> Base class to map EPICS PVs from DXP parameters to ophyd signals.<br> | [csaxs_bec.devices.epics.falcon_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/falcon_csaxs.py) |
| FalconcSAXS | <br> Falcon Sitoro detector for CSAXS<br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector<br> dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector<br> mca (EpicsMCARecord) : MCA parameters for Falcon detector<br> hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector<br> MIN_READOUT (float) : Minimum readout time for the detector<br> | [csaxs_bec.devices.epics.falcon_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/falcon_csaxs.py) |
| FalconHDF5Plugins | <br> HDF5 parameters for Falcon detector<br><br> Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.<br> | [csaxs_bec.devices.epics.falcon_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/falcon_csaxs.py) |
| FlomniGalilMotor | | [csaxs_bec.devices.omny.galil.fgalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/fgalil_ophyd.py) |
| FlomniSampleStorage | | [csaxs_bec.devices.omny.flomni_sample_storage](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/flomni_sample_storage.py) |
| FuprGalilMotor | | [csaxs_bec.devices.omny.galil.fupr_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/fupr_ophyd.py) |
| GirderMotorPITCH | Girder YAW pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| GirderMotorROLL | Girder ROLL pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| GirderMotorX1 | Girder X translation pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| GirderMotorY1 | Girder Y translation pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| GirderMotorYAW | Girder YAW pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| IDSCamera | | [csaxs_bec.devices.ids_cameras.ids_camera](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/ids_cameras/ids_camera.py) |
| InsertionDevice | Python wrapper for the CSAXS insertion device control<br><br> This wrapper provides a positioner interface for the ID control.<br> is completely custom XBPM with templates directly in the<br> VME repo. Thus it needs a custom ophyd template as well...<br><br> WARN: The x and y are not updated by the IOC<br> | [csaxs_bec.devices.epics.InsertionDevice](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/InsertionDevice.py) |
| LamniGalilMotor | | [csaxs_bec.devices.omny.galil.lgalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/lgalil_ophyd.py) |
| MCScSAXS | MCS card for cSAXS for implementation at cSAXS beamline | [csaxs_bec.devices.epics.mcs_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/mcs_csaxs.py) |
| NPointAxis | <br> NPointAxis class, which inherits from Device and PositionerBase. This class<br> represents an axis of an nPoint piezo stage and provides the necessary<br> functionality to move the axis and read its current position.<br> | [csaxs_bec.devices.npoint.npoint](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/npoint/npoint.py) |
| OMNYDewar | | [csaxs_bec.devices.omny.omny_dewar](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_dewar.py) |
| OMNYGalilMotor | | [csaxs_bec.devices.omny.galil.ogalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/ogalil_ophyd.py) |
| OMNYSampleStorage | | [csaxs_bec.devices.omny.omny_sample_storage](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_sample_storage.py) |
| OMNYTemperatures | | [csaxs_bec.devices.omny.omny_temperatures](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_temperatures.py) |
| OMNYVCS | | [csaxs_bec.devices.omny.omny_vcs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_vcs.py) |
| PilatuscSAXS | Pilatus_2 300k detector for CSAXS<br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> cam (SLSDetectorCam) : Detector camera<br> MIN_READOUT (float) : Minimum readout time for the detector<br><br> | [csaxs_bec.devices.epics.pilatus_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/pilatus_csaxs.py) |
| PmDetectorRotation | Detector rotation pseudo motor<br><br> Small wrapper to convert detector pusher position to rotation angle.<br> | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| PmMonoBender | Monochromator bender<br><br> Small wrapper to combine the four monochromator bender motors.<br> | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
| RtFlomniMotor | | [csaxs_bec.devices.omny.rt.rt_flomni_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py) |
| RtLamniMotor | | [csaxs_bec.devices.omny.rt.rt_lamni_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_lamni_ophyd.py) |
| RtOMNYMotor | | [csaxs_bec.devices.omny.rt.rt_omny_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_omny_ophyd.py) |
| SGalilMotor | "SGalil Motors at cSAXS have a<br> DC motor (y axis - vertical) - implemented as C<br> and a step motor (x-axis horizontal) - implemented as E<br> that require different communication for control<br> | [csaxs_bec.devices.omny.galil.sgalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/sgalil_ophyd.py) |
| SIS38XX | SIS38XX card for access to EPICs PVs at cSAXS beamline | [csaxs_bec.devices.epics.mcs_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/mcs_csaxs.py) |
| SLSDetectorCam | SLS Detector Camera - Pilatus<br><br> Base class to map EPICS PVs to ophyd signals.<br> | [csaxs_bec.devices.epics.pilatus_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/pilatus_csaxs.py) |
| SmaractMotor | | [csaxs_bec.devices.smaract.smaract_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/smaract/smaract_ophyd.py) |
| XbpmBase | Python wrapper for X-ray Beam Position Monitors<br><br> XBPM's consist of a metal-coated diamond window that ejects<br> photoelectrons from the incoming X-ray beam. These electons<br> are collected and their current is measured. Effectively<br> they act as four quadrant photodiodes and are used as BPMs<br> at the undulator beamlines of SLS.<br><br> Note: EPICS provided signals are read only, but the user can<br> change the beam position offset.<br> | [csaxs_bec.devices.epics.XbpmBase](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/XbpmBase.py) |
| XbpmCsaxsOp | Python wrapper for custom XBPMs in the cSAXS optics hutch<br><br> This is completely custom XBPM with templates directly in the<br> VME repo. Thus it needs a custom ophyd template as well...<br><br> WARN: The x and y are not updated by the IOC<br> | [csaxs_bec.devices.epics.XbpmBase](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/XbpmBase.py) |
| XbpmSim | Python wrapper for simulated X-ray Beam Position Monitors<br><br> XBPM's consist of a metal-coated diamond window that ejects<br> photoelectrons from the incoming X-ray beam. These electons<br> are collected and their current is measured. Effectively<br> they act as four quadrant photodiodes and are used as BPMs<br> at the undulator beamlines of SLS.<br><br> Note: EPICS provided signals are read only, but the user can<br> change the beam position offset.<br><br> This simulation device extends the basic proxy with a script that<br> fills signals with quasi-randomized values.<br> | [csaxs_bec.devices.epics.XbpmBase](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/XbpmBase.py) |
| Xeye | | [csaxs_bec.devices.sls_devices.cSAXS.xeye](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/sls_devices/cSAXS/xeye.py) |

View File

@@ -1,8 +1 @@
# 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

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

@@ -0,0 +1,274 @@
from bec_lib import bec_logger
from ophyd import Component, DeviceStatus, Kind
from ophyd_devices.devices.delay_generator_645 import DelayGenerator, TriggerSource
from ophyd_devices.interfaces.base_classes.bec_device_base import BECDeviceBase, CustomPrepare
from ophyd_devices.sim.sim_signals import SetableSignal
from ophyd_devices.utils import bec_utils
logger = bec_logger.logger
class DelayGeneratorcSAXSError(Exception):
"""Exception raised for errors."""
class DDGSetup(CustomPrepare["DelayGeneratorcSAXS"]):
"""
Custom Prepare class with hooks for beamline specific logic for the DG645 at CSAXS
"""
def on_wait_for_connection(self) -> None:
"""Init default parameter after the all signals are connected"""
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 on_stage(self) -> None:
"Hook execute before the scan starts"
if self.parent.scaninfo.scan_type == "step":
exp_time = self.parent.scaninfo.exp_time
delay = 0
self.parent.burst_disable()
self.parent.set_trigger(TriggerSource.SINGLE_SHOT)
self.parent.set_channels(signal="width", value=exp_time)
self.parent.set_channels(signal="delay", value=delay)
return
scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "")
if scan_name == "jjf_test":
# TODO implement the logic for JJF triggering
exp_time = 480e-6 # self.parent.scaninfo.exp_time
readout = 20e-6 # self.parent.scaninfo.readout_time
total_exposure = exp_time + readout
num_burst_cycle = self.parent.scaninfo.scan_msg.content["info"]["kwargs"]["num_points"]
num_burst_cycle = int(num_burst_cycle * self.parent.scaninfo.exp_time / total_exposure)
delay = 0
delay_burst = self.parent.delay_burst.get()
self.parent.set_trigger(trigger_source=TriggerSource.SINGLE_SHOT)
self.parent.set_channels(signal="width", value=exp_time)
self.parent.set_channels(signal="delay", value=delay)
self.parent.burst_enable(
count=num_burst_cycle, delay=delay_burst, period=total_exposure, config="first"
)
logger.info(
f"{self.parent.name}: On stage with n_burst: {num_burst_cycle} and total_exp {total_exposure}"
)
def on_trigger(self) -> DeviceStatus:
"""Method to be executed upon trigger"""
if self.parent.scaninfo.scan_type == "step":
self.parent.trigger_shot.put(1)
return
scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "")
if scan_name == "jjf_test":
exp_time = 480e-6 # self.parent.scaninfo.exp_time
readout = 20e-6 # self.parent.scaninfo.readout_time
total_exposure = exp_time + readout
num_burst_cycle = self.parent.scaninfo.scan_msg.content["info"]["kwargs"]["num_points"]
num_burst_cycle = int(num_burst_cycle * self.parent.scaninfo.exp_time / total_exposure)
# Start trigger cycle
self.parent.trigger_burst_readout.put(1)
# Create status object that will wait for the end of the burst cycle
status = self.wait_with_status(
signal_conditions=[(self.parent.burst_cycle_finished, 1)],
timeout=num_burst_cycle * total_exposure + 1, # add 1s to be sure
check_stopped=True,
exception_on_timeout=DelayGeneratorcSAXSError(
f"{self.parent.name} run into timeout in complete call."
),
)
logger.info(f"Return status {self.parent.name}")
return status
def on_complete(self) -> DeviceStatus:
pass
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(BECDeviceBase, DelayGenerator):
"""
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
# Custom signals passed on during the init procedure via BEC
# TODO review whether those should remain here like that
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,
name: str,
prefix: str = "",
kind: Kind = None,
ddg_config: dict = None,
parent=None,
device_manager=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,
parent=parent,
device_manager=device_manager,
**kwargs,
)
# if __name__ == "__main__":
# dgen = DelayGeneratorcSAXS("X12SA-CPCL-DDG3:", name="ddg3")

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

@@ -5,12 +5,13 @@ 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 ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from std_daq_client import StdDaqClient
logger = bec_logger.logger
@@ -39,6 +40,17 @@ class Eiger9MSetup(CustomDetectorMixin):
self.std_client = None
self._lock = threading.RLock()
def on_init(self) -> None:
"""Initialize the detector"""
self.initialize_default_parameter()
self.initialize_detector()
self.initialize_detector_backend()
def initialize_detector(self) -> None:
"""Initialize detector"""
self.stop_detector()
self.parent.cam.trigger_mode.put(TriggerSource.GATING)
def initialize_default_parameter(self) -> None:
"""Set default parameters for Eiger9M detector"""
self.update_readout_time()
@@ -52,29 +64,19 @@ class Eiger9MSetup(CustomDetectorMixin):
)
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
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
all_signals=True,
):
raise EigerTimeoutError(
f"Std client not in READY state, returns: {self.std_client.get_status()}"
@@ -95,7 +97,6 @@ class Eiger9MSetup(CustomDetectorMixin):
"""
# 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:
@@ -108,19 +109,112 @@ class Eiger9MSetup(CustomDetectorMixin):
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 on_stage(self) -> None:
"""Prepare the detector for scan"""
self.prepare_detector()
self.prepare_data_backend()
self.publish_file_location(done=False, successful=False)
self.arm_acquisition()
def prepare_detector(self) -> None:
"""Prepare detector for scan"""
self.set_detector_threshold()
self.set_acquisition_params()
self.parent.cam.trigger_mode.put(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.
"""
mokev = self.parent.device_manager.devices.mokev.obj.read()[
self.parent.device_manager.devices.mokev.name
]["value"]
factor = 1
unit = getattr(self.parent.cam.threshold_energy, "units", None)
if unit is not None and unit == "eV":
factor = 1000
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)
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"""
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)
self.update_readout_time()
def prepare_data_backend(self) -> None:
"""Prepare the data backend for the scan"""
self.parent.filepath.set(
self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5")
).wait()
self.filepath_exists(self.parent.filepath.get())
self.stop_detector_backend()
try:
self.std_client.start_writer_async(
{
"output_file": self.parent.filepath.get(),
"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
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_FOR_SIGNALS,
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 on_unstage(self) -> None:
"""Unstage the detector"""
pass
def on_complete(self) -> None:
"""Complete the detector"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
self.publish_file_location(done=True, successful=True)
def on_stop(self) -> None:
"""Stop the detector"""
self.stop_detector()
self.stop_detector_backend()
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][
@@ -132,7 +226,7 @@ class Eiger9MSetup(CustomDetectorMixin):
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
check_stopped=True,
all_signals=False,
):
@@ -140,7 +234,7 @@ class Eiger9MSetup(CustomDetectorMixin):
self.parent.cam.acquire.put(0)
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
check_stopped=True,
all_signals=False,
):
@@ -152,97 +246,12 @@ class Eiger9MSetup(CustomDetectorMixin):
"""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,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=False,
all_signals=True,
):
@@ -261,7 +270,7 @@ class Eiger9MSetup(CustomDetectorMixin):
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=True,
all_signals=False,
):
@@ -269,69 +278,35 @@ class Eiger9MSetup(CustomDetectorMixin):
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):
def finished(self, timeout: int = 5) -> None:
"""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()
with self._lock:
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=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):
@@ -352,7 +327,7 @@ class SLSDetectorCam(Device):
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
class TriggerSource(enum.IntEnum):
class TriggerSource(int, enum.Enum):
"""Trigger signals for Eiger9M detector"""
AUTO = 0
@@ -361,7 +336,7 @@ class TriggerSource(enum.IntEnum):
BURST_TRIGGER = 3
class DetectorState(enum.IntEnum):
class DetectorState(int, enum.Enum):
"""Detector states for Eiger9M detector"""
IDLE = 0
@@ -391,37 +366,16 @@ class Eiger9McSAXS(PSIDetectorBase):
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
USER_ACCESS = []
# specify Setup class
custom_prepare_cls = Eiger9MSetup
# specify minimum readout time for detector
# specify minimum readout time for detector and timeout for checks after unstage
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
# 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,13 +1,15 @@
import enum
import os
import threading
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
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
@@ -96,6 +98,16 @@ class FalconSetup(CustomDetectorMixin):
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self._lock = threading.RLock()
def on_init(self) -> None:
"""Initialize Falcon detector"""
self.initialize_default_parameter()
self.initialize_detector()
self.initialize_detector_backend()
def initialize_default_parameter(self) -> None:
"""
Set default parameters for Falcon
@@ -121,7 +133,7 @@ class FalconSetup(CustomDetectorMixin):
"""Initialize Falcon detector"""
self.stop_detector()
self.stop_detector_backend()
self.parent.set_trigger(
self.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# 1 Realtime
@@ -133,30 +145,6 @@ class FalconSetup(CustomDetectorMixin):
# 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)
@@ -170,9 +158,16 @@ class FalconSetup(CustomDetectorMixin):
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.parent.nd_array_mode.put(1)
def on_stage(self) -> None:
"""Prepare detector and backend for acquisition"""
self.prepare_detector()
self.prepare_data_backend()
self.publish_file_location(done=False, successful=False)
self.arm_acquisition()
def prepare_detector(self) -> None:
"""Prepare detector for acquisition"""
self.parent.set_trigger(
self.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
@@ -182,10 +177,10 @@ class FalconSetup(CustomDetectorMixin):
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.filepath.set(
self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5")
).wait()
file_path, file_name = os.path.split(self.parent.filepath.get())
self.parent.hdf5.file_path.put(file_path)
self.parent.hdf5.file_name.put(file_name)
self.parent.hdf5.file_template.put("%s%s")
@@ -209,7 +204,7 @@ class FalconSetup(CustomDetectorMixin):
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=True,
all_signals=False,
):
@@ -217,65 +212,86 @@ class FalconSetup(CustomDetectorMixin):
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 on_unstage(self) -> None:
"""Unstage detector and backend"""
pass
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
def on_complete(self) -> None:
"""Complete detector and backend"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
self.publish_file_location(done=True, successful=True)
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}"
)
def on_stop(self) -> None:
"""Stop detector and backend"""
self.stop_detector()
self.stop_detector_backend()
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_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 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 finished(self, timeout: int = 5) -> None:
"""Check if scan finished succesfully"""
with self._lock:
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=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()
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.parent.collect_mode.put(mapping)
self.parent.pixel_advance_mode.put(trigger)
self.parent.ignore_gate.put(ignore_gate)
class FalconcSAXS(PSIDetectorBase):
"""
@@ -300,6 +316,7 @@ class FalconcSAXS(PSIDetectorBase):
custom_prepare_cls = FalconSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
# specify class attributes
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
@@ -327,30 +344,6 @@ class FalconcSAXS(PSIDetectorBase):
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

@@ -3,10 +3,14 @@ import threading
from collections import defaultdict
import numpy as np
from bec_lib import MessageEndpoints, bec_logger, messages, threadlocked
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
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.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from ophyd_devices.utils import bec_utils
logger = bec_logger.logger
@@ -70,6 +74,11 @@ class MCSSetup(CustomDetectorMixin):
]
self.mca_data = defaultdict(lambda: [])
def on_init(self) -> None:
"""Init sequence for the detector"""
self.initialize_detector()
self.initialize_detector_backend()
def initialize_detector(self) -> None:
"""Initialize detector"""
# External trigger for pixel advance
@@ -80,7 +89,7 @@ class MCSSetup(CustomDetectorMixin):
# Set number of channels to 5
self.parent.mux_output.set(5)
# Trigger Mode used for cSAXS
self.parent.set_trigger(TriggerSource.MODE3)
self.parent.input_mode.set(TriggerSource.MODE3)
# specify polarity of trigger signals
self.parent.input_polarity.set(0)
self.parent.output_polarity.set(1)
@@ -109,17 +118,17 @@ class MCSSetup(CustomDetectorMixin):
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: [])
with self._lock:
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"""
@@ -138,10 +147,15 @@ class MCSSetup(CustomDetectorMixin):
expire=self._stream_ttl,
)
def on_stage(self) -> None:
"""Stage detector"""
self.prepare_detector()
self.prepare_detector_backend()
def prepare_detector(self) -> None:
"""Prepare detector for scan"""
self.set_acquisition_params()
self.parent.set_trigger(TriggerSource.MODE3)
self.parent.input_mode.set(TriggerSource.MODE3)
def set_acquisition_params(self) -> None:
"""Set acquisition parameters for scan"""
@@ -171,7 +185,15 @@ class MCSSetup(CustomDetectorMixin):
self.counter = 0
self.parent.erase_start.set(1)
def finished(self) -> None:
def on_unstage(self) -> None:
"""Unstage detector"""
pass
def on_complete(self) -> None:
"""Complete detector"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
def finished(self, timeout: int = 5) -> None:
"""Check if acquisition is finished, if not successful, rais MCSTimeoutError"""
signal_conditions = [
(lambda: self.acquisition_done, True),
@@ -179,7 +201,7 @@ class MCSSetup(CustomDetectorMixin):
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
timeout=timeout,
check_stopped=True,
all_signals=True,
):
@@ -191,12 +213,15 @@ class MCSSetup(CustomDetectorMixin):
f" {total_frames} frames arriving at the mcs card"
)
def on_stop(self) -> None:
"""Stop detector"""
self.stop_detector()
self.stop_detector_backend()
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
@@ -209,7 +234,7 @@ class SIS38XX(Device):
class MCScSAXS(PSIDetectorBase):
"""MCS card for cSAXS for implementation at cSAXS beamline"""
USER_ACCESS = ["describe", "_init_mcs"]
USER_ACCESS = []
SUB_PROGRESS = "progress"
SUB_VALUE = "value"
_default_sub = SUB_VALUE
@@ -218,6 +243,7 @@ class MCScSAXS(PSIDetectorBase):
custom_prepare_cls = MCSSetup
# specify minimum readout time for detector
MIN_READOUT = 0
TIMEOUT_FOR_SIGNALS = 5
# PV access to SISS38XX card
# Acquisition
@@ -268,11 +294,8 @@ class MCScSAXS(PSIDetectorBase):
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
device_manager=None,
sim_mode=False,
mcs_config=None,
**kwargs,
):
@@ -285,25 +308,11 @@ class MCScSAXS(PSIDetectorBase):
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__":

View File

@@ -1,19 +1,21 @@
import enum
import json
import os
import threading
import time
import numpy as np
import requests
from bec_lib import MessageEndpoints, bec_logger, messages
from bec_lib import bec_logger
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
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
MIN_READOUT = 3e-3
class PilatusError(Exception):
"""Base class for exceptions in this module."""
@@ -68,6 +70,15 @@ class PilatusSetup(CustomDetectorMixin):
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self._lock = threading.RLock()
def on_init(self) -> None:
"""Initialize the detector"""
self.initialize_default_parameter()
self.initialize_detector()
def initialize_default_parameter(self) -> None:
"""Set default parameters for Eiger9M detector"""
self.update_readout_time()
@@ -86,7 +97,15 @@ class PilatusSetup(CustomDetectorMixin):
# Stops the detector
self.stop_detector()
# Sets the trigger source to GATING
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
def on_stage(self) -> None:
"""Stage the detector for scan"""
self.prepare_detector()
self.prepare_data_backend()
self.publish_file_location(
done=False, successful=False, metadata={"input_path": self.parent.filepath_raw}
)
def prepare_detector(self) -> None:
"""
@@ -97,84 +116,7 @@ class PilatusSetup(CustomDetectorMixin):
"""
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()
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
def prepare_data_backend(self) -> None:
"""
@@ -187,7 +129,9 @@ class PilatusSetup(CustomDetectorMixin):
self.stop_detector_backend()
self.parent.filepath = self.parent.filewriter.compile_full_filename("pilatus_2.h5")
self.parent.filepath.set(
self.parent.filewriter.compile_full_filename("pilatus_2.h5")
).wait()
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}"
@@ -271,6 +215,76 @@ class PilatusSetup(CustomDetectorMixin):
except Exception as exc:
logger.info(f"Pilatus2 wait threw Exception: {exc}")
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 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 send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
"""
Send a put request to the given url
@@ -298,14 +312,8 @@ class PilatusSetup(CustomDetectorMixin):
"""
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
"""
def on_pre_scan(self) -> None:
"""Prepare detector for scan"""
self.arm_acquisition()
def arm_acquisition(self) -> None:
@@ -314,43 +322,18 @@ class PilatusSetup(CustomDetectorMixin):
# 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
def on_unstage(self) -> None:
"""Unstage the detector"""
pass
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,
def on_complete(self) -> None:
"""Complete the scan"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
self.publish_file_location(
done=True, successful=True, metadata={"input_path": self.parent.filepath_raw}
)
self.parent.connector.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
)
pipe.execute()
def finished(self) -> None:
def finished(self, timeout: int = 5) -> None:
"""Check if acquisition is finished."""
# pylint: disable=protected-access
# TODO: at the moment this relies on device.mcs.obj._staged attribute
@@ -359,7 +342,7 @@ class PilatusSetup(CustomDetectorMixin):
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
timeout=timeout,
check_stopped=True,
all_signals=True,
):
@@ -371,16 +354,21 @@ class PilatusSetup(CustomDetectorMixin):
self.stop_detector()
self.stop_detector_backend()
def on_stop(self) -> None:
"""Stop detector"""
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
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)
class PilatuscSAXS(PSIDetectorBase):
@@ -397,20 +385,16 @@ class PilatuscSAXS(PSIDetectorBase):
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
USER_ACCESS = []
# specify Setup class
custom_prepare_cls = PilatusSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
# 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,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]")) != 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

@@ -0,0 +1,312 @@
import threading
import time
import numpy as np
from ophyd import Component as Cpt
from ophyd import DeviceStatus, Kind, StatusBase
from ophyd_devices import PSIDeviceBase
from ophyd_devices.utils.bec_signals import PreviewSignal
try:
from pyueye import ueye
except ImportError:
# The pyueye library is not installed or doesn't provide the necessary c libs
ueye = None
class IDSCamera(PSIDeviceBase):
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
image_data = Cpt(PreviewSignal, value=np.empty((100, 100)), kind=Kind.omitted)
def __init__(
self,
prefix="",
*,
name: str,
camera_ID: int,
bits_per_pixel: int,
channels: int,
m_n_colormode: int,
kind=None,
device_manager=None,
**kwargs,
):
super().__init__(
prefix=prefix, name=name, kind=kind, device_manager=device_manager, **kwargs
)
self.camera_ID = camera_ID
self.bits_per_pixel = bits_per_pixel
self.bytes_per_pixel = None
self.channels = channels
self._m_n_colormode_input = m_n_colormode
self.m_n_colormode = None
self.ueye = ueye
self.h_cam = None
self.s_info = None
self.data_thread = None
self.c_info = None
self.pc_image_memory = None
self.mem_id = None
self.rect_aoi = None
self.pitch = None
self.n_bits_per_pixel = None
self.width = None
self.height = None
self.thread_event = threading.Event()
self.data_thread = None
def start_backend(self):
if self.ueye is None:
raise ImportError("The pyueye library is not installed.")
self.h_cam = self.ueye.HIDS(
self.camera_ID
) # 0: first available camera; 1-254: The camera with the specified camera ID
self.s_info = self.ueye.SENSORINFO()
self.c_info = self.ueye.CAMINFO()
self.pc_image_memory = self.ueye.c_mem_p()
self.mem_id = self.ueye.int()
self.rect_aoi = self.ueye.IS_RECT()
self.pitch = self.ueye.INT()
self.n_bits_per_pixel = self.ueye.INT(
self.bits_per_pixel
) # 24: bits per pixel for color mode; take 8 bits per pixel for monochrome
self.m_n_colormode = self.ueye.INT(
self._m_n_colormode_input
) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
# Starts the driver and establishes the connection to the camera
ret = self.ueye.is_InitCamera(self.h_cam, None)
if ret != self.ueye.IS_SUCCESS:
print("is_InitCamera ERROR")
# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that c_info points to
ret = self.ueye.is_GetCameraInfo(self.h_cam, self.c_info)
if ret != self.ueye.IS_SUCCESS:
print("is_GetCameraInfo ERROR")
# You can query additional information about the sensor type used in the camera
ret = self.ueye.is_GetSensorInfo(self.h_cam, self.s_info)
if ret != self.ueye.IS_SUCCESS:
print("is_GetSensorInfo ERROR")
ret = self.ueye.is_ResetToDefault(self.h_cam)
if ret != self.ueye.IS_SUCCESS:
print("is_ResetToDefault ERROR")
# Set display mode to DIB
ret = self.ueye.is_SetDisplayMode(self.h_cam, self.ueye.IS_SET_DM_DIB)
# Set the right color mode
if (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_BAYER
):
# setup the color depth to the current windows setting
self.ueye.is_GetColorDepth(self.h_cam, self.n_bits_per_pixel, self.m_n_colormode)
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
print("IS_COLORMODE_BAYER: ")
print("\tm_n_colormode: \t\t", self.m_n_colormode)
print("\tn_bits_per_pixel: \t\t", self.n_bits_per_pixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_CBYCRY
):
# for color camera models use RGB32 mode
m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
n_bits_per_pixel = self.ueye.INT(32)
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
print("IS_COLORMODE_CBYCRY: ")
print("\tm_n_colormode: \t\t", m_n_colormode)
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_MONOCHROME
):
# for color camera models use RGB32 mode
m_n_colormode = self.ueye.IS_CM_MONO8
n_bits_per_pixel = self.ueye.INT(8)
bytes_per_pixel = int(n_bits_per_pixel / 8)
print("IS_COLORMODE_MONOCHROME: ")
print("\tm_n_colormode: \t\t", m_n_colormode)
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
else:
# for monochrome camera models use Y8 mode
m_n_colormode = self.ueye.IS_CM_MONO8
n_bits_per_pixel = self.ueye.INT(8)
bytes_per_pixel = int(n_bits_per_pixel / 8)
print("else")
# Can be used to set the size and position of an "area of interest"(AOI) within an image
ret = self.ueye.is_AOI(
self.h_cam,
self.ueye.IS_AOI_IMAGE_GET_AOI,
self.rect_aoi,
self.ueye.sizeof(self.rect_aoi),
)
if ret != self.ueye.IS_SUCCESS:
print("is_AOI ERROR")
self.width = self.rect_aoi.s32Width
self.height = self.rect_aoi.s32Height
# Prints out some information about the camera and the sensor
print("Camera model:\t\t", self.s_info.strSensorName.decode("utf-8"))
print("Camera serial no.:\t", self.c_info.SerNo.decode("utf-8"))
print("Maximum image width:\t", self.width)
print("Maximum image height:\t", self.height)
print()
# ---------------------------------------------------------------------------------------------------------------------------------------
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by n_bits_per_pixel
ret = self.ueye.is_AllocImageMem(
self.h_cam,
self.width,
self.height,
self.n_bits_per_pixel,
self.pc_image_memory,
self.mem_id,
)
if ret != self.ueye.IS_SUCCESS:
print("is_AllocImageMem ERROR")
else:
# Makes the specified image memory the active memory
ret = self.ueye.is_SetImageMem(self.h_cam, self.pc_image_memory, self.mem_id)
if ret != self.ueye.IS_SUCCESS:
print("is_SetImageMem ERROR")
else:
# Set the desired color mode
ret = self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode)
# Activates the camera's live video mode (free run mode)
ret = self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT)
if ret != self.ueye.IS_SUCCESS:
print("is_CaptureVideo ERROR")
# Enables the queue mode for existing image memory sequences
ret = self.ueye.is_InquireImageMem(
self.h_cam,
self.pc_image_memory,
self.mem_id,
self.width,
self.height,
self.n_bits_per_pixel,
self.pitch,
)
if ret != self.ueye.IS_SUCCESS:
print("is_InquireImageMem ERROR")
else:
print("Press q to leave the programm")
# startmeasureframerate = True
# Gain = False
# Start live mode of camera immediately
self.start_live_mode()
def _start_data_thread(self):
self.data_thread = threading.Thread(target=self._receive_data_from_camera, daemon=True)
self.data_thread.start()
def _receive_data_from_camera(self):
while not self.thread_event.is_set():
if self.ueye is None:
print("pyueye library not available.")
return
# In order to display the image in an OpenCV window we need to...
# ...extract the data of our image memory
array = self.ueye.get_data(
self.pc_image_memory,
self.width,
self.height,
self.n_bits_per_pixel,
self.pitch,
copy=False,
)
# bytes_per_pixel = int(n_bits_per_pixel / 8)
# ...reshape it in an numpy array...
frame = np.reshape(array, (self.height.value, self.width.value, self.bytes_per_pixel))
self.image_data.put(frame)
time.sleep(0.1)
def wait_for_connection(self, all_signals=False, timeout=10):
if ueye is None:
raise ImportError(
"The pyueye library is not installed or doesn't provide the necessary c libs"
)
super().wait_for_connection(all_signals, timeout)
def start_live_mode(self):
if self.data_thread is not None:
self.stop_live_mode()
self._start_data_thread()
def stop_live_mode(self):
"""Stopping the camera live mode."""
self.thread_event.set()
if self.data_thread is not None:
self.data_thread.join()
self.thread_event.clear()
self.data_thread = None
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
self.start_backend()
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
"""
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device."""
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""Called right before the scan starts on all devices automatically."""
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""Called to inquire if a device has completed a scans."""
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
def on_stop(self) -> None:
"""Called when the device is stopped."""
def on_destroy(self) -> None:
"""Called when the device is destroyed. Cleanup resources here."""
self.stop_live_mode()

View File

@@ -0,0 +1,153 @@
import enum
import math
import jfjoch_client
from bec_lib.logger import bec_logger
logger = bec_logger.logger
class JungfrauJochClientError(Exception):
"""Base class for exceptions in this module."""
class DetectorState(enum.StrEnum):
"""Detector states for Jungfrau Joch detector
['Inactive', 'Idle', 'Busy', 'Measuring', 'Pedestal', 'Error']
"""
INACTIVE = "Inactive"
IDLE = "Idle"
BUSY = "Busy"
MEASURING = "Measuring"
PEDESTAL = "Pedestal"
ERROR = "Error"
class ResponseWaitDone(enum.IntEnum):
"""Response state for Jungfrau Joch detector wait till done"""
DETECTOR_IDLE = 200
TIMEOUT_PARAM_OUT_OF_RANGE = 400
JUNGFRAU_ERROR = 500
DETECTOR_INACTIVE = 502
TIMEOUT_REACHED = 504
class JungfrauJochClient:
"""Thin wrapper around the Jungfrau Joch API client"""
def __init__(self, host: str = "http://sls-jfjoch-001:8080") -> None:
self._initialised = False
configuration = jfjoch_client.Configuration(host=host)
api_client = jfjoch_client.ApiClient(configuration)
self.api = jfjoch_client.DefaultApi(api_client)
@property
def initialised(self) -> bool:
"""Check if jfj is connected and ready to receive commands"""
return self._initialised
@initialised.setter
def initialised(self, value: bool) -> None:
"""Set the connected status"""
self._initialised = value
def get_jungfrau_joch_status(self) -> DetectorState:
"""Get the status of JungfrauJoch"""
return self.api.status_get().state
def connect_and_initialise(self, timeout: int = 5) -> None:
"""Check if JungfrauJoch is connected and ready to receive commands"""
status = self.api.status_get().state
if status != DetectorState.IDLE:
self.api.initialize_post()
self.wait_till_done(timeout)
self.initialised = True
def set_detector_settings(self, settings: dict | jfjoch_client.DatasetSettings) -> None:
"""Set the detector settings. JungfrauJoch must be in IDLE, Error or Inactive state.
Note, the full settings have to be provided, otherwise the settings will be overwritten with default values.
Args:
settings (dict): dictionary of settings
"""
state = self.api.status_get().state
if state not in [DetectorState.IDLE, DetectorState.ERROR, DetectorState.INACTIVE]:
raise JungfrauJochClientError(
f"Detector must be in IDLE, ERROR or INACTIVE state to set settings. Current state: {state}"
)
if isinstance(settings, dict):
settings = jfjoch_client.DatasetSettings(**settings)
self.api.config_detector_put(settings)
def set_mesaurement_settings(self, settings: dict | jfjoch_client.DatasetSettings) -> None:
"""Set the measurement settings. JungfrauJoch must be in IDLE state.
The method call is blocking and JungfrauJoch will be ready to measure after the call resolves.
Please check the DataSettings class for the available settings.
The minimum required settings are:
beam_x_pxl: StrictFloat | StrictInt,
beam_y_pxl: StrictFloat | StrictInt,
detector_distance_mm: float | int,
incident_energy_keV: float | int,
Args:
settings (dict): dictionary of settings
"""
state = self.api.status_get().state
if state != DetectorState.IDLE:
raise JungfrauJochClientError(
f"Detector must be in IDLE state to set settings. Current state: {state}"
)
if isinstance(settings, dict):
settings = jfjoch_client.DatasetSettings(**settings)
try:
res = self.api.start_post_with_http_info(dataset_settings=settings)
if res.status_code != 200:
logger.error(
f"Error while setting measurement settings {settings}, response: {res}"
)
raise JungfrauJochClientError(
f"Error while setting measurement settings {settings}, response: {res}"
)
except Exception as e:
logger.error(
f"Error while setting measurement settings {settings}. Exception raised {e}"
)
raise JungfrauJochClientError(
f"Error while setting measurement settings {settings}. Exception raised {e}"
) from e
def wait_till_done(self, timeout: int = 5) -> None:
"""Wait until JungfrauJoch is done.
Args:
timeout (int): timeout in seconds
"""
success = False
try:
response = self.api.wait_till_done_post_with_http_info(math.ceil(timeout / 2))
if response.status_code != ResponseWaitDone.DETECTOR_IDLE:
logger.info(
f"Waitin for JungfrauJoch to be done, status: {ResponseWaitDone(response.status_code)}; response msg {response}"
)
response = self.api.wait_till_done_post_with_http_info(math.floor(timeout / 2))
if response.status_code == ResponseWaitDone.DETECTOR_IDLE:
success = True
return
except Exception as e:
logger.error(f"Error while waiting for JungfrauJoch to initialise: {e}")
raise JungfrauJochClientError(
f"Error while waiting for JungfrauJoch to initialise: {e}"
) from e
else:
if success is False:
logger.error(
f"Failed to initialise JungfrauJoch with status: {response.status_code}; response msg {response}"
)
raise JungfrauJochClientError(
f"Failed to initialise JungfrauJoch with status: {response.status_code}; response msg {response}"
)

View File

@@ -1,12 +1,15 @@
import functools
import socket
import threading
import time
from ophyd_devices.utils.controller import threadlocked
from ophyd_devices.utils.socket import raise_if_disconnected
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal, SignalRO
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 typeguard import typechecked
def channel_checked(fcn):
@@ -14,81 +17,32 @@ def channel_checked(fcn):
@functools.wraps(fcn)
def wrapper(self, *args, **kwargs):
# pylint: disable=protected-access
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 NpointError(Exception):
"""
Base class for Npoint errors.
"""
class NPointController:
_controller_instance = None
class NPointController(Controller):
"""
Controller for nPoint piezo stages. This class inherits from the Controller class
and provides a singleton interface to the nPoint controller.
"""
NUM_CHANNELS = 3
_axes_per_controller = 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
@@ -98,54 +52,13 @@ class NPointController:
if not self.connected:
print("npoint controller is currently disabled.")
return
print(f"Connected to controller at {self._server_and_port_name}")
print(f"Connected to controller at {self._socket_host}:{self._socket_port}")
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)]
)
for ii in range(self._axes_per_controller):
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.
@@ -174,7 +87,7 @@ class NPointController:
return device_range
@channel_checked
def _get_current_pos(self, channel: int) -> float:
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"])
@@ -187,7 +100,7 @@ class NPointController:
return pos
@channel_checked
def _set_target_pos(self, channel: int, pos: float) -> None:
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"])
@@ -199,7 +112,7 @@ class NPointController:
self._put(send_buffer)
@channel_checked
def _get_target_pos(self, channel: int) -> float:
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"])
@@ -214,17 +127,17 @@ class NPointController:
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"])
# # 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)
# if enable:
# data = ["00"] * 3 + ["01"]
# else:
# data = ["00"] * 4
# send_buffer = self.__write_single_location_buffer(addr, data)
self._put(send_buffer)
# self._put(send_buffer)
@channel_checked
def _get_servo(self, channel: int) -> int:
@@ -250,7 +163,7 @@ class NPointController:
"""
buffer = b"".join([bytes.fromhex(m) for m in buffer])
self.socket.put(buffer)
self.sock.put(buffer)
@threadlocked
def _put_and_receive(self, msg_hex_list: list) -> list:
@@ -264,8 +177,8 @@ class NPointController:
"""
buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list])
self.socket.put(buffer)
recv_msg = self.socket.receive()
self.sock.put(buffer)
recv_msg = self.sock.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
@@ -293,9 +206,9 @@ class NPointController:
raise RuntimeError("Connection failure. Please restart the controller.")
def _check_channel(self, channel: int) -> None:
if channel >= self.NUM_CHANNELS:
if channel >= self._axes_per_controller:
raise ValueError(
f"Channel {channel+1} exceeds the available number of channels ({self.NUM_CHANNELS})"
f"Channel {channel+1} exceeds the available number of channels ({self._axes_per_controller})"
)
@staticmethod
@@ -391,155 +304,285 @@ class NPointController:
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
class NpointSignalBase(SocketSignal):
"""
Base class for nPoint signals.
"""
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 __init__(self, signal_name, **kwargs):
self.signal_name = signal_name
super().__init__(**kwargs)
self.controller: NPointController = self.parent.controller
self.sock = self.parent.controller.sock
def show_all(self) -> None:
self.controller.show_all()
@raise_if_disconnected
def get(self) -> float:
"""Get current position for this channel.
class NpointSignalRO(NpointSignalBase):
"""
Base class for read-only signals.
"""
Raises:
RuntimeError: Raised if channel is not connected.
def __init__(self, signal_name, **kwargs):
super().__init__(signal_name, **kwargs)
self._metadata["write_access"] = False
Returns:
float: position
@threadlocked
def _socket_set(self, val):
raise ReadOnlyError("Read-only signals cannot be set")
class NpointReadbackSignal(NpointSignalRO):
"""
Signal to read the current position of an nPoint piezo stage.
"""
@threadlocked
def _socket_get(self):
return self.controller.get_current_pos(self.parent.axis_Id_numeric) * self.parent.sign
class NpointSetpointSignal(NpointSignalBase):
"""
Signal to set the target position of an nPoint piezo stage.
"""
setpoint = 0
@threadlocked
def _socket_get(self):
return self.controller.get_target_pos(self.parent.axis_Id_numeric) * self.parent.sign
@threadlocked
def _socket_set(self, val):
target_val = val * self.parent.sign
self.setpoint = target_val
return self.controller.set_target_pos(
self.parent.axis_Id_numeric, target_val * self.parent.sign
)
class NpointMotorIsMoving(SignalRO):
"""
Signal to indicate whether the motor is currently moving or not.
"""
def set_motor_is_moving(self, value: int) -> None:
"""
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).
Set the motor_is_moving signal to the specified value.
Args:
pos (float): New target position
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
None
value (int): 1 if the motor is moving, 0 otherwise.
"""
self.controller._set_target_pos(self.channel, pos)
time.sleep(self.settling_time)
self._readback = value
class NPointAxis(Device, PositionerBase):
"""
NPointAxis class, which inherits from Device and PositionerBase. This class
represents an axis of an nPoint piezo stage and provides the necessary
functionality to move the axis and read its current position.
"""
USER_ACCESS = ["controller"]
readback = Cpt(NpointReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(NpointSetpointSignal, signal_name="setpoint")
motor_is_moving = Cpt(NpointMotorIsMoving, value=0, kind="normal")
settling_time = Cpt(Signal, value=0.1, 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=8085,
limits=None,
sign=1,
socket_cls=SocketIO,
tolerance: float = 0.05,
**kwargs,
):
self.controller = NPointController(
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 = tolerance
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 connected(self) -> bool:
return self.controller.connected
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 servo(self) -> int:
"""Get servo status
def move(self, position, wait=True, **kwargs):
"""Move to a specified position, optionally waiting for motion to
complete.
Raises:
RuntimeError: Raised if channel is not connected.
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:
int: Servo status
Returns
-------
status : MoveStatus
Raises
------
TimeoutError
When motion takes longer than `timeout`
ValueError
On invalid positions
RuntimeError
If motion fails other than timing out
"""
return self.controller._get_servo(self.channel)
self._started_moving = False
timeout = kwargs.pop("timeout", 10)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)
@servo.setter
@raise_if_disconnected
@typechecked
def servo(self, val: bool) -> None:
"""Set servo status
def move_and_finish():
self.motor_is_moving.set_motor_is_moving(1)
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
time.sleep(self.settling_time.get())
self.motor_is_moving.set_motor_is_moving(0)
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
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 the axis_Id_alpha.
"""
return self._axis_Id_alpha
@axis_Id.setter
def axis_Id(self, val: str):
"""
Set the axis_Id_alpha and axis_Id_numeric based on the alpha value.
Args:
val (bool): Servo status
Raises:
RuntimeError: Raised if channel is not connected.
Returns:
None
val (str): Single-character axis identifier.
"""
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()
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:
return self.get_target_pos()
raise TypeError(f"Expected value of type str but received {type(val)}")
def within_limits(self, pos: float) -> bool:
return pos > self.low_limit and pos < self.high_limit
@property
def axis_Id_numeric(self):
"""
Return the numeric value of the axis_Id.
"""
return self._axis_Id_numeric
def move(self, position: float, wait=True) -> None:
self.set(position)
@axis_Id_numeric.setter
def axis_Id_numeric(self, val: int):
"""
Set the axis_Id_numeric and axis_Id_alpha based on the numeric value.
Args:
val (int): Numeric axis identifier.
"""
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 "um"
def stage(self) -> list[object]:
return super().stage()
def unstage(self) -> list[object]:
return super().unstage()
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")
npx = NPointAxis(axis_Id="A", name="npx", host="nPoint000003.psi.ch", port=23)
npy = NPointAxis(axis_Id="B", name="npy", host="nPoint000003.psi.ch", port=23)
npx.controller.on()
print("socket is open, axis is ready!")
npx.move(10)
print(npx.read())
npx.controller.off()

View File

View File

@@ -1,9 +1,10 @@
import os
import time
from bec_lib import MessageEndpoints, bec_logger, messages
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal
from ophyd import Device, EpicsSignal, EpicsSignalRO, Signal
logger = bec_logger.logger

View File

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

View File

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -10,7 +10,7 @@ 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 (
from csaxs_bec.devices.omny.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilController,
@@ -141,7 +141,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced):
class FlomniGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller"]
USER_ACCESS = ["controller", "drive_axis_to_limit"]
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
@@ -337,6 +337,18 @@ class FlomniGalilMotor(Device, PositionerBase):
def unstage(self) -> list[object]:
return super().unstage()
def drive_axis_to_limit(self, direction: str) -> None:
"""
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)

View File

@@ -1,4 +1,3 @@
import functools
import threading
import time
@@ -7,21 +6,19 @@ 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 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 (
from csaxs_bec.devices.omny.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilCommunicationError,
GalilController,
GalilError,
GalilMotorIsMoving,
GalilMotorResolution,
GalilReadbackSignal,
GalilSetpointSignal,
GalilSignalRO,
retry_once,
)
@@ -47,7 +44,7 @@ class FuprGalilController(GalilController):
raise NotImplementedError("This function is not implemented for the FuprGalilController.")
class FuprGalilReadbackSignal(GalilReadbackSignal):
class FuprGalilReadbackSignal(GalilSignalRO):
@retry_once
@threadlocked
def _socket_get(self) -> float:

View File

@@ -0,0 +1,472 @@
"""
This module contains the base class for Galil controllers as well as the signals used for Galil devices.
"""
import functools
import time
from bec_lib import bec_logger
from ophyd.utils import ReadOnlyError
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketSignal
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):
"""
Base class for Galil controllers. This class provides the basic functionality for Galil controllers and should be subclassed for specific devices.
"""
_axes_per_controller = 8
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",
"is_motor_on",
"all_axes_referenced",
]
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
@threadlocked
def socket_put(self, val: str) -> None:
self.sock.put(f"{val}\r".encode())
@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 stop_all_axes(self) -> str:
if not self.is_thread_active(1):
return self.socket_put_and_receive("XQ#STOP,1")
else:
return ":"
def get_digital_input(self, channel):
return bool(float(self.socket_put_and_receive(f"MG @IN[{channel}]").strip()))
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 folerr_status(self, axis_Id_numeric) -> bool:
return bool(float(self.socket_put_and_receive(f"MG folaxerr[{axis_Id_numeric}]").strip()))
def motor_temperature(self, axis_Id_numeric) -> float:
#this is only valid for omny. consider moving to ogalil
voltage = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
voltage2 = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
if voltage2 < voltage:
voltage = voltage2
# convert from [-10,10]V to [0,300]degC
temperature_degC = round((voltage+10.0) / 20.0 * 300.0, 1)
#the motors of the parking station have a different offset
#the range is reduced, so if at the limit, we show an extreme value
if self.sock.port == 8082:
#controller 2
if axis_Id_numeric == 6:
temperature_degC = round((voltage+10.0-11.4) / 20.0 * 300.0, 1)
if voltage > 9.9:
temperature_degC = 300
if axis_Id_numeric == 7:
temperature_degC = round((voltage+.0-12) / 20.0 * 300.0, 1)
if voltage > 9.9:
temperature_degC = 300
return temperature_degC
def all_axes_referenced(self) -> bool:
"""
Check if all axes are referenced.
"""
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
def _omny_get_microstep_position(self,axis_Id):
return float(self.socket_put_and_receive(f"MG _TD{axis_Id}").strip())
def _omny_get_reference_limit(self,axis_Id):
get_axis_no = float(self.socket_put_and_receive(f"MG frmmv").strip())
if(get_axis_no>0):
reference_is_before = float(self.socket_put_and_receive(f"MG _FL{axis_Id}").strip())
elif(get_axis_no<0):
reference_is_before = float(self.socket_put_and_receive(f"MG _BL{axis_Id}").strip())
else:
reference_is_before = 0
return reference_is_before
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str, verbose=0) -> 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.1)
self.socket_put_confirmed("XQ#FES")
time.sleep(0.1)
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.01)
if verbose:
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}", scope="drive axis to limit", show_asap=True)
time.sleep(0.5)
# 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.")
else:
print("Limit reached.")
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 find_reference(self, axis_Id_numeric: int, verbose=0, raise_error = 1) -> None:
"""
Find the reference of an axis.
Args:
axis_Id_numeric (int): Axis number
"""
time.sleep(0.1)
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
self.socket_put_and_receive("XQ#NEWPAR")
time.sleep(0.1)
self.socket_put_confirmed("XQ#FRM")
time.sleep(0.1)
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.1)
if verbose:
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}", scope="find axis reference", show_asap=True)
time.sleep(0.5)
if not self.axis_is_referenced(axis_Id_numeric):
if raise_error:
raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.")
else:
print(f"Failed to find reference of axis {axis_Id_numeric}.")
else:
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
print(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}"
field_names = [
"Axis",
"Name",
"Referenced",
"Motor On",
"Limits",
"Position",
]
# in case of OMNY
if self.sock.host == "mpc3217.psi.ch":
field_names.append("Temperature")
field_names.append("FolErr")
t.field_names = field_names
for ax in range(self._axes_per_controller):
axis = self._axis[ax]
if axis is not None:
if self.sock.host == "mpc3217.psi.ch":
#case of omny. possibly consider moving to ogalil
motor_on = self.is_motor_on(axis.axis_Id)
if motor_on == True:
motor_on = self.WARNING + "ON" + self.ENDC
else:
motor_on = "OFF"
folerr_status = self.folerr_status(axis.axis_Id_numeric)
if folerr_status == True:
folerr_status = self.WARNING + "True" + self.ENDC
else:
folerr_status = "False"
position = axis.readback.read().get(axis.name).get("value")
position = f'{position:.3f}'
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
axis.name,
self.axis_is_referenced(axis.axis_Id_numeric),
motor_on,
self.get_motor_limit_switch(axis.axis_Id),
position,
self.motor_temperature(axis.axis_Id_numeric),
self.folerr_status(axis.axis_Id_numeric),
]
)
else:
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
axis.name,
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()
self.show_status_other()
def show_status_other(self) -> None:
"""
Show additional device-specific status information.
Override in subclasses.
"""
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
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()
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)
#in the case of lamni, consider moving to lgalil
if self.parent.axis_Id_numeric == 2 and self.controller.sock.host == "mpc2680.psi.ch":
try:
rt = self.parent.device_manager.devices[self.parent.rt]
if rt.enabled:
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")
except KeyError:
logger.warning(
"RT is disabled. Failed to update RT angle interferometer status to galil."
)
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()

View File

@@ -0,0 +1,313 @@
import threading
import time
import numpy as np
from bec_lib.logger 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.omny.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilController,
GalilMotorIsMoving,
GalilMotorResolution,
GalilSetpointSignal,
GalilSignalRO,
retry_once,
)
logger = bec_logger.logger
class LamniGalilController(GalilController):
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",
]
def show_status_other(self):
if self.get_digital_input(5):
print("Emergency stop is not pushed.")
else:
print("Emergency stop is pushed.")
if self.get_digital_input(6):
print("Driver axis 2 error.")
if self.get_digital_input(13):
print("No air pressure at inner rotation.")
else:
print("There is air pressure at the inner rotation.")
if self.get_digital_input(14):
print("No air pressure at outer rotation axial.")
else:
print("There is air pressure at the outer rotation axial.")
if self.get_digital_input(15):
print("No air pressure at outer rotation radial.")
else:
print("There is air pressure at the outer rotation radial.")
swver = float(self.socket_put_and_receive("MGswver"))
print(f"Lgalil LAMNI firmware version {swver:2.0f}.")
def lamni_lights_off(self):
self.socket_put_confirmed("SB1")
def lamni_lights_on(self):
self.socket_put_confirmed("CB1")
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("MG@OUT[9]"))
air_off = bool(self.socket_put_and_receive("MG@OUT[13]"))
return rt_not_blocked_by_galil and air_off
class LamniGalilReadbackSignal(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.rtx]
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 LamniGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller", "drive_axis_to_limit", "find_reference"]
readback = Cpt(LamniGalilReadbackSignal, 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 = LamniGalilController(
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("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("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 find_reference(self):
"""
Find the reference of the axis.
"""
self.controller.find_reference(self.axis_Id_numeric)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def drive_axis_to_limit(self, direction: str) -> None:
"""
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)

View File

@@ -0,0 +1,515 @@
import functools
import threading
import time
import urllib.request
import xml.etree.ElementTree as ET
import numpy as np
from bec_lib.logger 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.omny.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilCommunicationError,
GalilController,
GalilError,
GalilMotorIsMoving,
GalilSetpointSignal,
GalilSignalRO,
)
logger = bec_logger.logger
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 GalilMotorResolution(GalilSignalRO):
@retry_once
@threadlocked
def _socket_get(self):
if self.controller.sock.port == 8083 and self.parent.axis_Id_numeric == 2:
# rotation stage
return 89565.8666667
else:
return 51200
class OMNYGalilReadbackSignal(GalilSignalRO):
previous_rotation_angle = 0
ignore_glitch = True
@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()
#here we introduce an offset of 25 to the rotation axis
#when setting a position this is taken into account in the controller
#that way we just do tomography from 0 to 180 degrees
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
return (current_pos / step_mm)+25
else:
return current_pos / step_mm
def read(self):
self._metadata["timestamp"] = time.time()
val = super().read()
#if reading rotation stage angle
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
current_readback_value = val[self.parent.name]["value"]
#print (f"previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}.")
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
message = f"Glitch detected in rotation stage. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}."
print(message)
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
val = super().read()
current_readback_value = val[self.parent.name]["value"]
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
message = f"Glitch detected in rotation stage second read. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}. Disabling the controller."
print(message)
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
self.parent.device_manager.devices["osamroy"].obj.controller.socket_put_confirmed("allaxref=0")
self.parent.device_manager.devices["osamroy"].obj.enabled = False
return val
self.previous_rotation_angle = current_readback_value
try:
rt = self.parent.device_manager.devices["rtx"]
if rt.enabled:
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]-25+54)
except KeyError:
logger.warning("Failed to set RT value during ogalil readback.")
return val
class OMNYGalilController(GalilController):
USER_ACCESS = [
"describe",
"show_running_threads",
"galil_show_all",
"socket_put_and_receive",
"socket_put_confirmed",
"get_motor_limit_switch",
"is_motor_on",
"all_axes_referenced",
"_ogalil_switchsocket",
"_ogalil_switchsocket_switch_all_on",
"_ogalil_switchsocket_status",
"_ogalil_switchsocket_are_all_on",
"_ogalil_folerr_not_ignore",
]
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
def on(self) -> None:
"""Open a new socket connection to the controller"""
self._ogalil_switchsocket_switch_all_on()
time.sleep(0.3)
super().on()
def _ogalil_switchsocket(self, number: int, switch: bool):
# number is socket number ranging from 1 to 4
# switch is either 0 or 1
if number not in range(1, 5):
raise Exception("Socket number ranges from 1 to 4")
else:
contents = urllib.request.urlopen(
f"http://mpc3217:8091/netio.cgi?pass=24A42C3929C5&output{number}={switch}"
).read()
# print(contents)
if b"OK" in contents:
print(f"Controller number switchsocket {number} is now {switch}")
return 1
else:
print(f"Failed to switch controller number {number}")
return 0
def _ogalil_switchsocket_are_all_on(self):
if self._ogalil_switchsocket_status() == [1, 1, 1, 1]:
return 1
else:
return 0
def _ogalil_tempdisabledebug(self):
# sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],"WH\r\n") < 1) {
# ret_str = sock_get(_ogalil_debugging_host_and_port_str[ogalil_no][],_ogalil_prompt_str)
# printf("%s _ogalil_redirect_debug_output(%d): expected \"IHx\" received \"%s\"\n",\
# cmd_str = sprintf("CF %s\r\n",substr(ret_str,3,1))
# if (sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],cmd_str) < 1) {
# cmd_str = sprintf("CW 2\r\n")
# if (sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],cmd_str) < 1) {
print("not yet implemented")
return 0
def _ogalil_folerr_reset_and_ignore(self, axis_id_numeric: int) -> None:
self.socket_put_confirmed(f"folaxerr[{axis_id_numeric}]=0")
self.socket_put_confirmed("folerr=0")
self.socket_put_confirmed("IgNoFol=1")
self.socket_put_confirmed("XQ#STOP,1")
def _ogalil_set_axis_to_pos_wo_reference_search(self, axis_id_numeric, axis_id, pos_mm, motor_resolution, motor_sign):
self.socket_put_confirmed("IgNoFol=1")
# pos_mm = pos_encoder / motor_resolution
pos_encoder = pos_mm * motor_resolution * motor_sign
#print(motor_resolution)
self.socket_put_confirmed(f"DE{axis_id}={pos_encoder:.0f}")
self.socket_put_confirmed(f"DP{axis_id}=_TP{axis_id}*ratio[{axis_id_numeric:.0f}]")
self.socket_put_confirmed(f"folaxerr[{axis_id_numeric}]=0")
self.socket_put_confirmed(f"axisref[{axis_id_numeric}]=1")
self.socket_put_confirmed("folerr=0")
self._ogalil_folerr_not_ignore()
def _ogalil_folerr_not_ignore(self):
self.socket_put_confirmed("IgNoFol=0")
def _ogalil_switchsocket_switch_all_on(self):
if not self._ogalil_switchsocket_are_all_on():
for j in range(1, 5):
self._ogalil_switchsocket(j, 1)
time.sleep(0.4)
def _ogalil_switchsocket_status(self):
contents = urllib.request.urlopen("http://mpc3217:8091/netio.xml").read()
root = ET.fromstring(contents)
returnvalue = []
for j in range(0, 4):
status = int(root[1][j][2].text)
returnvalue.append(int(root[1][j][2].text))
if status:
print(f"Controller {j+1} is ON")
else:
print(f"Controller {j+1} is OFF")
return returnvalue
def show_status_other(self):
swver = float(self.socket_put_and_receive("MGswver"))
allaxref = float(self.socket_put_and_receive("MGallaxref"))
tempab = float(self.socket_put_and_receive("MGtempab"))
timeab = float(self.socket_put_and_receive("MGtimeab"))
IgNoFol = float(self.socket_put_and_receive("MGIgNoFol"))
print(
f"OMNY galil firmware {swver:2.0f}, TempAbort: {tempab:1.0f}, Allaxref: {allaxref:1.0f}, TimeAbort: {timeab:1.0f}, Ignore Folerr: {IgNoFol:1.0f}\n"
)
if self.sock.port == 8083:
self._ogalil_switchsocket_status()
class OMNYGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller", "find_reference", "omny_osamx_to_scan_center", "drive_axis_to_limit", "_ogalil_folerr_reset_and_ignore", "_ogalil_set_axis_to_pos_wo_reference_search", "get_motor_limit_switch", "axis_is_referenced", "get_motor_temperature", "folerr_status"]
readback = Cpt(OMNYGalilReadbackSignal, 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="mpc3217.psi.ch",
port=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = OMNYGalilController(
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("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("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 _ogalil_folerr_reset_and_ignore(self):
# pylint: disable=protected-access
self.controller._ogalil_folerr_reset_and_ignore(self.axis_Id_numeric)
def _ogalil_set_axis_to_pos_wo_reference_search(self, pos_mm):
motor_resolution = self.motor_resolution.get()
self.controller._ogalil_set_axis_to_pos_wo_reference_search(self.axis_Id_numeric, self.axis_Id, pos_mm, motor_resolution, self.sign)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def find_reference(self, raise_error=1):
"""
Find the reference of the axis.
"""
verbose=1
self.controller.find_reference(self.axis_Id_numeric, verbose, raise_error)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def drive_axis_to_limit(self, direction: str) -> None:
"""
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction, verbose=1)
#now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
def get_motor_limit_switch(self) -> list:
"""
Get status of the motor limit switches
"""
return self.controller.get_motor_limit_switch(self.axis_Id)
def get_motor_temperature(self) -> float:
"""
Get motor temperature
"""
return self.controller.motor_temperature(self.axis_Id_numeric)
def axis_is_referenced(self) -> bool:
"""
check if an axis is referenced
"""
return self.controller.axis_is_referenced(self.axis_Id_numeric)
def _get_user_param_safe(self, device, var):
param = self.device_manager.devices[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 omny_osamx_to_scan_center(self, cenx):
if self.controller.sock.port == 8082 and self.axis_Id_numeric == 0:
# get last setpoint
osamx = self.device_manager.devices["osamx"]
osamx_current_setpoint = osamx.obj.readback.get()
omny_samx_in = self._get_user_param_safe("osamx","in")
if np.fabs(osamx_current_setpoint-(omny_samx_in+cenx/1000)) > 0.025:
message=f"Moving osamx to scan center. new osamx target {omny_samx_in+cenx/1000:.3f}."
logger.info(message)
osamx.read_only = False
#osamx.controller.("osamx", "controller.socket_put_confirmed('axspeed[0]=1000')")
osamx.set(omny_samx_in+cenx/1000)
time.sleep(0.1)
while(osamx.motor_is_moving.get()):
time.sleep(0.05)
osamx.read_only = True
time.sleep(2)
rt = self.device_manager.devices["rtx"]
if rt.enabled:
rt.obj.controller.laser_tracker_on()
rt.obj.controller.laser_tracker_check_and_wait_for_signalstrength()
def folerr_status(self) -> bool:
return self.controller.folerr_status(self.axis_Id_numeric)
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)

View File

@@ -4,7 +4,7 @@ Ophyd wrapper for the SGalil controller and stages.
## 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
from csaxs_bec.devices.omny.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

View File

@@ -0,0 +1,111 @@
import time
import datetime
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal
from prettytable import FRAME, PrettyTable
import numpy as np
class OMNYDewarError(Exception):
pass
class OMNYDewar(Device):
USER_ACCESS = [
"show_all",
"is_flow_low",
"help",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
dewar_press_0 = Cpt(
EpicsSignal, name="dewar_press_0", read_pv="XOMNY-TEMP-DEWAR-PRESS0:GET"
)
dewar_refilling = Cpt(
EpicsSignal, name="dewar_refilling", read_pv="XOMNY-TEMP-DEWAR-Refilling:GET"
)
dewar_press_1 = Cpt(
EpicsSignal, name="dewar_press_1", read_pv="XOMNY-TEMP-DEWAR-PRESS1:GET"
)
dewar_press_2 = Cpt(
EpicsSignal, name="dewar_press_2", read_pv="XOMNY-TEMP-DEWAR-PRESS2:GET"
)
dewar_flow_0 = Cpt(
EpicsSignal, name="dewar_flow_0", read_pv="XOMNY-TEMP-DEWAR-FLOW0:GET"
)
dewar_valvepos = Cpt(
EpicsSignal, name="dewar_valvepos", read_pv="XOMNY-TEMP-DEWAR-ValvePos:GET"
)
dewar_supply_voltage = Cpt(
EpicsSignal, name="dewar_supply_voltage", read_pv="XOMNY-TEMP-DEWAR-SupplyVoltage:GET"
)
dewar_uptime_h = Cpt(
EpicsSignal, name="dewar_uptime_h", read_pv="XOMNY-TEMP-DEWAR-UpH:GET"
)
dewar_uptime_m = Cpt(
EpicsSignal, name="dewar_uptime_m", read_pv="XOMNY-TEMP-DEWAR-UpM:GET"
)
dewar_uptime_s = Cpt(
EpicsSignal, name="dewar_uptime_s", read_pv="XOMNY-TEMP-DEWAR-UpS:GET"
)
dewar_valve_movements_h = Cpt(
EpicsSignal, name="dewar_valve_movements_h", read_pv="XOMNY-TEMP-DEWAR-MovH:GET"
)
dewar_valve_movements_m = Cpt(
EpicsSignal, name="dewar_valve_movements_m", read_pv="XOMNY-TEMP-DEWAR-MovM:GET"
)
dewar_valve_movements_s = Cpt(
EpicsSignal, name="dewar_valve_movements_s", read_pv="XOMNY-TEMP-DEWAR-MovS:GET"
)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.dewar_flow_0.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 is_flow_low(self):
if(float(self.dewar_flow_0.get())<3.8):
return True
else:
return False
def show_all(self):
red = "\x1b[91m"
white = "\x1b[0m"
print("OMNY Dewar Status")
print(f" DewarPressure: {float(self.dewar_press_0.get()):.0f} mbar")
print(f" Dewar Refilling: {float(self.dewar_refilling.get()):.0f}")
print(f" LN2flow In Pressure: {float(self.dewar_press_1.get()):.0f} mbar")
print(f" LN2flow Out Pressure: {float(self.dewar_press_2.get()):.0f} mbar")
print(f" LN2flow In Flow Rate: {float(self.dewar_flow_0.get()):.1f} l/s")
if self.is_flow_low():
print(red + "This flow rate is LOW. Increase the LN2flow In Pressure" + white)
print(f" Valve Opening (0-1): {float(self.dewar_valvepos.get()):.2f}")
print(f" Valve Supply Voltage: {float(self.dewar_supply_voltage.get()):.2f} V")
print(f" Uptime of System: {float(self.dewar_uptime_h.get()):2.0f}:{float(self.dewar_uptime_m.get()):2.0f}:{float(self.dewar_uptime_s.get()):2.0f}")
print(f" Active Valve Movements: {float(self.dewar_valve_movements_h.get()):2.0f}:{float(self.dewar_valve_movements_m.get()):2.0f}:{float(self.dewar_valve_movements_s.get()):2.0f}")
def help(self):
print("Help for OMNY Dewar:")
print("This device shows an overview of the dewar status. Use show_all()")

View File

@@ -22,13 +22,16 @@ class OMNYSampleStorage(Device):
"set_sample_in_samplestage",
"unset_sample_in_samplestage",
"get_sample_name_in_samplestage",
"get_sample_name_in_gripper",
"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",
"help",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
@@ -59,7 +62,7 @@ class OMNYSampleStorage(Device):
parking_placed = Dcpt(parking_placed)
sample_placed = {
f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {})
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)
@@ -136,20 +139,28 @@ class OMNYSampleStorage(Device):
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)
elif container == "O":
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, shuttle: str, slot_nr: int) -> bool:
def unset_sample_slot(self, container: str, slot_nr: int) -> bool:
if slot_nr > 20:
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
if shuttle == "A":
if container == "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":
elif container == "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":
elif container == "C":
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set("-")
elif container == "O":
getattr(self.sample_placed, f"sample{slot_nr}").set(0)
getattr(self.sample_names, f"sample{slot_nr}").set("-")
def set_shuttle_slot(self, container: str, slot_nr: int) -> bool:
if slot_nr > 6:
@@ -227,11 +238,25 @@ class OMNYSampleStorage(Device):
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()
t.title = "Shuttle " + ch
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 = []
@@ -260,8 +285,24 @@ class OMNYSampleStorage(Device):
row = []
row.extend([f"Position {i:3d}"])
if self.is_sample_slot_used("O", i):
row.extend(self.get_sample_name("O", i))
name = self.get_sample_name("O", i)
row.extend([name])
else:
row.extend(["free"])
t.add_row(row)
print(t)
print("Use dev.omny_samples.help() for assistance.")
def help(self):
print("Help for OMNY sample storage:")
print(" To get an overview use dev.omny_samples.show_all()")
print(" Modify a slot:")
print(" dev.omny_samples.unset_sample_slot('system',position)")
print(" dev.omny_samples.set_sample_slot('system',position,'name')")
print(" system can be A, B, C, O")
print(" dev.omny_samples.set_sample_in_gripper('name') / unset_sample_in_gripper()")
print(" dev.omny_samples.set_sample_in_samplestage('name'), unset_sample_in_samplestage()")
print(" dev.omny_samples.set_shuttle_slot(container, slot_nr) / unset_shuttle_slot(slot_nr)")
print(" dev.omny_samples.set_shuttle_slot('A',2)")
print(" omny.otransfer_help()")

View File

@@ -0,0 +1,260 @@
import time
import datetime
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal
from prettytable import FRAME, PrettyTable
import numpy as np
class OMNYTemperaturesError(Exception):
pass
class OMNYTemperatures(Device):
USER_ACCESS = [
"temperature_controller_used_get_name_and_values",
"set_setpoint",
"show_all",
"help",
"_set_TEMP_default_setpoints",
"temperature_controller_TEMP_running",
"temperature_controller_CRYO_running",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
temperature = {
f"temperature{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET", {}) for i in range(1, 49)
}
temperature = Dcpt(temperature)
temperature_setpoint = {
f"temperature_setpoint{i}": (EpicsSignal, f"XOMNY-TEMP{i}:SET", {}) for i in range(1, 49)
}
temperature_setpoint = Dcpt(temperature_setpoint)
temperature_unit = {
f"temperature_unit{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET.EGU", {}) for i in range(1, 49)
}
temperature_unit = Dcpt(temperature_unit)
temperature_alarmstate = {
f"temperature_alarmstate{i}": (EpicsSignal, f"XOMNY-TEMP{i}:ALARM", {}) for i in range(1, 49)
}
temperature_alarmstate = Dcpt(temperature_alarmstate)
temperature_names = {
f"temperature_name{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET.DESC", {"string": True})
for i in range(1, 49)
}
temperature_names = Dcpt(temperature_names)
temperature_update_time = Cpt(
EpicsSignal, name="temperature_update_time", read_pv="XOMNY-TEMP:UPDATED.VAL"
)
cryo_temperature = {
f"cryo_temperature{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:GET.VAL", {}) for i in range(1,5)
}
cryo_temperature = Dcpt(cryo_temperature)
cryo_temperature_setpoint = {
f"cryo_temperature_setpoint{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:SET.VAL", {}) for i in range(1,5)
}
cryo_temperature_setpoint = Dcpt(cryo_temperature_setpoint)
cryo_temperature_name = {
f"cryo_temperature_name{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:GET.DESC", {}) for i in range(1,5)
}
cryo_temperature_name = Dcpt(cryo_temperature_name)
cryo_temperature_update_time = Cpt(
EpicsSignal, name="cryo_temperature_update_time", read_pv="XOMNY-TEMP-CRYO:UPDATED.VAL"
)
cryo_temperature_closed_loop = Cpt(
EpicsSignal, name="cryo_temperature_closed_loop", read_pv="XOMNY-TEMP-CRYO:CLOSEDLOOP.VAL"
)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.temperature.temperature1.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_setpoint(self, type: str, controller_nr: int, temperature: float):
if type == "TEMP":
getattr(self.temperature_setpoint, f"temperature_setpoint{controller_nr}").set(temperature)
elif type == "CRYO":
getattr(self.cryo_temperature_setpoint, f"cryo_temperature_setpoint{controller_nr}").set(temperature)
else:
raise OMNYTemperaturesError("invalid type")
def _set_TEMP_default_setpoints(self):
self.set_setpoint("TEMP",8,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",9,23)
time.sleep(0.1)
self.set_setpoint("TEMP",10,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",16,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",17,23)
time.sleep(0.1)
self.set_setpoint("TEMP",18,26)
time.sleep(0.1)
self.set_setpoint("TEMP",19,26)
time.sleep(0.1)
self.set_setpoint("TEMP",20,26)
time.sleep(0.1)
self.set_setpoint("TEMP",21,23)
time.sleep(0.1)
self.set_setpoint("TEMP",22,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",23,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",24,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",25,25)
time.sleep(0.1)
self.set_setpoint("TEMP",27,25)
time.sleep(0.1)
self.set_setpoint("TEMP",28,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",29,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",30,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",31,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",35,25)
time.sleep(0.1)
self.set_setpoint("TEMP",36,25)
time.sleep(0.1)
self.set_setpoint("TEMP",37,25)
time.sleep(0.1)
self.set_setpoint("TEMP",38,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",39,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",41,30)
time.sleep(0.1)
self.set_setpoint("TEMP",42,25)
time.sleep(0.1)
self.set_setpoint("TEMP",44,25)
time.sleep(0.1)
self.set_setpoint("TEMP",45,-199.9)
time.sleep(0.1)
self.set_setpoint("TEMP",46,73.2)
time.sleep(0.1)
self.set_setpoint("TEMP",47,73.2)
time.sleep(0.1)
def temperature_controller_TEMP_running(self):
time_diff = np.fabs(float(self.temperature_update_time.get()) - time.time())
if time_diff > 600:
return False
else:
return True
def temperature_controller_CRYO_running(self):
time_diff = np.fabs(float(self.cryo_temperature_update_time.get()) - time.time())
if time_diff > 600:
return False
else:
return True
def temperature_controller_used_get_name_and_values(self, type: str, controller_nr: int) -> bool:
if type == "TEMP":
controller_name = str(getattr(self.temperature_names, f"temperature_name{controller_nr}").get())
if controller_name == '-' or controller_name == 'NOT INITIALIZED':
return (False, "none", 0, 0, "none", False, False)
else:
temperature = float(getattr(self.temperature, f"temperature{controller_nr}").get())
setpoint = float(getattr(self.temperature_setpoint, f"temperature_setpoint{controller_nr}").get())
unit = str(getattr(self.temperature_unit, f"temperature_unit{controller_nr}").get())
alarmstate = bool(getattr(self.temperature_alarmstate, f"temperature_alarmstate{controller_nr}").get())
controller_running = self.temperature_controller_TEMP_running()
return (True, controller_name, temperature, setpoint, unit, alarmstate,controller_running)
elif type == "CRYO":
controller_name = str(getattr(self.cryo_temperature_name, f"cryo_temperature_name{controller_nr}").get())
temperature = float(getattr(self.cryo_temperature, f"cryo_temperature{controller_nr}").get())
setpoint = float(getattr(self.cryo_temperature_setpoint, f"cryo_temperature_setpoint{controller_nr}").get())
unit = 'K'
alarmstate = False
controller_running = self.temperature_controller_CRYO_running()
return (True, controller_name, temperature, setpoint, unit, alarmstate,controller_running)
else:
raise OMNYTemperaturesError("invalid type")
def show_all(self):
red = "\x1b[91m"
white = "\x1b[0m"
t = PrettyTable()
t.clear()
t.title = "OMNY Temperature Controllers"
t.field_names = ["Channel","Name","Temperature","Setpoint","Unit","AlarmState"]
for i in range (1,49):
controller_status = self.temperature_controller_used_get_name_and_values("TEMP", i)
if controller_status[0]:
row = []
row.extend([f"{i}"])
row.extend([controller_status[1]])
row.extend([f"{controller_status[2]:.2f}"])
if (controller_status[3] < -199 and controller_status[4] == "degC") or (np.fabs(controller_status[3]-73.25) < 0.1 and controller_status[4] == "K"):
row.extend([" "])
else:
row.extend([f"{controller_status[3]:.2f}"])
row.extend([f"{controller_status[4]}"])
if controller_status[5]:
row.extend(["Alarm"])
else:
row.extend([" "])
t.add_row(row)
t.header = True
t.vrules = FRAME
print(t)
if not self.temperature_controller_TEMP_running():
print (red + "Warning: the temperature controller communication is not running" + white)
print (f"The last update was {datetime.datetime.fromtimestamp(float(self.temperature_update_time.get())).strftime('%c')}\n")
t.clear()
t.title = "OMNY Cryo Temperature Controller"
t.field_names = ["Channel","Name","Temperature","Setpoint","Unit"]
for i in range (1,5):
controller_status = self.temperature_controller_used_get_name_and_values("CRYO", i)
if controller_status[0]:
row = []
row.extend([f"{i}"])
row.extend([controller_status[1]])
row.extend([f"{controller_status[2]:.2f}"])
row.extend([f"{controller_status[3]:.2f}"])
row.extend([f"{controller_status[4]}"])
t.add_row(row)
t.header = True
t.vrules = FRAME
print(t)
if bool(self.cryo_temperature_closed_loop.get()):
print ("Cryo controller is running in closed loop.")
else:
print ("Cryo controller is running in " + red + "open" + white + " loop.")
if not self.temperature_controller_CRYO_running():
print (red + "Warning: the temperature controller communication is not running" + white)
print (f"The last update was {datetime.datetime.fromtimestamp(float(self.cryo_temperature_update_time.get())).strftime('%c')}\n")
print("Use dev.omny_temperatures.help() for assistance.")
def help(self):
print("Help for OMNY temperatures:")

View File

@@ -0,0 +1,161 @@
import time
import datetime
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal
from prettytable import FRAME, PrettyTable
import numpy as np
class OMNYVCSError(Exception):
pass
class OMNYVCS(Device):
USER_ACCESS = [
"show_all",
"valves_in_measurement_position",
"help",
]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
XOMNY_ES1_EXPMP1_VOLTAGE = Cpt(
EpicsSignal, name="XOMNY_ES1_EXPMP1_VOLTAGE", read_pv="XOMNY-ES1-EXPMP1:VOLTAGE"
)
XOMNY_ES1_EXPMP1_PRESSURE = Cpt(
EpicsSignal, name="XOMNY_ES1_EXPMP1_PRESSURE", read_pv="XOMNY-ES1-EXPMP1:PRESSURE"
)
XOMNY_ES1_PU2MT1_VOLTAGE = Cpt(
EpicsSignal, name="XOMNY_ES1_PU2MT1_VOLTAGE", read_pv="XOMNY-ES1-PU2MT1:VOLTAGE"
)
XOMNY_ES1_PU2MT1_PRESSURE = Cpt(
EpicsSignal, name="XOMNY_ES1_PU2MT1_PRESSURE", read_pv="XOMNY-ES1-PU2MT1:PRESSURE"
)
XOMNY_ES1_PU1MF1_VOLTAGE = Cpt(
EpicsSignal, name="XOMNY_ES1_PU1MF1_VOLTAGE", read_pv="XOMNY-ES1-PU1MF1:VOLTAGE"
)
XOMNY_ES1_PU1MF1_PRESSURE = Cpt(
EpicsSignal, name="XOMNY_ES1_PU1MF1_PRESSURE", read_pv="XOMNY-ES1-PU1MF1:PRESSURE"
)
XOMNY_ES1_LL1MF1_VOLTAGE = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1MF1_VOLTAGE", read_pv="XOMNY-ES1-LL1MF1:VOLTAGE"
)
XOMNY_ES1_LL1MF1_PRESSURE = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1MF1_PRESSURE", read_pv="XOMNY-ES1-LL1MF1:PRESSURE"
)
XOMNY_ES1_VV1MT1_VOLTAGE = Cpt(
EpicsSignal, name="XOMNY_ES1_VV1MT1_VOLTAGE", read_pv="XOMNY-ES1-VV1MT1:VOLTAGE"
)
XOMNY_ES1_VV1MT1_PRESSURE = Cpt(
EpicsSignal, name="XOMNY_ES1_VV1MT1_PRESSURE", read_pv="XOMNY-ES1-VV1MT1:PRESSURE"
)
XOMNY_ES1_EXPVG1_POSITION = Cpt(
EpicsSignal, name="XOMNY_ES1_EXPVG1_POSITION", read_pv="XOMNY-ES1-EXPVG1:POSITION"
)
XOMNY_ES1_WI2VG1_POSITION = Cpt(
EpicsSignal, name="XOMNY_ES1_WI2VG1_POSITION", read_pv="XOMNY-ES1-WI2VG1:POSITION"
)
XOMNY_ES1_EXPVG2_POSITION = Cpt(
EpicsSignal, name="XOMNY_ES1_EXPVG2_POSITION", read_pv="XOMNY-ES1-EXPVG2:POSITION"
)
XOMNY_ES1_LL1VG1_CLOSE = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1VG1_CLOSE", read_pv="XOMNY-ES1-LL1VG1:CLOSE"
)
XOMNY_ES1_LL1DK_VG_CLOSED = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1DK_VG_CLOSED", read_pv="XOMNY-ES1-LL1DK:VG_CLOSED"
)
XOMNY_ES1_LL1SH_VG_CLOSED = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1SH_VG_CLOSED", read_pv="XOMNY-ES1-LL1SH:VG_CLOSED"
)
XOMNY_ES1_LL1SH_MAN_POS_OK = Cpt(
EpicsSignal, name="XOMNY_ES1_LL1SH_MAN_POS_OK", read_pv="XOMNY-ES1-LL1SH:MAN_POS_OK"
)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.XOMNY_ES1_LL1SH_MAN_POS_OK.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 show_all(self):
red = "\x1b[91m"
white = "\x1b[0m"
print("OMNY Vaccum Status")
if float(self.XOMNY_ES1_EXPMP1_VOLTAGE.get()) < 0.5:
print(red + " Main chamber: Sensor failure" + white)
else:
print(f" Main chamber: {float(self.XOMNY_ES1_EXPMP1_PRESSURE.get()):.2e} mbar")
if float(self.XOMNY_ES1_PU2MT1_VOLTAGE.get()) < 0.5:
print(red + " Flight tube: Sensor failure" + white)
else:
print(f" Flight tube: {float(self.XOMNY_ES1_PU2MT1_PRESSURE.get()):.2e} mbar")
if float(self.XOMNY_ES1_PU1MF1_VOLTAGE.get()) < 0.5:
print(red+" Beamline: Sensor failure"+white)
else:
print(f" Beamline: {float(self.XOMNY_ES1_PU1MF1_PRESSURE.get()):.2e} mbar")
if float(self.XOMNY_ES1_LL1MF1_VOLTAGE.get()) < 0.5:
print(red+" LoadLock: Sensor failure"+white)
else:
print(f" LoadLock: {float(self.XOMNY_ES1_LL1MF1_PRESSURE.get()):.2e} mbar")
if float(self.XOMNY_ES1_VV1MT1_VOLTAGE.get()) < 0.5:
print(red+" Pre-pump: Sensor failure"+white)
else:
print(f" Pre-pump: {float(self.XOMNY_ES1_VV1MT1_PRESSURE.get()):.2e} mbar")
print("\nValve status")
print(f" Upstream gate valve: {str(self.XOMNY_ES1_EXPVG1_POSITION.get())}")
#printf (" Upstream window valve: %s\n", _ovcs_upstream_window_valve)
print(f" Downstream window bypass: {str(self.XOMNY_ES1_WI2VG1_POSITION.get())}")
print(f" Downstream gate valve: {str(self.XOMNY_ES1_EXPVG2_POSITION.get())}")
if self.valves_in_measurement_position():
print("The OMNY valves are in the correct configuration to perform a measurement")
else:
print(red+"The valves of the OMNY vacuum system are not in the state for measurements."+white)
print("\nLoad Lock Status")
print(f" Chamber: {str(self.XOMNY_ES1_LL1VG1_CLOSE.get())}")
print(f" Dock: {str(self.XOMNY_ES1_LL1DK_VG_CLOSED.get())}")
print(f" Shuttle: {str(self.XOMNY_ES1_LL1SH_VG_CLOSED.get())}")
print(f" Manipulator status: {str(self.XOMNY_ES1_LL1SH_MAN_POS_OK.get())}")
def valves_in_measurement_position(self):
if str(self.XOMNY_ES1_EXPVG1_POSITION.get()) == "OPEN" and str(self.XOMNY_ES1_WI2VG1_POSITION.get()) == "CLOSED" and str(self.XOMNY_ES1_EXPVG2_POSITION.get()) == "OPEN":
return True
else:
return False
def help(self):
print("Help for OMNY Vacuum System:")
print("This device shows an overview of the OMNY vacuum status. Use show_all()")

View File

@@ -3,29 +3,31 @@ import time
from typing import List
import numpy as np
from bec_lib import MessageEndpoints, bec_logger, messages
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
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 (
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
from prettytable import PrettyTable
from csaxs_bec.devices.omny.rt.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):
class RtFlomniController(Controller):
_axes_per_controller = 3
USER_ACCESS = [
"socket_put_and_receive",
"set_rotation_angle",
@@ -45,12 +47,13 @@ class RtFlomniController(RtController):
"read_ssi_interferometer",
"laser_tracker_check_signalstrength",
"laser_tracker_check_enabled",
"is_axis_moving",
]
def __init__(
self,
*,
name=None,
name="RtFlomniController",
socket_cls=None,
socket_host=None,
socket_port=None,
@@ -73,6 +76,15 @@ class RtFlomniController(RtController):
self._min_scan_buffer_reached = False
self.rt_pid_voltage = None
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("o")))
return not axis_is_on_target
@threadlocked
def stop_all_axes(self):
self.socket_put("sc")
def add_pos_to_scan(self, positions) -> None:
def send_positions(parent, positions):
parent._min_scan_buffer_reached = False
@@ -487,7 +499,7 @@ class RtFlomniController(RtController):
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=1, metadata=self.readout_metadata
).dumps(),
),
)
# while scan is running
while mode > 0:
@@ -522,13 +534,13 @@ class RtFlomniController(RtController):
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}."
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
)
def publish_device_data(self, signals, point_id):
@@ -536,7 +548,7 @@ class RtFlomniController(RtController):
MessageEndpoints.device_read("rt_flomni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
).dumps(),
),
)
def start_readout(self):
@@ -683,15 +695,16 @@ class RtFlomniMotor(Device, PositionerBase):
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
def check_value(self, value, **kwargs):
"""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}")
if low_limit < high_limit and not (low_limit <= value <= high_limit):
raise LimitError(f"position={value} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
# pylint: disable=protected-access
walk.item._metadata["connected"] = self.controller.connected
def _forward_readback(self, **kwargs):
@@ -761,7 +774,7 @@ class RtFlomniMotor(Device, PositionerBase):
def axis_Id(self, val):
if isinstance(val, str):
if len(val) != 1:
raise ValueError(f"Only single-character axis_Ids are supported.")
raise ValueError("Only single-character axis_Ids are supported.")
self._axis_Id_alpha = val
self._axis_Id_numeric = ord(val.lower()) - 97
else:
@@ -775,7 +788,7 @@ class RtFlomniMotor(Device, PositionerBase):
def axis_Id_numeric(self, val):
if isinstance(val, int):
if val > 26:
raise ValueError(f"Numeric value exceeds supported range.")
raise ValueError("Numeric value exceeds supported range.")
self._axis_Id_alpha = val
self._axis_Id_numeric = (chr(val + 97)).capitalize()
else:
@@ -789,14 +802,6 @@ class RtFlomniMotor(Device, PositionerBase):
"""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

View File

@@ -3,7 +3,8 @@ import threading
import time
import numpy as np
from bec_lib import MessageEndpoints, bec_logger, messages
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
@@ -11,6 +12,8 @@ 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 csaxs_bec.devices.omny.rt.rt_ophyd import RtCommunicationError, RtError
logger = bec_logger.logger
@@ -41,6 +44,11 @@ def retry_once(fcn):
class RtLamniController(Controller):
"""
RT-Lamni controller class for all rt devices.
"""
_axes_per_controller = 3
USER_ACCESS = [
"socket_put_and_receive",
"set_rotation_angle",
@@ -60,110 +68,25 @@ class RtLamniController(Controller):
self,
*,
name="RtLamniController",
kind=None,
parent=None,
socket=None,
socket_cls=None,
socket_host=None,
socket_port=None,
attr_name="",
parent=None,
labels=None,
kind=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")
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._min_scan_buffer_reached = False
@threadlocked
def feedback_disable(self):
@@ -175,6 +98,19 @@ class RtLamniController(Controller):
self.set_device_enabled("lopty", True)
self.set_device_enabled("loptz", True)
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("o")))
return not axis_is_on_target
@threadlocked
def stop_all_axes(self):
self.socket_put("sc")
@threadlocked
def set_rotation_angle(self, val: float):
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
@threadlocked
def _set_axis_velocity(self, um_per_s):
self.socket_put(f"V{um_per_s}")
@@ -234,19 +170,6 @@ class RtLamniController(Controller):
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")
@@ -256,7 +179,7 @@ class RtLamniController(Controller):
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")
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
@@ -268,7 +191,7 @@ class RtLamniController(Controller):
def get_scan_status(self):
return_table = (self.socket_put_and_receive(f"sr")).split(",")
if len(return_table) != 3:
raise RtLamniCommunicationError(
raise RtCommunicationError(
f"Expected to receive 3 return values. Instead received {return_table}"
)
mode = int(return_table[0])
@@ -288,7 +211,7 @@ class RtLamniController(Controller):
logger.error(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
raise RtLamniError(
raise RtError(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
# here exception
@@ -296,7 +219,7 @@ class RtLamniController(Controller):
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.")
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")
@@ -305,29 +228,6 @@ class RtLamniController(Controller):
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:
@@ -439,7 +339,7 @@ class RtLamniController(Controller):
)
def feedback_status_angle_lamni(self) -> bool:
return_table = (self.socket_put_and_receive(f"J7")).split(",")
return_table = (self.socket_put_and_receive("J7")).split(",")
logger.debug(
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
)
@@ -448,21 +348,21 @@ class RtLamniController(Controller):
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.")
logger.info("LamNI resetting interferometer inclusive angular interferomter.")
else:
self.feedback_disable()
logger.info(
f"LamNI resetting interferomter except angular interferometer which is already running."
"LamNI resetting interferomter except angular interferometer which is already running."
)
# set these as closed loop target position
self.socket_put(f"pa0,0")
self.socket_put("pa0,0")
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
self.socket_put(f"pa1,0")
self.socket_put("pa1,0")
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
self.socket_put(
f"pa2,0"
"pa2,0"
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
self.clear_trajectory_generator()
@@ -476,7 +376,7 @@ class RtLamniController(Controller):
logger.error(
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
)
raise RtLamniError(
raise RtError(
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
)
@@ -515,7 +415,7 @@ class RtLamniController(Controller):
logger.error(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
raise RtLamniError(
raise RtError(
"Cannot start scan because feedback loop is not running or there is an interferometer error."
)
@@ -523,15 +423,6 @@ class RtLamniController(Controller):
# 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):
@@ -567,7 +458,7 @@ class RtLamniReadbackSignal(RtLamniSignalRO):
readback_index = 1
else:
raise RtLamniError("Currently, only two axes are supported.")
print(return_table)
current_pos = float(return_table[readback_index])
current_pos *= self.parent.sign
@@ -667,7 +558,9 @@ class RtLamniMotor(Device, PositionerBase):
):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtLamniController(socket=socket_cls(host=host, port=port))
self.controller = RtLamniController(
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)
@@ -705,15 +598,16 @@ class RtLamniMotor(Device, PositionerBase):
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
def check_value(self, value, **kwargs):
"""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}")
if low_limit < high_limit and not (low_limit <= value <= high_limit):
raise LimitError(f"position={value} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
# pylint: disable=protected-access
walk.item._metadata["connected"] = self.controller.connected
def _forward_readback(self, **kwargs):
@@ -782,7 +676,7 @@ class RtLamniMotor(Device, PositionerBase):
def axis_Id(self, val):
if isinstance(val, str):
if len(val) != 1:
raise ValueError(f"Only single-character axis_Ids are supported.")
raise ValueError("Only single-character axis_Ids are supported.")
self._axis_Id_alpha = val
self._axis_Id_numeric = ord(val.lower()) - 97
else:
@@ -796,7 +690,7 @@ class RtLamniMotor(Device, PositionerBase):
def axis_Id_numeric(self, val):
if isinstance(val, int):
if val > 26:
raise ValueError(f"Numeric value exceeds supported range.")
raise ValueError("Numeric value exceeds supported range.")
self._axis_Id_alpha = val
self._axis_Id_numeric = (chr(val + 97)).capitalize()
else:
@@ -823,9 +717,7 @@ class RtLamniMotor(Device, PositionerBase):
return super().stop(success=success)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
if __name__ == "__main__": # pragma: no cover
mock = False
if not mock:
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
"""
This module contains base signals for RT devices. Controller and motors are implemented in the
bespoke modules such as `rt_flomni_ophyd.py` or `rt_lamni_ophyd.py`.
"""
import functools
import time
from bec_lib import bec_logger
from ophyd.utils import ReadOnlyError
from ophyd_devices.utils.controller import ControllerCommunicationError, threadlocked
from ophyd_devices.utils.socket import SocketSignal
logger = bec_logger.logger
class RtCommunicationError(ControllerCommunicationError):
pass
class RtError(ControllerCommunicationError):
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 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("J4")).split(",")
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

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

@@ -80,6 +80,8 @@ class SmaractController(Controller):
"describe",
"axis_is_referenced",
"all_axes_referenced",
"set_closed_loop_move_speed",
"is_axis_moving",
]
def __init__(
@@ -111,10 +113,6 @@ class SmaractController(Controller):
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

View File

@@ -0,0 +1,9 @@
def patch_dual_pvs(device):
device.wait_for_connection(all_signals=True)
for walk in device.walk_signals():
if not hasattr(walk.item, "_read_pv"):
continue
if not hasattr(walk.item, "_write_pv"):
continue
if walk.item._read_pv.pvname.endswith("_RBV"):
walk.item._read_pv = walk.item._write_pv

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
from bec_lib import DeviceManagerBase
from bec_lib.devicemanager import DeviceManagerBase
from bec_server.file_writer.file_writer import HDF5Storage

View File

@@ -23,7 +23,8 @@ but they are executed in a specific order:
import time
import numpy as np
from bec_lib import MessageEndpoints, bec_logger
from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints
from bec_server.scan_server.errors import ScanAbortion
from bec_server.scan_server.scans import RequestBase, ScanArgType, ScanBase
@@ -109,9 +110,8 @@ class LamNIMixin:
logger.info(
f"Compensating {[val/1000 for val in lamni_to_stage_coordinates(x_drift,y_drift)]}"
)
yield from self.stubs.set_and_wait(
device=["lsamx", "lsamy"], positions=[move_x, move_y]
)
yield from self.stubs.set(device="lsamx", value=move_x)
yield from self.stubs.set(device="lsamy", value=move_y)
time.sleep(0.01)
rtx_current = yield from self.stubs.send_rpc_and_wait("rtx", "readback.get")
@@ -147,9 +147,9 @@ class LamNIMixin:
+ lamni_to_stage_coordinates(x_drift, y_drift)[1] / 1000
+ lamni_to_stage_coordinates(x_drift2, y_drift2)[1] / 1000
)
yield from self.stubs.set_and_wait(
device=["lsamx", "lsamy"], positions=[move_x, move_y]
)
yield from self.stubs.set(device="lsamx", value=move_x)
yield from self.stubs.set(device="lsamy", value=move_y)
time.sleep(0.01)
rtx_current = yield from self.stubs.send_rpc_and_wait("rtx", "readback.get")
rty_current = yield from self.stubs.send_rpc_and_wait("rty", "readback.get")
@@ -443,7 +443,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
}
}
)
yield from self.stubs.set_and_wait(device=["lsamrot"], positions=[angle])
yield from self.stubs.set(device="lsamrot", value=angle)
def scan_core(self):
if self.scan_type == "step":
@@ -456,7 +456,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
# scan ID before sending the message to the device server
yield from self.stubs.kickoff(device="rtx")
while True:
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
yield from self.stubs.read(group="monitored")
msg = self.device_manager.connector.get(MessageEndpoints.device_status("rt_scan"))
if msg:
status = msg

View File

@@ -1,4 +1,6 @@
from .flomni_fermat_scan import FlomniFermatScan
from .jungfrau_joch_scan import JungfrauJochTestScan
from .LamNIFermatScan import LamNIFermatScan, LamNIMoveToScanCenter
from .omny_fermat_scan import OMNYFermatScan
from .owis_grid import OwisGrid
from .sgalil_grid import SgalilGrid

View File

@@ -23,7 +23,8 @@ but they are executed in a specific order:
import time
import numpy as np
from bec_lib import MessageEndpoints, bec_logger, messages
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from bec_server.scan_server.errors import ScanAbortion
from bec_server.scan_server.scans import SyncFlyScanBase
@@ -34,7 +35,7 @@ class FlomniFermatScan(SyncFlyScanBase):
scan_name = "flomni_fermat_scan"
scan_report_hint = "table"
scan_type = "fly"
required_kwargs = ["fov_size", "exp_time", "step", "angle"]
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
@@ -58,8 +59,8 @@ class FlomniFermatScan(SyncFlyScanBase):
Args:
fovx(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 200 um
fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um
cenx(float) [mm]: center position in x.
ceny(float) [mm]: center position in y.
cenx(float) [um]: center position in x.
ceny(float) [um]: center position in y.
exp_time(float) [s]: exposure time
step(float) [um]: stepsize
zshift(float) [um]: shift in z
@@ -94,6 +95,7 @@ class FlomniFermatScan(SyncFlyScanBase):
if self.zshift < -100:
logger.warning("The zshift is smaller than -100 um. It will be limited to -100 um.")
self.zshift = -100
self.flomni_rotation_status = None
def initialize(self):
self.scan_motors = []
@@ -114,13 +116,12 @@ class FlomniFermatScan(SyncFlyScanBase):
shorten the movement time. In order to keep the last state, even if the
server is restarted, the state is stored in a global variable in redis.
"""
producer = self.device_manager.producer
msg = producer.get(MessageEndpoints.global_vars("reverse_flomni_trajectory"))
msg = self.connector.get(MessageEndpoints.global_vars("reverse_flomni_trajectory"))
if msg:
val = msg.content.get("value", False)
else:
val = False
producer.set(
self.connector.set(
MessageEndpoints.global_vars("reverse_flomni_trajectory"),
messages.VariableMessage(value=(not val)),
)
@@ -149,17 +150,17 @@ class FlomniFermatScan(SyncFlyScanBase):
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
def _prepare_setup_part2(self):
yield from self.stubs.wait(wait_type="move", device="fsamroy", wait_group="flomni_rotation")
yield from self.stubs.set(
device="rtx", value=self.positions[0][0], wait_group="prepare_setup_part2"
)
yield from self.stubs.set(
device="rtz", value=self.positions[0][2], wait_group="prepare_setup_part2"
)
if self.flomni_rotation_status:
self.flomni_rotation_status.wait()
rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False)
yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on")
yield from self.stubs.wait(
wait_type="move", device=["rtx", "rtz"], wait_group="prepare_setup_part2"
)
rtx_status.wait()
rtz_status.wait()
yield from self._transfer_positions_to_flomni()
yield from self.stubs.send_rpc_and_wait(
"rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx
@@ -199,7 +200,9 @@ class FlomniFermatScan(SyncFlyScanBase):
}
}
)
yield from self.stubs.set(device="fsamroy", value=angle, wait_group="flomni_rotation")
self.flomni_rotation_status = yield from self.stubs.set(
device="fsamroy", value=angle, wait=False
)
def _transfer_positions_to_flomni(self):
yield from self.stubs.send_rpc_and_wait(
@@ -275,8 +278,8 @@ class FlomniFermatScan(SyncFlyScanBase):
# scan ID before sending the message to the device server
yield from self.stubs.kickoff(device="rtx")
while True:
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
status = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan"))
yield from self.stubs.read(group="monitored")
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
if status:
status_id = status.content.get("status", 1)
request_id = status.metadata.get("RID")
@@ -296,19 +299,7 @@ class FlomniFermatScan(SyncFlyScanBase):
"""return to the start position"""
# in flomni, we need to move to the start position of the next scan
if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3:
yield from self.stubs.set(
device="rtx", value=self.positions[-1][0], wait_group="scan_motor"
)
yield from self.stubs.set(
device="rty", value=self.positions[-1][1], wait_group="scan_motor"
)
yield from self.stubs.set(
device="rtz", value=self.positions[-1][2], wait_group="scan_motor"
)
yield from self.stubs.wait(
wait_type="move", device=["rtx", "rty", "rtz"], wait_group="scan_motor"
)
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
return
logger.warning("No positions found to return to start")

View File

@@ -0,0 +1,58 @@
""" Module with JungfrauJochTestScan class. """
from bec_lib import bec_logger
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanAbortion
logger = bec_logger.logger
class JungfrauJochTestScan(AsyncFlyScanBase):
"""Owis-based grid scan."""
scan_name = "jjf_test"
# scan_report_hint = "device_progress"
required_kwargs = ["points", "exp_time", "readout_time"]
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
gui_config = {
"Acquisition Parameters": ["num_points", "cycles"],
"Exposure Parameters": ["exp_time", "readout_time"],
}
def __init__(
self, num_points: int, exp_time: float, readout_time: float, cycles: int = 1, **kwargs
):
"""
JungfrauJoch Test scan.
Args:
device (DeviceBase) : The device to be triggered, currently only for delaygenerator csaxs
num_points (int) : Number of points per burst
exp_time (float) : exposure time.
readout_time (float): readout time of detector
cycles (int) : number of cycles, default is 1
Example:
scans.jjf_test(points = 100, exp_time= 1e-3, readout_time=1e-3, cycles = 2)
"""
if readout_time <= 0:
raise ScanAbortion(f"Readout time must be larger than 0, provided value {readout_time}")
super().__init__(exp_time=exp_time, readout_time=readout_time, **kwargs)
self.device = "ddg"
self.num_points = num_points
self.cycles = cycles
self.primary_readout_cycle = 0.2
def scan_core(self):
logger.info(f"Starting with Scan Core")
total_exposure = self.num_points * (self.exp_time + self.readout_time)
for i in range(self.cycles):
logger.info(f"Beginning cycle {i} of {self.cycles}")
status = yield from self.stubs.trigger(min_wait=total_exposure, wait=False)
yield from self.stubs.read(group="monitored", point_id=self.point_id, wait=True)
self.point_id += 1
status.wait()
logger.info(f"Finished cycle {i} of {self.cycles}")
logger.info(f"Finished scan")
self.num_pos = self.point_id

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