Compare commits

...

207 Commits

Author SHA1 Message Date
gac-x12sa
d7290e3942 wip work at beamline 2025-04-23 10:51:14 +02:00
gac-x12sa
730926f5b3 wip jfj integration 2025-03-25 16:30:01 +01:00
gac-x12sa
18215a05b5 wip jfj, implemented first logic for computing triggers and pulse widths 2025-03-24 17:39:26 +01:00
gac-x12sa
5cd93fc5aa wip moving readout from class attribute to constant 2025-03-24 17:38:01 +01:00
gac-x12sa
2b4a13ebc2 wip eiger, improve stage procedure for BEC core scans 2025-03-24 08:23:18 +01:00
gac-x12sa
ee8fa8b962 wip fixing jfj client, improve start procedure 2025-03-24 08:23:18 +01:00
gac-x12sa
b281e458f9 wip small fixes of ddg csaxs for readout times 2025-03-24 08:23:17 +01:00
gac-x12sa
48bd7f73a8 refactor: delay generator ready for step scanning 2025-03-21 15:12:35 +01:00
gac-x12sa
b806487c54 wip: draft from working at the beamline 2025-02-26 15:45:22 +01:00
53dca4dc6f wip, fix enums for pytest 3.10 2025-02-26 11:28:04 +01:00
ccf8bb8474 refactor: client refactoring, adding tests for jfj_client models 2025-02-26 11:22:14 +01: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
de10eb95e1 Merge branch 'fix/file_writer' into 'main'
fix: added missing nexus layout

See merge request bec/csaxs_bec!10
2024-04-23 19:07:11 +02:00
bb7786c808 fix: added missing nexus layout 2024-04-23 18:24:34 +02:00
cdd47e8dbb Merge branch 'build/fixed_plugin_import' into 'main'
fix: fixed CA startup for device server

See merge request bec/csaxs_bec!9
2024-04-22 19:34:10 +02:00
97224e37cf fix: fixed CA startup for device server 2024-04-22 19:29:28 +02:00
124878a064 Merge branch 'build/fixed_plugin_import' into 'main'
build: fixed plugin import

See merge request bec/csaxs_bec!8
2024-04-22 19:19:50 +02:00
1a4e992d2f fix: fixed startup and entry points 2024-04-22 19:06:43 +02:00
0a59439ecc build: fixed plugin import 2024-04-22 18:44:22 +02:00
140ceb7359 Merge branch 'feat/transition_to_new_plugin_structure' into 'main'
feat: removed old and introduced new structure

See merge request bec/csaxs-bec!7
2024-04-19 17:56:43 +02:00
4326c5b4a0 ci: add ci file 2024-04-19 17:48:35 +02:00
daf1ec0317 refactor: updated configs, minor improvements, formatting 2024-04-19 17:48:35 +02:00
c109abce12 feat: populated csaxs_bec from ophyd and bec 2024-04-19 17:48:30 +02:00
96921da618 feat: removed old and introduced new structure 2024-04-18 09:37:58 +02:00
058be66aaa Merge branch 'doc-pmodule-update' into 'master'
doc: psi-python311/2024.02 is now stable

See merge request bec/csaxs-bec!6
2024-03-13 21:29:46 +01:00
b2786dca83 doc: psi-python311/2024.02 is now stable 2024-03-12 13:32:35 +01:00
197 changed files with 35916 additions and 12722 deletions

3
.git_hooks/post-commit Executable file
View File

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

View File

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

3
.gitignore vendored
View File

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

20
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,20 @@
include:
- project: bec/awi_utils
file: /templates/plugin-repo-template.yml
inputs:
name: "csaxs"
target: "csaxs_bec"
branch: $CHILD_PIPELINE_BRANCH
pages:
stage: Deploy
needs: []
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:
- 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

28
LICENSE Normal file
View File

@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,161 +0,0 @@
import builtins
import time
from rich import box
from rich.console import Console
from rich.table import Table
from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose
# import builtins to avoid linter errors
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
bec = builtins.__dict__.get("bec")
class LamNIOpticsMixin:
@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 leye_out(self):
self.loptics_in()
fshclose()
leyey_out = self._get_user_param_safe("leyey", "out")
umv(dev.leyey, leyey_out)
epics_put("XOMNYI-XEYE-ACQ:0", 2)
# move rotation stage to zero to avoid problems with wires
umv(dev.lsamrot, 0)
umv(dev.dttrz, 5854, dev.fttrz, 2395)
def leye_in(self):
bec.queue.next_dataset_number += 1
# move rotation stage to zero to avoid problems with wires
umv(dev.lsamrot, 0)
umv(dev.dttrz, 6419.677, dev.fttrz, 2959.979)
while True:
moved_out = (input("Did the flight tube move out? (Y/n)") or "y").lower()
if moved_out == "y":
break
if moved_out == "n":
return
leyex_in = self._get_user_param_safe("leyex", "in")
leyey_in = self._get_user_param_safe("leyey", "in")
umv(dev.leyex, leyex_in, dev.leyey, leyey_in)
self.align.update_frame()
def _lfzp_in(self):
loptx_in = self._get_user_param_safe("loptx", "in")
lopty_in = self._get_user_param_safe("lopty", "in")
umv(
dev.loptx, loptx_in, dev.lopty, lopty_in
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
def lfzp_in(self):
"""
move in the lamni zone plate.
This will disable rt feedback, move the FZP and re-enabled the feedback.
"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
self._lfzp_in()
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_enable_with_reset()
def loptics_in(self):
"""
Move in the lamni optics, including the FZP and the OSA.
"""
self.lfzp_in()
self.losa_in()
def loptics_out(self):
"""Move out the lamni optics"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
# self.lcs_out()
self.losa_out()
loptx_out = self._get_user_param_safe("loptx", "out")
lopty_out = self._get_user_param_safe("lopty", "out")
umv(dev.loptx, loptx_out, dev.lopty, lopty_out)
if "rtx" in dev and dev.rtx.enabled:
time.sleep(1)
dev.rtx.controller.feedback_enable_with_reset()
def lcs_in(self):
# umv lcsx -1.852 lcsy -0.095
pass
def lcs_out(self):
umv(dev.lcsy, 3)
def losa_in(self):
# 6.2 keV, 170 um FZP
# umv(dev.losax, -1.4450000, dev.losay, -0.1800)
# umv(dev.losaz, -1)
# 6.7, 170
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
# umv(dev.losaz, 1.0000)
# 7.2, 150
losax_in = self._get_user_param_safe("losax", "in")
losay_in = self._get_user_param_safe("losay", "in")
losaz_in = self._get_user_param_safe("losaz", "in")
umv(dev.losax, losax_in, dev.losay, losay_in)
umv(dev.losaz, losaz_in)
# 11 kev
# umv(dev.losax, -1.161000, dev.losay, -0.196)
# umv(dev.losaz, 1.0000)
def losa_out(self):
losay_out = self._get_user_param_safe("losay", "out")
losaz_out = self._get_user_param_safe("losaz", "out")
umv(dev.losaz, losaz_out)
umv(dev.losay, losay_out)
def lfzp_info(self):
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:",
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
beam_size = (
-diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6
)
table.add_row(
f"{diameter*1e6:.2f} microns",
f"{focal_distance:.2f} mm",
f"{beam_size:.2f} microns",
)
console.print(table)
print("OSA Information:")
# print(f"Current losaz %.1f\n", A[losaz])
# print("The OSA will collide with the sample plane at %.1f\n\n", 89.3-A[loptz])
print(
"The numbers presented here are for a sample in the plane of the lamni sample holder.\n"
)

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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
@@ -27,7 +28,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

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

View File

@@ -0,0 +1,329 @@
import builtins
import time
from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
# import builtins to avoid linter errors
dev = builtins.__dict__.get("dev")
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):
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 leye_out(self):
self.loptics_in()
fshclose()
leyey_out = self._get_user_param_safe("leyey", "out")
umv(dev.leyey, leyey_out)
epics_put("XOMNYI-XEYE-ACQ:0", 2)
# move rotation stage to zero to avoid problems with wires
umv(dev.lsamrot, 0)
umv(dev.dttrz, 5854, dev.fttrz, 2395)
def leye_in(self):
bec.queue.next_dataset_number += 1
# move rotation stage to zero to avoid problems with wires
umv(dev.lsamrot, 0)
umv(dev.dttrz, 6419.677, dev.fttrz, 2959.979)
while True:
moved_out = (input("Did the flight tube move out? (Y/n)") or "y").lower()
if moved_out == "y":
break
if moved_out == "n":
return
leyex_in = self._get_user_param_safe("leyex", "in")
leyey_in = self._get_user_param_safe("leyey", "in")
umv(dev.leyex, leyex_in, dev.leyey, leyey_in)
self.align.update_frame()
def _lfzp_in(self):
loptx_in = self._get_user_param_safe("loptx", "in")
lopty_in = self._get_user_param_safe("lopty", "in")
umv(
dev.loptx, loptx_in, dev.lopty, lopty_in
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
def lfzp_in(self):
"""
move in the lamni zone plate.
This will disable rt feedback, move the FZP and re-enabled the feedback.
"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
self._lfzp_in()
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_enable_with_reset()
def loptics_in(self):
"""
Move in the lamni optics, including the FZP and the OSA.
"""
self.lfzp_in()
self.losa_in()
def loptics_out(self):
"""Move out the lamni optics"""
if "rtx" in dev and dev.rtx.enabled:
dev.rtx.controller.feedback_disable()
# self.lcs_out()
self.losa_out()
loptx_out = self._get_user_param_safe("loptx", "out")
lopty_out = self._get_user_param_safe("lopty", "out")
umv(dev.loptx, loptx_out, dev.lopty, lopty_out)
if "rtx" in dev and dev.rtx.enabled:
time.sleep(1)
dev.rtx.controller.feedback_enable_with_reset()
def lcs_in(self):
# umv lcsx -1.852 lcsy -0.095
pass
def lcs_out(self):
umv(dev.lcsy, 3)
def losa_in(self):
# 6.2 keV, 170 um FZP
# umv(dev.losax, -1.4450000, dev.losay, -0.1800)
# umv(dev.losaz, -1)
# 6.7, 170
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
# umv(dev.losaz, 1.0000)
# 7.2, 150
losax_in = self._get_user_param_safe("losax", "in")
losay_in = self._get_user_param_safe("losay", "in")
losaz_in = self._get_user_param_safe("losaz", "in")
umv(dev.losax, losax_in, dev.losay, losay_in)
umv(dev.losaz, losaz_in)
# 11 kev
# umv(dev.losax, -1.161000, dev.losay, -0.196)
# umv(dev.losaz, 1.0000)
def losa_out(self):
losay_out = self._get_user_param_safe("losay", "out")
losaz_out = self._get_user_param_safe("losaz", "out")
umv(dev.losaz, losaz_out)
umv(dev.losay, losay_out)
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]
console = Console()
table = Table(
title=f"At the current energy of {mokev_val:.4f} keV we have following options:",
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
beam_size = (
-diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6
)
table.add_row(
f"{diameter*1e6:.2f} microns",
f"{focal_distance:.2f} mm",
f"{beam_size:.2f} microns",
)
console.print(table)
print("OSA Information:")
# print(f"Current losaz %.1f\n", A[losaz])
# print("The OSA will collide with the sample plane at %.1f\n\n", 89.3-A[loptz])
print(
"The numbers presented here are for a sample in the plane of the lamni sample holder.\n"
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,21 @@
"""
Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to add additional command line arguments.
"""
def extend_command_line_args(parser):
"""
Extend the command line arguments of the BEC client.
"""
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
return parser
# def get_config() -> ServiceConfig:
# """
# Create and return the service configuration.
# """
# return ServiceConfig(redis={"host": "localhost", "port": 6379})

View File

View File

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

View File

View File

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

File diff suppressed because it is too large Load Diff

View File

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

@@ -0,0 +1,364 @@
############################################################
#################### flOMNI Galil motors ###################
############################################################
feyex:
description: Xray eye X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: D
host: mpc2844.psi.ch
limits:
- -30
- -1
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -16.267
out: -1
feyey:
description: Xray eye Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: E
host: mpc2844.psi.ch
limits:
- -1
- -10
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: -10.467
fheater:
description: Heater Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -15
- 0
port: 8082
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
foptx:
description: Optics X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -17
- -12
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: -13.761
fopty:
description: Optics Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: F
host: mpc2844.psi.ch
limits:
- 0
- 4
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: true
readoutPriority: baseline
userParameter:
in: 0.552
out: 0.752
foptz:
description: Optics Z
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 0
- 27
port: 8082
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 23
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: OSA X
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
limits:
- 10.2
- 10.6
port: 3334
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 9.124
out: 5.3
fosay:
description: OSA Y
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
limits:
- -3.1
- -2.9
port: 3334
sign: -1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0.367
fosaz:
description: OSA Z
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
limits:
- -6
- -4
port: 3334
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 8.5
out: 6
############################################################
#################### flOMNI RT motors ######################
############################################################
rtx:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: A
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
low_signal: 11000
min_signal: 10000
rt_pid_voltage: -0.06219
rty:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: B
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request
userParameter:
tomo_additional_offsety: 0
rtz:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
axis_Id: C
host: mpc2844.psi.ch
port: 2222
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: on_request

View File

@@ -0,0 +1,64 @@
eiger9m:
description: Eiger9m HPC area detector 9M with JungfrauJoch backend
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_jfj.Eiger9MCSAXS
deviceConfig:
host: "http://sls-jfjoch-001"
port: 8080
deviceTags:
- cSAXS
- eiger9m
onFailure: buffer
enabled: true
readoutPriority: async
softwareTrigger: false
ddg_jfj:
description: DelayGenerator for triggering all detectors
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
# Two test devices from the simulation
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
bpm4i:
readoutPriority: monitored
deviceClass: ophyd_devices.SimMonitor
deviceConfig:
deviceTags:
- beamline
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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

@@ -0,0 +1 @@

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,995 @@
"""Module for cSAXS specific integration of the DG645 delay generator."""
from bec_lib import bec_logger
from bec_lib.devicemanager import ScanInfo
from ophyd.status import DeviceStatus, StatusBase, SubscriptionStatus
from ophyd_devices.devices.delay_generator_645 import DelayGenerator, TriggerSource
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.jungfraujoch.readout_constants import EIGER9M_READOUT_TIME_32BIT
# 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 DelayGeneratorError(Exception):
"""Exception raised for errors."""
READOUT_TIME = [10e-6, EIGER9M_READOUT_TIME_32BIT, 10e-6, 10e-6, 10e-6] # TO, AB, CD, EF, GH
class DelayGeneratorcSAXS(PSIDeviceBase, DelayGenerator):
"""'X12SA-CPCL-DDGX: for X=1 to5'
For DDG CAQTDM panel:
-> caqtdm -noMsg -attach -macro P=X12SA-CPCL-,R=DDG3: srsDG645.ui
-> telnet x12sa-vserv-01 50005 (for connecting to the IOC of all DDGs)
all telnet ports are listed here, e.g. for host x12sa-vserv-01: /ioc/hosts/x12sa-vserv-01/shellbox.conf
-> The IOC runs up to 5 different DDGs, 1...5, check if they are connected and powered on.
After power cycling, a reboot of the IOC may help to properly establish the connection again.
In telnet:
-> exit to resboot IOC
-> CTRL D to exit telnet
"""
# Readout time of detectors, will be subtracted from exp_time
# TBD should this behave like this or should it be added to the exp_time?
def __init__(
self,
name: str,
prefix: str = "",
scan_info: ScanInfo | None = None,
ddg_config: dict | None = None,
trigger_level: float = 2.5,
trigger_source: TriggerSource = TriggerSource.SINGLE_SHOT,
device_manager=None,
**kwargs,
):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.device_manager = device_manager
self.ddg_config = {
# Setup default values for channels T0, AB, CD, EF, GH
"width": [0.1, 0.1, 0.1, 0.1, 0.1],
"delay": [0, 0, 0, 0, 0],
"polarity": [1, 0, 1, 1, 1],
"amplitude": [4.0, 4.5, 4.5, 4.5, 4.5],
"offset": [0, 0, 0, 0, 0],
}
if ddg_config is not None:
for key in self.ddg_config:
if key in ddg_config: # Update only known keys
self.ddg_config[key] = ddg_config[key]
self._init_signal_values = {"level": trigger_level, "source": trigger_source}
########################################
## Utility Methods for the device ##
def compute_num_trigger(self) -> int:
"""Method to comput the number of triggers based on the most recent ScanStatusMessage"""
num_points = self.scan_info.msg.num_points
num_bursts = self.scan_info.msg.scan_parameters["frames_per_trigger"]
# Pull the frame_time_us from central location!!
trigger_per_point = int(
self.scan_info.msg.scan_parameters["exp_time"] / (self.frame_time + self.readout_time)
)
num_trigger = num_points * num_bursts * trigger_per_point
return num_trigger
########################################
# 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.
"""
self.frame_time = 300e-6 # 480us
self.readout_time = 200e-6 # 20us
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
# Set default signal values
for name, value in self._init_signal_values.items():
getattr(self, name).set(value).wait()
# Set default values for all channels
for name, value in self.ddg_config.items():
self.set_channels(name, value)
# Check that JungfrauJoch exists and is loaded in config.
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.
"""
# Step scan, trigger by BEC
if self.scan_info.msg.scan_type == "step":
self.set_trigger(TriggerSource.SINGLE_SHOT)
n_trigger = self.compute_num_trigger()
self.burst_enable(
count=n_trigger, delay=0, period=self.frame_time + self.readout_time, config="first"
)
width = self.frame_time
self.set_channels("width", width)
self.set_channels("delay", 0)
return
elif self.scan_info.msg.scan_type == "fly":
pass
else: # Unknown scan type
raise ValueError(f"Unknown scan type {self.scan_info.msg.scan_type}")
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device."""
self.set_trigger(TriggerSource.SINGLE_SHOT)
status = self.task_handler.submit_task(self.check_if_ddg_okay)
return status
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."""
if self.source.get() == TriggerSource.SINGLE_SHOT:
# Force the burst_cycle_finished readout
self.trigger_burst_readout.put(1)
status = self.task_handler.submit_task(
task=self.wait_for_condition,
task_args=(
self._check_ddg,
self.scan_info.msg.scan_parameters["exp_time"]
* self.scan_info.msg.scan_parameters["frames_per_trigger"]
* 1.5,
),
task_kwargs={"check_stopped": True},
run=False,
)
self.trigger_shot.put(1)
self.task_handler.start_task(status)
return status
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."""
self.burst_disable() # This will stop any triggers
self.task_handler.shutdown()
def _check_ddg(self) -> bool:
self.trigger_burst_readout.put(1)
return self.burst_cycle_finished.get() == 1
if __name__ == "__main__": # pragme: no cover
ddg3 = DelayGeneratorcSAXS(prefix="X12SA-CPCL-DDG3:", name="ddg3")
ddg3.set_channels("delay", 0)
# 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")
# import time
# from bec_lib import bec_logger
# from ophyd import Component, DeviceStatus
# from ophyd_devices.interfaces.base_classes.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 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:
# self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
# # scantype "jjf_test"
# scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "")
# if scan_name == "jjf_test":
# # exp_time = self.parent.scaninfo.exp_time
# # readout = self.parent.scaninfo.readout_time
# # num_burst_cycle = self.parent.scaninfo.scan_msg.content["info"]["kwargs"]["num_points"]
# # total_exposure = exp_time+readout
# 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_stage(self) -> None:
# 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)
# self.parent.set_channels("width", exp_time)
# self.parent.set_channels("delay", 0.0)
# logger.info(
# f"{self.parent.name}: On stage with n_burst: {num_burst_cycle} and total_exp {total_exposure}"
# )
# self.parent.burst_enable(num_burst_cycle, 0, total_exposure, config="first")
# 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)
# 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)
# cycle = self.parent.scaninfo.scan_msg.content["info"]["kwargs"]["cycles"]
# # time.sleep(num_burst_cycle*total_exposure)
# def check_ddg() -> int:
# self.parent.trigger_burst_readout.put(1)
# return self.parent.burst_cycle_finished.get()
# status = self.wait_with_status(
# signal_conditions=[(check_ddg, 1)],
# timeout=num_burst_cycle * total_exposure + 1,
# check_stopped=True,
# exception_on_timeout=DelayGeneratorError(
# f"{self.parent.name} run into timeout in complete call."
# ),
# )
# logger.info(f"Return status {self.parent.name}")
# return status
# # timer = 0
# # while True:
# # self.parent.trigger_burst_readout.put(1)
# # state = self.parent.burst_cycle_finished.get()
# # if state == 1:
# # break
# # time.sleep(0.05)
# # timer +=0.05
# # if timer>3:
# # raise TimeoutError(f"{self.parent.name} did not return. Bit state for end_burst_cycle is {state} for state")
# def on_complete(self) -> DeviceStatus:
# pass
# # logger.info(f"On complete started for {self.parent.name}")
# # scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "")
# # if scan_name != "jjf_test":
# # return None
# # def check_ddg()->int:
# # lambda r : self.parent.trigger_burst_readout.put(1)
# # return lambda r: self.parent.burst_cycle_finished.get()
# # status = self.wait_with_status(signal_conditions=[(check_ddg, 1)],
# # timeout=3,
# # check_stopped=True,
# # exception_on_timeout=DelayGeneratorError(f"{self.parent.name} run into timeout in complete call.")
# # )
# # logger.info(f"Return status {self.parent.name}")
# # return status
# 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,
# 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,
# **kwargs,
# )
# if __name__ == "__main__":
# # Start delay generator in simulation mode.
# # Note: To run, access to Epics must be available.
# import time
# config = {
# "delay_burst": 40.0e-3,
# "delta_width": 0,
# "additional_triggers": 0,
# "polarity": [1, 0, 1, 1, 1], # T0 # to eiger and lecroy4
# "amplitude": 4.5,
# "offset": 0,
# "thres_trig_level": 2.5,
# "set_high_on_exposure": False,
# "set_high_on_stage": False,
# }
# start = time.time()
# print(f"Start with init of DDG3 with config: {config}")
# dgen = DelayGeneratorcSAXS("X12SA-CPCL-DDG3:", name="dgen", ddg_config=config)
# print(f"Finished init after: {time.time()-start}s")
# start = time.time()
# print(f"Start setting up DDG3")
# exp_time = 1 / (2e3) # 2 kHz
# readout = exp_time / 10
# delay = 0
# num_burst_cycle = 1e4 # N triggers
# total_exposure = exp_time + readout
# delay_burst = dgen.delay_burst.get()
# dgen.set_trigger(trigger_source=TriggerSource.SINGLE_SHOT)
# dgen.set_channels(signal="width", value=exp_time)
# dgen.set_channels(signal="delay", value=0)
# dgen.burst_enable(
# count=num_burst_cycle, delay=delay_burst, period=total_exposure, config="first"
# )
# print(
# f"Start sending {num_burst_cycle} triggers after {time.time()-start}s, ETA {num_burst_cycle*total_exposure}s"
# )
# break_time = time.time()
# dgen.trigger()
# # Wait here briefly for status to finish, whether this is realiable has to be tested
# time.sleep(num_burst_cycle * total_exposure)
# timer = 0
# while True:
# dgen.trigger_burst_readout.put(1)
# state = dgen.burst_cycle_finished.get()
# if state == 1:
# break
# time.sleep(0.05)
# timer += 0.05
# if timer > 3:
# raise TimeoutError(f"dgen.name did not return with value {state} for state")
# print(
# f"Finished trigger cascade of {num_burst_cycle} with {exp_time}s -> {num_burst_cycle*exp_time}s after {time.time()-start}s in total, {break_time} for sending triggers."
# )

View File

@@ -0,0 +1,381 @@
import enum
import os
import threading
import time
from typing import Any
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import ADComponent as ADCpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from std_daq_client import StdDaqClient
logger = bec_logger.logger
class EigerError(Exception):
"""Base class for exceptions in this module."""
class EigerTimeoutError(EigerError):
"""Raised when the Eiger does not respond in time."""
class Eiger9MSetup(CustomDetectorMixin):
"""Eiger setup class
Parent class: CustomDetectorMixin
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self.std_rest_server_url = (
kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000"
)
self.std_client = None
self._lock = threading.RLock()
def 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()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector_backend(self) -> None:
"""Initialize detector backend"""
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
self.std_client.stop_writer()
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_FOR_SIGNALS,
all_signals=True,
):
raise EigerTimeoutError(
f"Std client not in READY state, returns: {self.std_client.get_status()}"
)
def update_std_cfg(self, cfg_key: str, value: Any) -> None:
"""
Update std_daq config
Checks that the new value matches the type of the former entry.
Args:
cfg_key (str) : config key of value to be updated
value (Any) : value to be updated for the specified key
Raises:
Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value
"""
cfg = self.std_client.get_config()
old_value = cfg.get(cfg_key)
if old_value is None:
raise EigerError(
f"Tried to change entry for key {cfg_key} in std_config that does not exist"
)
if not isinstance(value, type(old_value)):
raise EigerError(
f"Type of new value {type(value)}:{value} does not match old value"
f" {type(old_value)}:{old_value}"
)
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)
signal_conditions = [
(
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
"value"
],
DetectorState.IDLE,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
check_stopped=True,
all_signals=False,
):
# Retry stop detector and wait for remaining time
self.parent.cam.acquire.put(0)
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
check_stopped=True,
all_signals=False,
):
raise EigerTimeoutError(
f"Failed to stop detector, detector state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None:
"""Close file writer"""
self.std_client.stop_writer()
def 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_FOR_SIGNALS,
check_stopped=False,
all_signals=True,
):
raise EigerError(f"Timeout of 3s reached for filepath {filepath}")
def arm_acquisition(self) -> None:
"""Arm Eiger detector for acquisition"""
self.parent.cam.acquire.put(1)
signal_conditions = [
(
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
"value"
],
DetectorState.RUNNING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=True,
all_signals=False,
):
raise EigerTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def finished(self, timeout: int = 5) -> None:
"""Check if acquisition is finished."""
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):
"""
SLS Detector Camera - Eiger9M
Base class to map EPICS PVs to ophyd signals.
"""
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
num_images = ADCpt(EpicsSignalWithRBV, "NumCycles")
num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
acquire = ADCpt(EpicsSignal, "Acquire")
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
class TriggerSource(int, enum.Enum):
"""Trigger signals for Eiger9M detector"""
AUTO = 0
TRIGGER = 1
GATING = 2
BURST_TRIGGER = 3
class DetectorState(int, enum.Enum):
"""Detector states for Eiger9M detector"""
IDLE = 0
ERROR = 1
WAITING = 2
FINISHED = 3
TRANSMITTING = 4
RUNNING = 5
STOPPED = 6
STILL_WAITING = 7
INITIALIZING = 8
DISCONNECTED = 9
ABORTED = 10
class Eiger9McSAXS(PSIDetectorBase):
"""
Eiger9M detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
Various EpicsPVs for controlling the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = []
# specify Setup class
custom_prepare_cls = Eiger9MSetup
# 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:")
if __name__ == "__main__":
eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)

View File

@@ -0,0 +1,349 @@
import enum
import os
import threading
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.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
class FalconError(Exception):
"""Base class for exceptions in this module."""
class FalconTimeoutError(FalconError):
"""Raised when the Falcon does not respond in time."""
class DetectorState(enum.IntEnum):
"""Detector states for Falcon detector"""
DONE = 0
ACQUIRING = 1
class TriggerSource(enum.IntEnum):
"""Trigger source for Falcon detector"""
USER = 0
GATE = 1
SYNC = 2
class MappingSource(enum.IntEnum):
"""Mapping source for Falcon detector"""
SPECTRUM = 0
MAPPING = 1
class EpicsDXPFalcon(Device):
"""
DXP parameters for Falcon detector
Base class to map EPICS PVs from DXP parameters to ophyd signals.
"""
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
# Energy Filter PVs
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
# Misc PVs
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
class FalconHDF5Plugins(Device):
"""
HDF5 parameters for Falcon detector
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
"""
capture = Cpt(EpicsSignalWithRBV, "Capture")
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
class FalconSetup(CustomDetectorMixin):
"""
Falcon setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def __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
This will set:
- readout (float): readout time in seconds
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
"""
self.parent.value_pixel_per_buffer = 20
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize Falcon detector"""
self.stop_detector()
self.stop_detector_backend()
self.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# 1 Realtime
self.parent.preset_mode.put(1)
# 0 Normal, 1 Inverted
self.parent.input_logic_polarity.put(0)
# 0 Manual 1 Auto
self.parent.auto_pixels_per_buffer.put(0)
# Sets the number of pixels/spectra in the buffer
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
def initialize_detector_backend(self) -> None:
"""Initialize the detector backend for Falcon."""
self.parent.hdf5.enable.put(1)
# file location of h5 layout for cSAXS
self.parent.hdf5.xml_file_name.put("layout.xml")
# TODO Check if lazy open is needed and wanted!
self.parent.hdf5.lazy_open.put(1)
self.parent.hdf5.temp_suffix.put("")
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
self.parent.hdf5.queue_size.put(2000)
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.parent.nd_array_mode.put(1)
def 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.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
self.parent.pixels_per_run.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
def prepare_data_backend(self) -> None:
"""Prepare data backend for acquisition"""
self.parent.filepath.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")
self.parent.hdf5.num_capture.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.hdf5.file_write_mode.put(2)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.parent.hdf5.array_counter.put(0)
# Start file writing
self.parent.hdf5.capture.put(1)
def arm_acquisition(self) -> None:
"""Arm detector for acquisition"""
self.parent.start_all.put(1)
signal_conditions = [
(
lambda: self.parent.state.read()[self.parent.state.name]["value"],
DetectorState.ACQUIRING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=True,
all_signals=False,
):
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def on_unstage(self) -> None:
"""Unstage detector and backend"""
pass
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)
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):
"""
Falcon Sitoro detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
mca (EpicsMCARecord) : MCA parameters for Falcon detector
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
MIN_READOUT (float) : Minimum readout time for the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
# specify Setup class
custom_prepare_cls = FalconSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
# specify class attributes
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
mca = Cpt(EpicsMCARecord, "mca1")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
stop_all = Cpt(EpicsSignal, "StopAll")
erase_all = Cpt(EpicsSignal, "EraseAll")
start_all = Cpt(EpicsSignal, "StartAll")
state = Cpt(EpicsSignal, "Acquiring")
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
preset_real = Cpt(EpicsSignal, "PresetReal")
preset_events = Cpt(EpicsSignal, "PresetEvents")
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
if __name__ == "__main__":
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)

View File

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

View File

@@ -0,0 +1,400 @@
import enum
import json
import os
import threading
import time
import numpy as np
import requests
from bec_lib import bec_logger
from ophyd import ADComponent as ADCpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
class PilatusError(Exception):
"""Base class for exceptions in this module."""
class PilatusTimeoutError(PilatusError):
"""Raised when the Pilatus does not respond in time during unstage."""
class TriggerSource(enum.IntEnum):
"""Trigger source options for the detector"""
INTERNAL = 0
EXT_ENABLE = 1
EXT_TRIGGER = 2
MULTI_TRIGGER = 3
ALGINMENT = 4
class SLSDetectorCam(Device):
"""SLS Detector Camera - Pilatus
Base class to map EPICS PVs to ophyd signals.
"""
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
acquire = ADCpt(EpicsSignal, "Acquire")
armed = ADCpt(EpicsSignalRO, "Armed")
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True)
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
class PilatusSetup(CustomDetectorMixin):
"""Pilatus setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def __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()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize detector"""
# Stops the detector
self.stop_detector()
# Sets the trigger source to GATING
self.parent.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:
"""
Prepare detector for scan.
Includes checking the detector threshold,
setting the acquisition parameters and setting the trigger source
"""
self.set_detector_threshold()
self.set_acquisition_params()
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
def prepare_data_backend(self) -> None:
"""
Prepare the detector backend of pilatus for a scan
A zmq service is running on xbl-daq-34 that is waiting
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
"""
self.stop_detector_backend()
self.parent.filepath.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}"
)
self.parent.cam.auto_increment.put(1) # auto increment
self.parent.cam.file_number.put(0) # first iter
self.parent.cam.file_format.put(0) # 0: TIFF
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
# TODO better to remove hard coded path with link to home directory/pilatus_2
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
self.parent.filepath_raw = os.path.join(
basepath,
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
)
# Make directory if needed
self.create_directory(self.parent.filepath_raw)
headers = {"Content-Type": "application/json", "Accept": "application/json"}
# start the stream on x12sa-pd-2
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
data_msg = {
"source": [
{
"searchPath": "/",
"searchPattern": "glob:*.cbf",
"destinationPath": self.parent.filepath_raw,
}
]
}
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# start the data receiver on xbl-daq-34
url = "http://xbl-daq-34:8091/pilatus_2/run"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"addr": "tcp://x12sa-pd-2:8888",
"dst": ["file"],
"numFrm": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
"ifType": "PULL",
"user": self.parent.scaninfo.username,
},
]
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# Wait for server to become available again
time.sleep(0.1)
logger.info(f"{res.status_code} -{res.text} - {res.content}")
# Send requests.put to xbl-daq-34 to wait for data
url = "http://xbl-daq-34:8091/pilatus_2/wait"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"frmCnt": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
},
]
try:
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res}")
if not res.ok:
res.raise_for_status()
except Exception as exc:
logger.info(f"Pilatus2 wait threw Exception: {exc}")
def 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
Args:
url (str): url to send the request to
data (dict): data to be sent with the request (optional)
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
def send_requests_delete(self, url: str, headers: dict = None) -> object:
"""
Send a delete request to the given url
Args:
url (str): url to send the request to
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.delete(url=url, headers=headers, timeout=5)
def on_pre_scan(self) -> None:
"""Prepare detector for scan"""
self.arm_acquisition()
def arm_acquisition(self) -> None:
"""Arms the detector for the acquisition"""
self.parent.cam.acquire.put(1)
# TODO is this sleep needed? to be tested with detector and for how long
time.sleep(0.5)
def on_unstage(self) -> None:
"""Unstage the detector"""
pass
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}
)
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
signal_conditions = [
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=timeout,
check_stopped=True,
all_signals=True,
):
raise PilatusTimeoutError(
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
" the file writer"
)
self.stop_detector()
self.stop_detector_backend()
def 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 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):
"""Pilatus_2 300k detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
MIN_READOUT (float) : Minimum readout time for the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = []
# 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:")
if __name__ == "__main__":
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)

View File

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

View File

@@ -0,0 +1,479 @@
import threading
import time
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device, Kind
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from ophyd_devices.sim.sim_signals import SetableSignal
try:
from pyueye import ueye
except ImportError:
# The pyueye library is not installed or doesn't provide the necessary c libs
ueye = None
class IDSCustomPrepare(CustomDetectorMixin):
USER_ACCESS = ["pyueye"]
pyueye = ueye
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None:
super().__init__(*_args, parent=parent, **_kwargs)
self.ueye = ueye
self.h_cam = None
self.s_info = None
self.data_thread = None
self.thread_event = None
def on_connection_established(self):
self.hCam = self.ueye.HIDS(
self.parent.camera_ID
) # 0: first available camera; 1-254: The camera with the specified camera ID
self.sInfo = self.ueye.SENSORINFO()
self.cInfo = self.ueye.CAMINFO()
self.pcImageMemory = self.ueye.c_mem_p()
self.MemID = self.ueye.int()
self.rectAOI = self.ueye.IS_RECT()
self.pitch = self.ueye.INT()
self.nBitsPerPixel = self.ueye.INT(
self.parent.bits_per_pixel
) # 24: bits per pixel for color mode; take 8 bits per pixel for monochrome
self.channels = (
self.parent.channels
) # 3: channels for color mode(RGB); take 1 channel for monochrome
self.m_nColorMode = self.ueye.INT(
self.parent.m_n_colormode
) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
self.bytes_per_pixel = int(self.nBitsPerPixel / 8)
# Starts the driver and establishes the connection to the camera
nRet = self.ueye.is_InitCamera(self.hCam, None)
if nRet != 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 cInfo points to
nRet = self.ueye.is_GetCameraInfo(self.hCam, self.cInfo)
if nRet != self.ueye.IS_SUCCESS:
print("is_GetCameraInfo ERROR")
# You can query additional information about the sensor type used in the camera
nRet = self.ueye.is_GetSensorInfo(self.hCam, self.sInfo)
if nRet != self.ueye.IS_SUCCESS:
print("is_GetSensorInfo ERROR")
nRet = self.ueye.is_ResetToDefault(self.hCam)
if nRet != self.ueye.IS_SUCCESS:
print("is_ResetToDefault ERROR")
# Set display mode to DIB
nRet = self.ueye.is_SetDisplayMode(self.hCam, self.ueye.IS_SET_DM_DIB)
# Set the right color mode
if (
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_BAYER
):
# setup the color depth to the current windows setting
self.ueye.is_GetColorDepth(self.hCam, self.nBitsPerPixel, self.m_nColorMode)
bytes_per_pixel = int(self.nBitsPerPixel / 8)
print("IS_COLORMODE_BAYER: ")
print("\tm_nColorMode: \t\t", self.m_nColorMode)
print("\tnBitsPerPixel: \t\t", self.nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif (
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_CBYCRY
):
# for color camera models use RGB32 mode
m_nColorMode = ueye.IS_CM_BGRA8_PACKED
nBitsPerPixel = ueye.INT(32)
bytes_per_pixel = int(self.nBitsPerPixel / 8)
print("IS_COLORMODE_CBYCRY: ")
print("\tm_nColorMode: \t\t", m_nColorMode)
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif (
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_MONOCHROME
):
# for color camera models use RGB32 mode
m_nColorMode = self.ueye.IS_CM_MONO8
nBitsPerPixel = self.ueye.INT(8)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("IS_COLORMODE_MONOCHROME: ")
print("\tm_nColorMode: \t\t", m_nColorMode)
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
else:
# for monochrome camera models use Y8 mode
m_nColorMode = self.ueye.IS_CM_MONO8
nBitsPerPixel = self.ueye.INT(8)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("else")
# Can be used to set the size and position of an "area of interest"(AOI) within an image
nRet = self.ueye.is_AOI(
self.hCam, ueye.IS_AOI_IMAGE_GET_AOI, self.rectAOI, self.ueye.sizeof(self.rectAOI)
)
if nRet != self.ueye.IS_SUCCESS:
print("is_AOI ERROR")
self.width = self.rectAOI.s32Width
self.height = self.rectAOI.s32Height
# Prints out some information about the camera and the sensor
print("Camera model:\t\t", self.sInfo.strSensorName.decode("utf-8"))
print("Camera serial no.:\t", self.cInfo.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 nBitsPerPixel
nRet = self.ueye.is_AllocImageMem(
self.hCam, self.width, self.height, self.nBitsPerPixel, self.pcImageMemory, self.MemID
)
if nRet != self.ueye.IS_SUCCESS:
print("is_AllocImageMem ERROR")
else:
# Makes the specified image memory the active memory
nRet = self.ueye.is_SetImageMem(self.hCam, self.pcImageMemory, self.MemID)
if nRet != self.ueye.IS_SUCCESS:
print("is_SetImageMem ERROR")
else:
# Set the desired color mode
nRet = self.ueye.is_SetColorMode(self.hCam, self.m_nColorMode)
# Activates the camera's live video mode (free run mode)
nRet = self.ueye.is_CaptureVideo(self.hCam, self.ueye.IS_DONT_WAIT)
if nRet != self.ueye.IS_SUCCESS:
print("is_CaptureVideo ERROR")
# Enables the queue mode for existing image memory sequences
nRet = self.ueye.is_InquireImageMem(
self.hCam,
self.pcImageMemory,
self.MemID,
self.width,
self.height,
self.nBitsPerPixel,
self.pitch,
)
if nRet != 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.parent.start_live_mode()
def _start_data_thread(self):
self.thread_event = threading.Event()
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():
# In order to display the image in an OpenCV window we need to...
# ...extract the data of our image memory
array = ueye.get_data(
self.pcImageMemory,
self.width,
self.height,
self.nBitsPerPixel,
self.pitch,
copy=False,
)
# bytes_per_pixel = int(nBitsPerPixel / 8)
# ...reshape it in an numpy array...
frame = np.reshape(array, (self.height.value, self.width.value, self.bytes_per_pixel))
self.parent.image_data.put(frame)
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=frame)
time.sleep(0.1)
def on_trigger(self):
pass
# self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=self.parent.image_data.get())
class IDSCamera(PSIDetectorBase):
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
custom_prepare_cls = IDSCustomPrepare
image_data = Cpt(SetableSignal, value=np.empty((100, 100)), kind=Kind.omitted)
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
def __init__(
self,
prefix="",
*,
name: str,
camera_ID: int,
bits_per_pixel: int,
channels: int,
m_n_colormode: int,
kind=None,
parent=None,
device_manager=None,
**kwargs,
):
super().__init__(
prefix, name=name, kind=kind, parent=parent, device_manager=device_manager, **kwargs
)
self.camera_ID = camera_ID
self.bits_per_pixel = bits_per_pixel
self.channels = channels
self.m_n_colormode = m_n_colormode
#TODO fix connected and wait_for_connection
self.custom_prepare.on_connection_established()
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)
#self.custom_prepare.on_connection_established()
def destroy(self):
"""Extend Ophyds destroy function to kill the data thread"""
self.stop_live_mode()
super().destroy()
def start_live_mode(self):
if self.custom_prepare.data_thread is not None:
self.stop_live_mode()
self.custom_prepare._start_data_thread()
def stop_live_mode(self):
"""Stopping the camera live mode."""
if self.custom_prepare.thread_event is not None:
self.custom_prepare.thread_event.set()
if self.custom_prepare.data_thread is not None:
self.custom_prepare.data_thread.join()
self.custom_prepare.thread_event = None
self.custom_prepare.data_thread = None
"""from pyueye import ueye
import numpy as np
import cv2
import sys
import time
#---------------------------------------------------------------------------------------------------------------------------------------
#Variables
hCam = ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID
sInfo = ueye.SENSORINFO()
cInfo = ueye.CAMINFO()
pcImageMemory = ueye.c_mem_p()
MemID = ueye.int()
rectAOI = ueye.IS_RECT()
pitch = ueye.INT()
nBitsPerPixel = ueye.INT(24) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome
channels = 3 #3: channels for color mode(RGB); take 1 channel for monochrome
m_nColorMode = ueye.INT(1) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
bytes_per_pixel = int(nBitsPerPixel / 8)
ids_cam
...
deviceConfig:
camera_ID: 202
bits_per_pixel: 24
channels: 3
m_n_colormode: 1
#---------------------------------------------------------------------------------------------------------------------------------------
print("START")
print()
# Starts the driver and establishes the connection to the camera
nRet = ueye.is_InitCamera(hCam, None)
if nRet != 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 cInfo points to
nRet = ueye.is_GetCameraInfo(hCam, cInfo)
if nRet != ueye.IS_SUCCESS:
print("is_GetCameraInfo ERROR")
# You can query additional information about the sensor type used in the camera
nRet = ueye.is_GetSensorInfo(hCam, sInfo)
if nRet != ueye.IS_SUCCESS:
print("is_GetSensorInfo ERROR")
nRet = ueye.is_ResetToDefault( hCam)
if nRet != ueye.IS_SUCCESS:
print("is_ResetToDefault ERROR")
# Set display mode to DIB
nRet = ueye.is_SetDisplayMode(hCam, ueye.IS_SET_DM_DIB)
# Set the right color mode
if int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_BAYER:
# setup the color depth to the current windows setting
ueye.is_GetColorDepth(hCam, nBitsPerPixel, m_nColorMode)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("IS_COLORMODE_BAYER: ", )
print("\tm_nColorMode: \t\t", m_nColorMode)
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_CBYCRY:
# for color camera models use RGB32 mode
m_nColorMode = ueye.IS_CM_BGRA8_PACKED
nBitsPerPixel = ueye.INT(32)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("IS_COLORMODE_CBYCRY: ", )
print("\tm_nColorMode: \t\t", m_nColorMode)
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_MONOCHROME:
# for color camera models use RGB32 mode
m_nColorMode = ueye.IS_CM_MONO8
nBitsPerPixel = ueye.INT(8)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("IS_COLORMODE_MONOCHROME: ", )
print("\tm_nColorMode: \t\t", m_nColorMode)
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
print()
else:
# for monochrome camera models use Y8 mode
m_nColorMode = ueye.IS_CM_MONO8
nBitsPerPixel = ueye.INT(8)
bytes_per_pixel = int(nBitsPerPixel / 8)
print("else")
# Can be used to set the size and position of an "area of interest"(AOI) within an image
nRet = ueye.is_AOI(hCam, ueye.IS_AOI_IMAGE_GET_AOI, rectAOI, ueye.sizeof(rectAOI))
if nRet != ueye.IS_SUCCESS:
print("is_AOI ERROR")
width = rectAOI.s32Width
height = rectAOI.s32Height
# Prints out some information about the camera and the sensor
print("Camera model:\t\t", sInfo.strSensorName.decode('utf-8'))
print("Camera serial no.:\t", cInfo.SerNo.decode('utf-8'))
print("Maximum image width:\t", width)
print("Maximum image height:\t", height)
print()
#---------------------------------------------------------------------------------------------------------------------------------------
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by nBitsPerPixel
nRet = ueye.is_AllocImageMem(hCam, width, height, nBitsPerPixel, pcImageMemory, MemID)
if nRet != ueye.IS_SUCCESS:
print("is_AllocImageMem ERROR")
else:
# Makes the specified image memory the active memory
nRet = ueye.is_SetImageMem(hCam, pcImageMemory, MemID)
if nRet != ueye.IS_SUCCESS:
print("is_SetImageMem ERROR")
else:
# Set the desired color mode
nRet = ueye.is_SetColorMode(hCam, m_nColorMode)
# Activates the camera's live video mode (free run mode)
nRet = ueye.is_CaptureVideo(hCam, ueye.IS_DONT_WAIT)
if nRet != ueye.IS_SUCCESS:
print("is_CaptureVideo ERROR")
# Enables the queue mode for existing image memory sequences
nRet = ueye.is_InquireImageMem(hCam, pcImageMemory, MemID, width, height, nBitsPerPixel, pitch)
if nRet != ueye.IS_SUCCESS:
print("is_InquireImageMem ERROR")
else:
print("Press q to leave the programm")
startmeasureframerate=True
Gain = False
#---------------------------------------------------------------------------------------------------------------------------------------
# Continuous image display
while(nRet == ueye.IS_SUCCESS):
# In order to display the image in an OpenCV window we need to...
# ...extract the data of our image memory
array = ueye.get_data(pcImageMemory, width, height, nBitsPerPixel, pitch, copy=False)
# bytes_per_pixel = int(nBitsPerPixel / 8)
# ...reshape it in an numpy array...
frame = np.reshape(array,(height.value, width.value, bytes_per_pixel))
# ...resize the image by a half
frame = cv2.resize(frame,(0,0),fx=0.5, fy=0.5)
#---------------------------------------------------------------------------------------------------------------------------------------
#Include image data processing here
#---------------------------------------------------------------------------------------------------------------------------------------
#...and finally display it
cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame)
if startmeasureframerate:
starttime = time.time()
startmeasureframerate=False
framenumber=0
if time.time() > starttime+5:
print(f"Caught {framenumber/5} frames per second")
startmeasureframerate=True
Gain = ~Gain
if Gain:
nRet = ueye.is_SetGainBoost(hCam, 1)
else:
nRet = ueye.is_SetGainBoost(hCam, 0)
print(f"Gain setting status {nRet}")
#...and finally display it
cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame)
framenumber+=1
time.sleep(0.1)
# Press q if you want to end the loop
if (cv2.waitKey(1) & 0xFF) == ord('q'):
break
#---------------------------------------------------------------------------------------------------------------------------------------
# Releases an image memory that was allocated using is_AllocImageMem() and removes it from the driver management
ueye.is_FreeImageMem(hCam, pcImageMemory, MemID)
# Disables the hCam camera handle and releases the data structures and memory areas taken up by the uEye camera
ueye.is_ExitCamera(hCam)
# Destroys the OpenCv windows
cv2.destroyAllWindows()
print()
print("END")
"""

View File

@@ -0,0 +1,83 @@
import time
import numpy as np
from bec_lib import bec_logger
from ophyd import Kind, Signal
from ophyd.utils import ReadOnlyError
from ophyd_devices.utils.bec_device_base import BECDeviceBase
logger = bec_logger.logger
# Readout precision for Setable/ReadOnlySignal signals
PRECISION = 3
class ReadOnlySignal(Signal):
"""Setable signal for simulated devices.
The signal will store the value in sim_state of the SimulatedData class of the parent device.
It will also return the value from sim_state when get is called. Compared to the ReadOnlySignal,
this signal can be written to.
The setable signal inherits from the Signal class of ophyd, thus the class attribute needs to be
initiated as a Component (class from ophyd).
>>> signal = SetableSignal(name="signal", parent=parent, value=0)
Parameters
----------
name (string) : Name of the signal
parent (object) : Parent object of the signal, default none.
value (any) : Initial value of the signal, default 0.
kind (int) : Kind of the signal, default Kind.normal.
precision (float) : Precision of the signal, default PRECISION.
"""
def __init__(
self,
name: str,
*args,
fcn: callable,
kind: int = Kind.normal,
precision: float = PRECISION,
**kwargs,
):
super().__init__(*args, name=name, value=value, kind=kind, **kwargs)
self._metadata.update(connected=True, write_access=False)
self._value = None
self.precision = precision
self.fcn = fcn
# pylint: disable=arguments-differ
def get(self):
"""Get the current position of the simulated device.
Core function for signal.
"""
self._value = self.fcn()
return self._value
# pylint: disable=arguments-differ
def put(self, value):
"""Put the value to the simulated device.
Core function for signal.
"""
self._update_sim_state(value)
self._value = value
def describe(self):
"""Describe the readback signal.
Core function for signal.
"""
res = super().describe()
if self.precision is not None:
res[self.name]["precision"] = self.precision
return res
@property
def timestamp(self):
"""Timestamp of the readback value"""
return self._get_timestamp()

View File

@@ -0,0 +1,221 @@
"""Eiger detector for cSAXS beamline at the Swiss Light Source.
16bit mode supports 8e7 counts/s per pixel,
you will never have more than 12bit subframes, which means 4000 counts per subframe.
32bit mode supports 2e7 counts/s per pixel,
you will never have more than 24bit subframe, which means 16.7 million counts per subframe.
"""
import enum
from typing import TYPE_CHECKING
from bec_lib.devicemanager import ScanInfo
from bec_lib.logger import bec_logger
from jfjoch_client.models.dataset_settings import DatasetSettings
from jfjoch_client.models.detector_settings import DetectorSettings
from jfjoch_client.models.detector_timing import DetectorTiming
from ophyd import DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.jungfraujoch.jungfrau_joch_client import JungfrauJochClient
from csaxs_bec.devices.jungfraujoch.readout_constants import EIGER9M_READOUT_TIME_32BIT
if TYPE_CHECKING: # pragma no cover
from bec_lib.devicemanager import ScanInfo
from bec_server.device_server.device_server import DeviceManagerDS
logger = bec_logger.logger
class EigerCSAXSBitDepth(int, enum.Enum):
"""Bit depth for EIGER detector at cSAXS beamline."""
BIT_DEPTH_16 = 16
BIT_DEPTH_32 = 32
class Eiger9MCSAXS(PSIDeviceBase):
"""
-----------
JungfrauJoch - one needs to connect to the jfj-server (sls-jfjoch-001)
Relevant commands for debugging:
sudo systemctl restart jfjoch_broker
sudo systemctl status jfjoch_broker
Some additional notes:
------------
- If energy on JFJ is set via DetectorSettings, the one in DatasetSettings will be ignored.
- One can set this initially, and then set it to none in DetectorSettings such that any update in DatasetSettings will be considered.
- IMPORTANT: Any change in energy will be detector. It will be best to have a check ourselves with a certain tolerance of ~ % to not constantly update the energy.
- in 'gating' mode, frame_time_us and count_time_us are not used.
- The image_time_us of the DatasetSettings and the frame_time_us of the DetectorSettings need to be the same.
- The difference between frame_time_us and count_time_us is the readout time.
- 16bit and 32bit, when do we switch?
- If switching is desired, the readout time needs to be adapted as a function of internal exposure time, and bit depth.
This can be up to ~400us for 32bit, and long exposures. This needs to be discussed!
- 16bit mode supports 8e7 counts/s per pixel,
It needs to be se to None.
------------
Eiger - if power cycling is needed. Use a combination of commands that connect to the chip, and the conda package.
The package is available via:
cd /sls/X12SA/data/gac-x12sa/erik/micromamba
source setup_9m.sh
------------
Nice to set high voltage low first, from conda package (sls_detector_package)
p highvoltage 0 or 150 (operational)
g highvoltage
# Put high voltage to 0 before power cylcing it.
telnet bchip500
cd power_control_user/
./on
./off
"""
########################################
# Beamline Specific Implementations #
########################################
USER_ACCESS = ["jfj_client"]
def __init__(
self,
name: str,
host: str = "http://sls-jfjoch-001",
port: int = 8080,
scan_info: ScanInfo = None,
device_manager=None,
**kwargs,
):
"""
Initialize the PSI Device Base class.
Args:
name (str) : Name of the device
scan_info (ScanInfo): The scan info to use.
"""
super().__init__(name=name, scan_info=scan_info, **kwargs)
self._host = f"{host}:{port}"
self.jfj_client = JungfrauJochClient(host=self._host, parent=self)
self.device_manager = device_manager
if self.device_manager is not None:
self.device_manager: DeviceManagerDS
self._bit_depth = 16
self.frame_time = 500e-6 # 500us, will be ignored in DetectorTiming.Gated
self.count_time = 300e-6 # 480us, will be ignored in DetectorTiming.Gated
# If not gated, frame_time and count_time will be used and logic has to be adjusted
self._timing = DetectorTiming.GATED
def on_init(self) -> None:
"""
Called when the device is initialized.
No siganls are connected at this point,
thus should not be set here but in 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.
"""
# Stop first in case it was in an uncertain state (i.e. measuring)
logger.info(f"On connected for {self.name}")
self.jfj_client.stop()
# Try to connect, needs to be in Inactive or Error state
self.jfj_client.connect_and_initialise(timeout=5)
# Set energy threshold for EIGER detector
threshold_ke_v = 6.200 # Grab this from mono energy pseudo device
# Energy threshold provided in DetectorSettings, than it is ignored in DatasetSettings
# This sets the energy threshold for the EIGER detector
settings = DetectorSettings(
frame_time_us=int(self.frame_time * 1e6),
count_time_us=int(self.count_time * 1e6),
eiger_bit_depth=self._bit_depth,
eiger_threshold_ke_v=threshold_ke_v,
timing=self._timing,
)
self.jfj_client.set_detector_settings(settings)
# Second call is needed to ensure that eiger_threshold_ke_v is set to None
# if not, DatasetSettings for eiger_threshold_ke_v will be ignored
# settings = DetectorSettings(
# frame_time_us=int(self.frame_time * 1e6),
# count_time_us=int(self.count_time * 1e6),
# eiger_bit_depth=self._bit_depth,
# timing=self._timing,
# )
# self.jfj_client.set_detector_settings(settings)
def on_stage(self) -> DeviceStatus | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info object.
"""
# Delay generator ddg_jfj needs to be activate
ddg = self.device_manager.devices.get("ddg_jfj", None)
if ddg is None:
logger.warning("ddg_jfj not found in device manager")
raise ValueError("ddg_jfj not found in device manager")
ntrigger = ddg.compute_num_trigger()
if self.scan_info.msg.scan_type == "step":
# Energy threshold provided in DetectorSettings, than it is ignored in DatasetSettings
print()
data_settings = DatasetSettings(
image_time_us=int(self.frame_time * 1e6), # this is frame_time
ntrigger=ntrigger,
beam_x_pxl=0,
beam_y_pxl=0,
detector_distance_mm=100,
incident_energy_ke_v=10.00,
# file_prefix = full_path_to_file,
)
# status = self.task_handler.submit_task(
# self.jfj_client.start, task_args=(data_settings,), run=True
# )
# return status
self.jfj_client.start(settings=data_settings)
# This method computes trigger_pulse_width, ntriggers and bit_depth
# trigger_pulse_width -> image_time in s (image_time_us)
# ntriggers -> number of images per trigger
# bit_depth -> 16 or 32
def on_unstage(self) -> DeviceStatus | None:
"""Called while unstaging the device."""
def on_pre_scan(self) -> DeviceStatus | None:
"""Called right before the scan starts on all devices automatically."""
def on_trigger(self) -> DeviceStatus | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | None:
"""Called to inquire if a device has completed a scans."""
def wait_for_complete():
timeout = 10
for _ in range(timeout):
try:
self.jfj_client.wait_till_done(timeout=1)
except TimeoutError:
continue
except Exception as e:
raise ValueError(f"Error in complete for {self.name}, exception: {e}") from e
else:
break
status = self.task_handler.submit_task(wait_for_complete, run=True)
return status
def on_kickoff(self) -> DeviceStatus | 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."""
self.jfj_client.stop()
self.task_handler.shutdown()

View File

@@ -0,0 +1,193 @@
"""Module with client interface for the Jungfrau Joch detector API"""
import enum
import math
import time
from bec_lib.logger import bec_logger
from jfjoch_client.api.default_api import DefaultApi
from jfjoch_client.api_client import ApiClient
from jfjoch_client.api_response import ApiResponse
from jfjoch_client.configuration import Configuration
from jfjoch_client.models.broker_status import BrokerStatus
from jfjoch_client.models.dataset_settings import DatasetSettings
from jfjoch_client.models.detector_settings import DetectorSettings
from ophyd import Device
logger = bec_logger.logger
class JungfrauJochClientError(Exception):
"""Base class for exceptions in this module."""
class DetectorState(str, enum.Enum):
"""Possible Detector states for Jungfrau Joch detector"""
INACTIVE = "Inactive"
IDLE = "Idle"
BUSY = "Busy"
MEASURING = "Measuring"
PEDESTAL = "Pedestal"
ERROR = "Error"
class ResponseWaitDone(int, enum.Enum):
"""Response state for Jungfrau Joch detector wait till done call"""
DETECTOR_IDLE = 200
TIMEOUT_PARAM_OUT_OF_BOUNDS = 400
JUNGFRAU_ERROR = 500
DETECTOR_INACTIVE = 502
TIMEOUT_REACHED = 504
class ResponseCancelDone(int, enum.Enum):
"""HTTP Response for cancel post"""
CANCEL_SENT_TO_FPGA = 200
class JungfrauJochClient:
"""Thin wrapper around the Jungfrau Joch API client.
sudo systemctl restart jfjoch_broker
sudo systemctl status jfjoch_broker
It looks as if the detector is not being stopped properly.
One module remains running, how can we restart the detector?
"""
def __init__(
self, host: str = "http://sls-jfjoch-001:8080", parent: Device | None = None
) -> None:
self._initialised = False
configuration = Configuration(host=host)
api_client = ApiClient(configuration)
self.api = DefaultApi(api_client)
self._parent_name = parent.name if parent else self.__class__.__name__
@property
def jjf_state(self) -> BrokerStatus:
"""Get the status of JungfrauJoch"""
response = self.api.status_get()
return BrokerStatus(**response.to_dict())
@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_detector_state(self) -> DetectorState:
"""Get the status of JungfrauJoch"""
return DetectorState(self.jjf_state.state)
def connect_and_initialise(self, timeout: int = 5) -> None:
"""Check if JungfrauJoch is connected and ready to receive commands"""
status = self.get_detector_state()
if status != DetectorState.IDLE:
self.api.initialize_post()
self.wait_till_done(timeout) # Blocking call
self.initialised = True
def set_detector_settings(self, settings: dict | DetectorSettings) -> 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]:
time.sleep(1) # This can be improved.... #TODO
state = self.api.status_get().state
if state not in [DetectorState.IDLE, DetectorState.ERROR, DetectorState.INACTIVE]:
raise JungfrauJochClientError(
f"Error in {self._parent_name}. Detector must be in IDLE, ERROR or INACTIVE state to set settings. Current state: {state}"
)
if isinstance(settings, dict):
settings = DetectorSettings(**settings)
self.api.config_detector_put(detector_settings=settings)
# Check with Filip if this call is blocking! also check if put_with_http is better
def start(self, settings: dict | DatasetSettings) -> None:
"""Start the mesaurement. DatasetSettings must be provided, and JungfrauJoch must be in IDLE state.
The method call is blocking and JungfrauJoch will be ready to measure after the call resolves.
Args:
settings (dict): dictionary of settings
Please check the DataSettings class for the available settings. Minimum required settings are
beam_x_pxl, beam_y_pxl, detector_distance_mm, incident_energy_keV.
"""
state = self.get_detector_state()
if state != DetectorState.IDLE:
raise JungfrauJochClientError(
f"Error in {self._parent_name}. Detector must be in IDLE state to set settings. Current state: {state}"
)
if isinstance(settings, dict):
settings = DatasetSettings(**settings)
try:
res: ApiResponse = self.api.start_post_with_http_info(dataset_settings=settings)
if res.status_code != 200:
response = f"Error in {self._parent_name}, while setting measurement settings {settings}, response: {res}"
raise JungfrauJochClientError(response)
except JungfrauJochClientError as e:
logger.error(e)
raise e
except Exception as e:
response = f"Error in {self._parent_name}, while setting measurement settings {settings}, exception: {e}"
logger.error(response)
raise JungfrauJochClientError(response) from e
def stop(self) -> None:
"""Stop the acquisition"""
try:
res: ApiResponse = self.api.cancel_post_with_http_info() # Should we use a timeout?
if res.status_code != ResponseCancelDone.CANCEL_SENT_TO_FPGA:
response = f"Error in device {self._parent_name} while stopping the measurement. API Response: {res}"
raise JungfrauJochClientError(response)
except JungfrauJochClientError as e:
raise e
except Exception as exc:
raise JungfrauJochClientError from exc
def wait_till_done(self, timeout: int = 5) -> None:
"""Wait for JungfrauJoch to be in Idle state. Blocking call with timeout.
Args:
timeout (int): timeout in seconds
"""
try:
response = self.api.wait_till_done_post_with_http_info(math.ceil(timeout / 2))
if response.status_code == ResponseWaitDone.DETECTOR_IDLE:
return
logger.info(
f"Waiting for device {self._parent_name}, jungfrau joch to become IDLE, "
f"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:
return
except Exception as e:
logger.error(
f"Error in device {self._parent_name} while waiting for JungfrauJoch to initialise: {e}"
)
raise JungfrauJochClientError(
f"Error in device {self._parent_name} while waiting for JungfrauJoch to initialise. Exception: {e}"
) from e
# If the response is IDLE, this part is never reached. We will raise a TimeoutError.
msg = (
f"TimeoutError in device {self._parent_name}, failed to initialise JungfrauJoch with status:"
f"{response.status_code}; response msg {response}"
)
logger.error(msg)
raise TimeoutError(msg)

View File

@@ -0,0 +1,4 @@
"""Readout constants for all relevant detectors at cSAXS beamline."""
# -> should 20e-6, 20us : parallel vs nonparallel, exact values to be checked
EIGER9M_READOUT_TIME_32BIT = 100e-6 # s

View File

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

View File

@@ -0,0 +1,588 @@
import functools
import threading
import time
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
def channel_checked(fcn):
"""Decorator to catch attempted access to channels that are not available."""
@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 NpointError(Exception):
"""
Base class for Npoint errors.
"""
class NPointController(Controller):
"""
Controller for nPoint piezo stages. This class inherits from the Controller class
and provides a singleton interface to the nPoint controller.
"""
_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 show_all(self) -> None:
"""Display current status of all channels
Returns:
None
"""
if not self.connected:
print("npoint controller is currently disabled.")
return
print(f"Connected to controller at {self._socket_host}:{self._socket_port}")
t = PrettyTable()
t.field_names = ["Channel", "Range", "Position", "Target"]
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)
@channel_checked
def _get_range(self, channel: int) -> int:
"""Get the range of the specified channel axis.
Args:
channel (int): Channel for which the range should be requested.
Raises:
RuntimeError: Raised if the received message doesn't have the expected number of bytes (10).
Returns:
int: Range
"""
# for first channel: 0x11 83 10 78
addr = self._channel_base.copy()
addr.extend([f"{16 + 16 * channel:x}", self._range_offset])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
if len(recvd) != 10:
raise RuntimeError(
f"Received buffer is corrupted. Expected 10 bytes and instead got {len(recvd)}"
)
device_range = self._hex_list_to_int(recvd[5:-1], signed=False)
return device_range
@channel_checked
def get_current_pos(self, channel: int) -> float:
# for first channel: 0x11 83 13 34
addr = self._channel_base.copy()
addr.extend([f"{19 + 16 * channel:x}", "34"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
pos_buffer = recvd[5:-1]
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
return pos
@channel_checked
def set_target_pos(self, channel: int, pos: float) -> None:
# for first channel: 0x11 83 12 18 00 00 00 00
addr = self._channel_base.copy()
addr.extend([f"{18 + channel * 16:x}", "18"])
target = int(round(1048574 / 100 * pos))
data = [f"{m:02x}" for m in target.to_bytes(4, byteorder="big", signed=True)]
send_buffer = self.__write_single_location_buffer(addr, data)
self._put(send_buffer)
@channel_checked
def get_target_pos(self, channel: int) -> float:
# for first channel: 0x11 83 12 18
addr = self._channel_base.copy()
addr.extend([f"{18 + channel * 16:x}", "18"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
pos_buffer = recvd[5:-1]
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
return pos
@channel_checked
def _set_servo(self, channel: int, enable: bool) -> None:
print("Not tested")
return
# # for first channel: 0x11 83 10 84 00 00 00 00
# addr = self._channel_base.copy()
# addr.extend([f"{16 + channel * 16:x}", "84"])
# if enable:
# data = ["00"] * 3 + ["01"]
# else:
# data = ["00"] * 4
# send_buffer = self.__write_single_location_buffer(addr, data)
# self._put(send_buffer)
@channel_checked
def _get_servo(self, channel: int) -> int:
# for first channel: 0x11 83 10 84 00 00 00 00
addr = self._channel_base.copy()
addr.extend([f"{16 + channel * 16:x}", "84"])
send_buffer = self.__read_single_location_buffer(addr)
recvd = self._put_and_receive(send_buffer)
buffer = recvd[5:-1]
status = self._hex_list_to_int(buffer)
return status
@threadlocked
def _put(self, buffer: list) -> None:
"""Translates a list of hex values to bytes and sends them to the socket.
Args:
buffer (list): List of hex values without leading 0x
Returns:
None
"""
buffer = b"".join([bytes.fromhex(m) for m in buffer])
self.sock.put(buffer)
@threadlocked
def _put_and_receive(self, msg_hex_list: list) -> list:
"""Send msg to socket and wait for a reply.
Args:
msg_hex_list (list): List of hex values without leading 0x.
Returns:
list: Received message as a list of hex values
"""
buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list])
self.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
def _verify_received_msg(self, in_list: list, out_list: list) -> None:
"""Ensure that the first address bits of sent and received messages are the same.
Args:
in_list (list): list containing the sent message
out_list (list): list containing the received message
Raises:
RuntimeError: Raised if first two address bits of 'in' and 'out' are not identical
Returns:
None
"""
# first, translate hex (str) values to int
in_list_int = [int(val, 16) for val in in_list]
out_list_int = [int(val, 16) for val in out_list]
# first ints of the reply should be the same. Otherwise something went wrong
if not in_list_int[:2] == out_list_int[:2]:
raise RuntimeError("Connection failure. Please restart the controller.")
def _check_channel(self, channel: int) -> None:
if channel >= self._axes_per_controller:
raise ValueError(
f"Channel {channel+1} exceeds the available number of channels ({self._axes_per_controller})"
)
@staticmethod
def _hex_list_to_int(in_buffer: list, byteorder="little", signed=True) -> int:
"""Translate hex list to int.
Args:
in_buffer (list): Input buffer; received as list of hex values
byteorder (str, optional): Byteorder of in_buffer. Defaults to "little".
signed (bool, optional): Whether the hex list represents a signed int. Defaults to True.
Returns:
int: Translated integer.
"""
if byteorder == "little":
in_buffer.reverse()
# make sure that all hex strings have the same format ("FF")
val_hex = [f"{int(m, 16):02x}" for m in in_buffer]
val_bytes = [bytes.fromhex(m) for m in val_hex]
val = int.from_bytes(b"".join(val_bytes), byteorder="big", signed=signed)
return val
@staticmethod
def __read_single_location_buffer(addr) -> list:
"""Prepare buffer for reading from a single memory location (hex address).
Number of bytes: 6
Format: 0xA0 [addr] 0x55
Return Value: 0xA0 [addr] [data] 0x55
Sample Hex Transmission from PC to LC.400: A0 18 12 83 11 55
Sample Hex Return Transmission from LC.400 to PC: A0 18 12 83 11 64 00 00 00 55
Args:
addr (list): Hex address to read from
Returns:
list: List of hex values representing the read instruction.
"""
buffer = []
buffer.append(NPointController._read_single_loc_bit)
if isinstance(addr, list):
addr.reverse()
buffer.extend(addr)
else:
buffer.append(addr)
buffer.append(NPointController._trailing_bit)
return buffer
@staticmethod
def __write_single_location_buffer(addr: list, data: list) -> list:
"""Prepare buffer for writing to a single memory location (hex address).
Number of bytes: 10
Format: 0xA2 [addr] [data] 0x55
Return Value: none
Sample Hex Transmission from PC to C.400: A2 18 12 83 11 E8 03 00 00 55
Args:
addr (list): List of hex values representing the address to write to.
data (list): List of hex values representing the data that should be written.
Returns:
list: List of hex values representing the write instruction.
"""
buffer = []
buffer.append(NPointController._write_single_loc_bit)
if isinstance(addr, list):
addr.reverse()
buffer.extend(addr)
else:
buffer.append(addr)
if isinstance(data, list):
data.reverse()
buffer.extend(data)
else:
buffer.append(data)
buffer.append(NPointController._trailing_bit)
return buffer
@staticmethod
def __read_array():
raise NotImplementedError
@staticmethod
def __write_next_command():
raise NotImplementedError
def __del__(self):
if self.connected:
print("Closing npoint socket")
self.off()
class NpointSignalBase(SocketSignal):
"""
Base class for nPoint signals.
"""
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
class NpointSignalRO(NpointSignalBase):
"""
Base class for read-only signals.
"""
def __init__(self, signal_name, **kwargs):
super().__init__(signal_name, **kwargs)
self._metadata["write_access"] = False
@threadlocked
def _socket_set(self, val):
raise ReadOnlyError("Read-only signals cannot be set")
class 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:
"""
Set the motor_is_moving signal to the specified value.
Args:
value (int): 1 if the motor is moving, 0 otherwise.
"""
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 limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
walk.item._metadata["connected"] = self.controller.connected
@raise_if_disconnected
def move(self, position, wait=True, **kwargs):
"""Move to a specified position, optionally waiting for motion to
complete.
Parameters
----------
position
Position to move to
moved_cb : callable
Call this callback when movement has finished. This callback must
accept one keyword argument: 'obj' which will be set to this
positioner instance.
timeout : float, optional
Maximum time to wait for the motion. If None, the default timeout
for this positioner is used.
Returns
-------
status : MoveStatus
Raises
------
TimeoutError
When motion takes longer than `timeout`
ValueError
On invalid positions
RuntimeError
If motion fails other than timing out
"""
self._started_moving = False
timeout = kwargs.pop("timeout", 10)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)
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 (str): Single-character axis identifier.
"""
if isinstance(val, str):
if len(val) != 1:
raise ValueError("Only single-character axis_Ids are supported.")
self._axis_Id_alpha = val
self._axis_Id_numeric = ord(val.lower()) - 97
else:
raise TypeError(f"Expected value of type str but received {type(val)}")
@property
def axis_Id_numeric(self):
"""
Return the numeric value of the axis_Id.
"""
return self._axis_Id_numeric
@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__":
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

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