Compare commits

...

217 Commits

Author SHA1 Message Date
x01da 4a00d3baeb Automatic backup triggered by new deployment
CI for debye_bec / test (push) Successful in 58s
2026-06-02 13:29:38 +02:00
x01da 52d97b2d29 fix: mo1_bragg calculator
CI for debye_bec / test (push) Successful in 57s
CI for debye_bec / test (pull_request) Failing after 38s
2026-06-02 13:24:14 +02:00
x01da 40309491b0 fix: nexus file structure for nidaq continuous scan
CI for debye_bec / test (pull_request) Successful in 1m1s
CI for debye_bec / test (push) Successful in 57s
2026-05-27 14:23:21 +02:00
appel_c 2f0265fff7 refactor: deprecate old nidaq_cont_scan implementation
CI for debye_bec / test (pull_request) Successful in 53s
CI for debye_bec / test (push) Successful in 56s
2026-05-27 13:23:43 +02:00
x01da 2d21eb90fe fix: fix scan_done PV with RBV
CI for debye_bec / test (pull_request) Successful in 53s
CI for debye_bec / test (push) Successful in 56s
2026-05-27 11:09:30 +02:00
x01da 8ddf67e817 fix: catch positions is None
CI for debye_bec / test (push) Successful in 1m3s
CI for debye_bec / test (pull_request) Successful in 1m3s
2026-05-27 10:14:49 +02:00
x01da 617cca71a5 fix: nexus structure safe guards.
CI for debye_bec / test (push) Successful in 56s
CI for debye_bec / test (pull_request) Successful in 56s
2026-05-27 10:06:21 +02:00
x01da 7e20d46881 fix: fix bug in mo1_bragg stage
CI for debye_bec / test (pull_request) Successful in 56s
CI for debye_bec / test (push) Successful in 57s
2026-05-27 09:09:45 +02:00
appel_c 74ff173f98 refactor: cleanup, fix tests
CI for debye_bec / test (pull_request) Successful in 53s
CI for debye_bec / test (push) Successful in 1m1s
2026-05-27 08:30:05 +02:00
x01da 87758710d9 fix: fixes from tests at the beamline 2026-05-27 08:30:05 +02:00
appel_c 8bc36ed6a2 refactor(mo1-bragg): migrate mo1_bragg to scans v4 2026-05-27 08:30:05 +02:00
appel_c 78d58ad26f refactor(hutch-cam): migrate hutch cam to scans v4 2026-05-27 08:30:05 +02:00
appel_c 359ef0b6d7 refactor(nidaq): migrate nidaq to scans v4 2026-05-22 10:10:20 +02:00
appel_c 262a0b6318 refactor(pilatus): migrate to scans v4 interface 2026-05-22 10:04:49 +02:00
appel_c 98d5c22667 refactor(nidaq): migrate NIDAQ to v4 scan_info 2026-05-21 17:41:49 +02:00
wakonig_k 0e77dd5679 feat: add NIDAQ continuous scan v4 implementation and update related tests
CI for debye_bec / test (pull_request) Successful in 54s
CI for debye_bec / test (push) Successful in 56s
2026-05-21 17:16:50 +02:00
wakonig_k f8e5b5e073 feat: migrate XAS simple scan to V4 implementation and remove mono bragg scans 2026-05-21 17:16:50 +02:00
wakonig_k 70750d6aa1 chore(nidaq): improve readbability of nidaq signal definition
CI for debye_bec / test (pull_request) Successful in 1m4s
CI for debye_bec / test (push) Failing after 27s
2026-05-21 17:01:38 +02:00
perl_d 94aca18a22 Updating to template version 1.4.0
CI for debye_bec / test (pull_request) Successful in 1m12s
CI for debye_bec / test (push) Successful in 1m15s
2026-05-20 11:00:38 +02:00
perl_d ba82fd1715 Updating to template version 1.3.2
CI for debye_bec / test (pull_request) Successful in 1m7s
CI for debye_bec / test (push) Successful in 1m4s
2026-05-20 10:39:21 +02:00
hitz_s c998c06b41 Merge pull request 'refactoring' (#73) from feat/digital_twin into main
CI for debye_bec / test (push) Successful in 1m7s
Reviewed-on: #73
2026-05-19 10:59:21 +02:00
x01da 8b8138ec05 refactoring
CI for debye_bec / test (push) Successful in 1m7s
CI for debye_bec / test (pull_request) Successful in 1m6s
2026-05-19 10:57:59 +02:00
x01da ef4c82262c refactoring
CI for debye_bec / test (pull_request) Successful in 1m6s
CI for debye_bec / test (push) Successful in 1m7s
2026-05-19 10:45:42 +02:00
x01da 5a54675f1e refactoring
CI for debye_bec / test (push) Successful in 1m16s
CI for debye_bec / test (pull_request) Successful in 1m14s
2026-05-19 08:42:14 +02:00
x01da 62582da1d9 refactoring
CI for debye_bec / test (push) Successful in 1m11s
2026-05-19 06:44:04 +02:00
x01da 6b5ff49b04 refactoring
CI for debye_bec / test (push) Successful in 1m7s
2026-05-18 10:38:01 +02:00
hitz_s bda7a688e1 Merge pull request 'feat/various-improvements' (#69) from feat/various-improvements into main
CI for debye_bec / test (push) Successful in 1m8s
Reviewed-on: #69
2026-05-18 09:05:24 +02:00
x01da 3e80b0fd8d Automatic generated files for digital twin
CI for debye_bec / test (pull_request) Successful in 1m5s
CI for debye_bec / test (push) Successful in 1m7s
2026-05-18 09:03:26 +02:00
x01da 823142b296 Add config checker 2026-05-18 09:03:26 +02:00
appel_c 5d6d0535af test: remove scan_motors to fix tests
CI for debye_bec / test (pull_request) Successful in 1m4s
CI for debye_bec / test (push) Successful in 1m7s
2026-05-18 08:36:30 +02:00
x01da 7797ce1980 fix mo1_bragg test
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m5s
2026-05-18 07:38:46 +02:00
x01da 7b1ea281a3 Changed from EpicsMotor to EpicsMotorEC
CI for debye_bec / test (push) Failing after 1m9s
CI for debye_bec / test (pull_request) Failing after 1m10s
2026-05-18 06:57:11 +02:00
x01da a75320ccbc refactoring, bugfix theme move buttons
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m7s
2026-05-13 16:17:12 +02:00
x01da c04d829fc6 wip: digital twin
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m6s
2026-05-13 13:29:09 +02:00
x01da 5d862e1d5b Adding additional signals for nidaq and mono
CI for debye_bec / test (pull_request) Failing after 1m1s
CI for debye_bec / test (push) Failing after 1m8s
2026-05-13 08:57:56 +02:00
x01da 3e959e6c5d wip: digital twin
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m9s
2026-05-11 10:16:42 +02:00
x01da fe43dafac8 wip: digital twin
CI for debye_bec / test (push) Failing after 1m1s
CI for debye_bec / test (pull_request) Failing after 1m0s
2026-05-07 15:49:13 +02:00
x01da 8493b60468 wip: digital twin
CI for debye_bec / test (push) Failing after 1m0s
CI for debye_bec / test (pull_request) Failing after 1m2s
2026-05-07 14:52:54 +02:00
x01da 0365d6eac7 refactoring
CI for debye_bec / test (pull_request) Failing after 1m4s
CI for debye_bec / test (push) Failing after 1m5s
2026-05-07 07:32:43 +02:00
x01da 6da7e665b3 wip: digital twin
CI for debye_bec / test (push) Failing after 1m0s
CI for debye_bec / test (pull_request) Failing after 1m1s
2026-05-06 16:12:25 +02:00
x01da b0a7d6905c wip: digital twin
CI for debye_bec / test (push) Failing after 1m0s
CI for debye_bec / test (pull_request) Failing after 1m3s
2026-05-06 14:53:06 +02:00
x01da acc5e320cf wip: digital twin
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 1m0s
2026-05-06 12:58:45 +02:00
x01da 3d2485aea7 wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m3s
CI for debye_bec / test (push) Failing after 1m35s
2026-05-05 15:32:01 +02:00
x01da 131d7f7f3e Updated nexus structure
CI for debye_bec / test (push) Failing after 1m9s
CI for debye_bec / test (pull_request) Failing after 1m35s
2026-05-05 13:38:56 +02:00
x01da 16bd819a9f wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m3s
CI for debye_bec / test (push) Failing after 1m5s
2026-05-04 12:46:50 +02:00
x01da b14f2c0fe3 wip: digital twin
CI for debye_bec / test (push) Failing after 1m11s
CI for debye_bec / test (pull_request) Failing after 1m9s
2026-05-04 06:52:48 +02:00
x01da 09799554ba signal name correction 2026-05-04 06:52:30 +02:00
x01da 3d756469e3 Change of kind for angle signal 2026-05-04 06:52:01 +02:00
x01da 4ca59c57be wip: digital twin
CI for debye_bec / test (push) Failing after 1m1s
CI for debye_bec / test (pull_request) Failing after 1m4s
2026-04-30 15:46:05 +02:00
x01da 576c59f5e5 Added angle signal for digital twin 2026-04-30 15:45:28 +02:00
x01da ce3f231276 wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m4s
CI for debye_bec / test (push) Failing after 1m6s
2026-04-30 14:47:16 +02:00
x01da 274bb9154c wip: digital twin
CI for debye_bec / test (push) Failing after 1m5s
CI for debye_bec / test (pull_request) Failing after 1m8s
2026-04-30 08:11:33 +02:00
x01da 282756288f Adding xrt library for digital twin 2026-04-30 08:11:09 +02:00
x01da 101954476c wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m1s
CI for debye_bec / test (push) Failing after 1m6s
2026-04-29 16:36:52 +02:00
x01da 339adab06c wip: digital twin widget
CI for debye_bec / test (push) Failing after 1m5s
CI for debye_bec / test (pull_request) Failing after 1m2s
2026-04-29 14:22:10 +02:00
x01da 588152871c wip: move components to label 2026-04-29 14:21:46 +02:00
x01da f3fbdbf5f2 update of config and nexus structure 2026-04-29 14:18:48 +02:00
x01da 7fb68d67de Adding bender radius signal
CI for debye_bec / test (pull_request) Failing after 1m4s
CI for debye_bec / test (push) Failing after 1m5s
2026-04-28 15:16:59 +02:00
x01da 3132658396 Signal name change (consistency) 2026-04-28 15:16:40 +02:00
x01da 6e149a6a73 Adding string representation of status 2026-04-28 15:16:03 +02:00
x01da 204e2827eb Adding signals of additional nidaq signals 2026-04-28 15:14:48 +02:00
x01da adf3a8ab11 Renaming of offset signals
CI for debye_bec / test (push) Failing after 1m4s
CI for debye_bec / test (pull_request) Failing after 59s
2026-04-28 11:21:47 +02:00
x01da 6a2d813506 Corrected ot_rotx name 2026-04-28 10:10:13 +02:00
x01da 4103b3153a feat: Added frontend absorber
CI for debye_bec / test (push) Failing after 1m3s
CI for debye_bec / test (pull_request) Failing after 2m16s
2026-04-27 15:20:02 +02:00
wakonig_k bee4562ab1 chore: remove deprecated scan motors
CI for debye_bec / test (pull_request) Successful in 1m1s
CI for debye_bec / test (push) Successful in 1m2s
2026-04-21 13:41:12 +02:00
x01da c428bb5a87 Change of order of nidaq signals
CI for debye_bec / test (push) Successful in 1m8s
CI for debye_bec / test (pull_request) Successful in 1m9s
2026-04-02 14:22:29 +02:00
x01da 632d554245 add additional signals to nidaq
CI for debye_bec / test (push) Successful in 1m3s
CI for debye_bec / test (pull_request) Successful in 1m6s
2026-03-25 09:48:25 +01:00
x01da efd8842540 do not close pilatus curtain after measurements
CI for debye_bec / test (push) Successful in 1m1s
CI for debye_bec / test (pull_request) Successful in 1m3s
2026-03-02 13:28:24 +01:00
x01da fd1626fbcd nidaq improvement on_stage 2026-03-02 13:27:51 +01:00
x01da 062df3171b add additional signals for xrd tigger information 2026-03-02 13:27:51 +01:00
x01da 37a268fe7b add additional CI channels for NIDAQ. Add enable PV for dead time correction 2026-03-02 13:27:51 +01:00
x01da e9e7d84e60 reworked function to get rid of (potentionally infinite) loop. 2026-03-02 13:27:51 +01:00
x01da 1c0c9ad53e add opencv dependency for hutch cameras 2026-03-02 13:27:51 +01:00
x01da faeb991b75 create hutch camera class 2026-03-02 13:27:51 +01:00
x01da 7377613213 add config signals 2026-03-02 13:27:51 +01:00
x01da d8383d3b73 add string representation of signals. add Pips class/device 2026-03-02 13:27:51 +01:00
x01da c3bfab2056 add string representation of signals 2026-03-02 13:27:51 +01:00
x01da 0261c601ff uncomment Pilatus-Sample distance 2026-03-02 13:27:51 +01:00
x01da d3dc130f11 uncomment ionization chamber and add pips diode 2026-03-02 13:27:51 +01:00
x01da ed1e5a027f add hutch cameras to config 2026-03-02 13:27:51 +01:00
x01da df2961ce8e add hutch cameras config 2026-03-02 13:27:51 +01:00
x01da e179fc1a07 add gas sensors to config 2026-03-02 13:27:51 +01:00
perl_d 60d1dfc5af Update repo with template version v1.2.8
CI for debye_bec / test (pull_request) Successful in 1m4s
CI for debye_bec / test (push) Successful in 1m2s
2026-02-27 15:49:26 +01:00
perl_d 3e2e37908b Update repo with template version v1.2.7
CI for debye_bec / test (push) Failing after 0s
CI for debye_bec / test (pull_request) Failing after 0s
2026-02-27 12:11:40 +01:00
appel_c 804a731181 test(pilatus): Fix on_complete callback for pilatus
CI for debye_bec / test (pull_request) Successful in 1m0s
CI for debye_bec / test (push) Successful in 1m4s
2025-12-05 14:18:46 +01:00
appel_c 99f6192f37 refactor: deprecate duplicate Status implementation.
CI for debye_bec / test (push) Failing after 1m2s
CI for debye_bec / test (pull_request) Failing after 1m9s
2025-11-30 22:29:23 +01:00
appel_c 0a8272685d fix(status): cleanup and remove of old status usage 2025-11-30 22:28:34 +01:00
appel_c c6ed27966c fix(status): fix compare and transition status occurences
CI for debye_bec / test (push) Failing after 1m9s
CI for debye_bec / test (pull_request) Failing after 3m33s
2025-11-26 13:46:53 +01:00
appel_c 6a8f6c7988 fix: remove enums from typehints
CI for debye_bec / test (pull_request) Failing after 2s
CI for debye_bec / test (push) Successful in 1m16s
2025-09-18 07:17:48 +02:00
appel_c 6bfc8999f7 refactor: fix set_exception for AndStatusWithList 2025-09-18 07:14:39 +02:00
appel_c c70088e7bc tests: fix test cases after refactoring
CI for debye_bec / test (pull_request) Successful in 1m7s
CI for debye_bec / test (push) Successful in 1m5s
2025-09-17 07:44:31 +02:00
appel_c 190eae2c3f fix: formatting 2025-09-17 07:44:31 +02:00
appel_c 1d6caa2291 refactor(pilatus-curtain): cleanup pilatus curtain 2025-09-17 07:44:31 +02:00
appel_c 9b739c852d fix(mo1-bragg-devices): revert signal name change, motor_is_moving 2025-09-17 07:44:31 +02:00
appel_c 9d9a2e9681 refactor(pilatus): Cleanupt PIlatus integration 2025-09-17 07:44:31 +02:00
x01da ed759da14f bugfix for XAS only scans 2025-09-17 07:44:31 +02:00
x01da 158175f545 add baseline signals (not workling yet) 2025-09-17 07:44:31 +02:00
x01da ed9148ed96 bugfix 2025-09-17 07:44:31 +02:00
x01da 0a83b59af8 refacotring 2025-09-17 07:44:31 +02:00
x01da a67394a9a2 refactoring 2025-09-17 07:44:31 +02:00
x01da a6f0d01558 updated/added configs 2025-09-17 07:44:31 +02:00
gac-x01da 217a14d03d configure mono trigger signal 2025-09-17 07:44:31 +02:00
gac-x01da 4424f83b8b working example of combined xas_xrd scan 2025-09-17 07:44:31 +02:00
gac-x01da 626b0dc8a0 added xrd energy signal 2025-09-17 07:44:31 +02:00
gac-x01da 1f7fdb89d7 add on_stage for xas_xrd scans 2025-09-17 07:44:31 +02:00
gac-x01da ee748d56c4 added timestamp signals for nidaq 2025-09-17 07:44:31 +02:00
gac-x01da 02e6462ea1 updated trigger signals of mono 2025-09-17 07:44:31 +02:00
appel_c 2633c8be0a refactor(pilatus): add live mode to pilatus 2025-09-17 07:44:31 +02:00
appel_c 09c3e395de refactor(pilatus): update config, add live mode 2025-09-17 07:44:31 +02:00
appel_c 8e5bdd230d test(pilatus): add tests for the pilatus. on_stage & on_connected tests fail due to AD baseclass callbacks 2025-09-17 07:44:31 +02:00
gac-x01da dd0fe31cb7 feat(pilatus): Initial commit of Pilatus integration 2025-09-17 07:44:31 +02:00
appel_c 442c421d05 fix: update repo to copier template v1-2-2
CI for debye_bec / test (pull_request) Successful in 59s
CI for debye_bec / test (push) Successful in 58s
2025-09-11 18:17:22 +02:00
appel_c 85042a7f45 feat: update repo with copier template for gitea migration 2025-09-11 15:43:15 +02:00
appel_c 6a1992f605 fix(test-mo1-bragg): fix test for mo1_bragg mov succeeds. 2025-07-31 15:55:08 +02:00
appel_c aaf4084517 fix: rename .service_config to .config 2025-07-31 15:54:54 +02:00
gac-x01da 34dbc1839d fix test 2025-06-25 16:05:20 +02:00
gac-x01da db6a9a502f bugfix for when nidaq is stopped early 2025-06-25 15:56:39 +02:00
gac-x01da e6586ceab2 Make sure that parameters are set before validating 2025-06-25 15:55:54 +02:00
appel_c ed6d64c7f9 fix(camera): fix num_rotation_90 for cameras 2025-06-23 16:31:34 +02:00
appel_c 43e8aea6c8 tests: fix test for MR, mo1_bragg and nidaq tests skipped, check issue 22 2025-06-20 09:30:53 +02:00
appel_c abf432f2a9 refactor: fix formatting 2025-06-20 09:19:51 +02:00
gac-x01da b3672cf5f5 refactor: add compare and transition status for nidaq and mo1_bragg 2025-06-20 09:16:27 +02:00
gac-x01da fa434794c3 feat(metadata-schema): add metadata schema 2025-06-20 09:16:03 +02:00
gac-x01da 9be74da098 feat(debye-nexus-structure): add first nexus template for debye 2025-06-20 09:15:39 +02:00
gac-x01da 29913cea61 refactor(nidaq-enums): add additional enums for NIDAQ 2025-06-20 09:14:40 +02:00
gac-x01da 881bc9e7a3 refactor(mo1-bragg-device): add Pvs for mono 2025-06-20 09:14:10 +02:00
appel_c e941647750 refactor(progress-signal): ProgressSignal for mo1_bragg and nidaq 2025-06-18 14:32:31 +02:00
appel_c 827557b667 refactor(debye-cam): add preview signal to camera integrations 2025-06-18 14:32:31 +02:00
gac-x01da a9fd62d249 feat: add initial file structure 2025-06-18 14:29:51 +02:00
appel_c 27ff5697af fix(device-configs): change to relative path for !include syntax 2025-06-18 14:23:19 +02:00
appel_c 07d05f9490 fix(nidaq): fix proper handling return of DeviceStatus for complete method 2025-06-18 14:23:19 +02:00
gac-x01da 39adeb72de test: add test for nidaq continous scan 2025-06-18 14:23:19 +02:00
gac-x01da bc666dc807 fix: double timeout for for wait for ScanControlMessage 2025-06-18 14:23:19 +02:00
gac-x01da 89cc27a8da feat: add nidaq_continuous_scan scan 2025-06-18 14:23:19 +02:00
gac-x01da 718a001a8a typo and adding pinhole motors 2025-06-18 14:23:19 +02:00
gac-x01da f038679d76 refactor(nidaq): add energy pv from nidaq 2025-06-18 14:23:19 +02:00
gac-x01da a1433efbf8 update of test_config to include focusing mirror 2025-06-18 14:23:19 +02:00
gac-x01da 79ead32e79 feat(nidaq): ensure nidaq is powered on during on_connected 2025-06-18 14:23:19 +02:00
gac-x01da c934aa8e9a refactor: update configs with optic slit config, machine config 2025-06-18 14:23:19 +02:00
perl_d 0d87e958d0 Update copier template source to github 2025-06-11 16:26:24 +02:00
appel_c e4556ad90e refactor: migrate debye_bec to copier template 2025-05-16 15:21:04 +02:00
ci_update_bot da89f9287c docs: Update device list 2025-05-16 13:19:11 +00:00
appel_c 665c290a90 test(test-camers): Add unit tests for camera integration of prosilica and basler cameras 2025-05-16 15:14:17 +02:00
gac-x01da 10b0608d31 fix: debug with devices, throttle live_view to 1Hz 2025-05-16 15:14:17 +02:00
appel_c ca2cf40d6a refactor: refactor basler and prosilica cameras 2025-05-16 15:14:17 +02:00
appel_c 415c601d2a feat(debye-base-cam): introduce base class for cameras at debye 2025-05-16 15:14:17 +02:00
ci_update_bot 0c1f41cd7c docs: Update device list 2025-05-16 13:08:16 +00:00
gac-x01da 0cdad97d00 refactor: add mo1_bragg_angle to x01da_test_config 2025-05-16 15:02:28 +02:00
appel_c b3f63f4f76 tests(mo1-bragg-angle): add tests for mo1_bragg_angle 2025-05-09 16:26:00 +02:00
appel_c 87ea95e975 fix: improve move method of mo1_bragg_devices 2025-05-09 16:25:41 +02:00
appel_c 4d9a062b8c tests: fix tests and formatting 2025-05-09 16:00:07 +02:00
gac-x01da c782324065 feat(mo1-bragg-angle): add Mo1BraggAngle positioner class; closes #14 2025-05-09 08:41:44 +02:00
gac-x01da 5bb0df2ddf refactor(mo1-bragg-positioner): remove move_type, cleanupt of angle pvs in mo1_bragg_positioner 2025-05-09 08:36:26 +02:00
ci_update_bot 20759e7ff1 docs: Update device list 2025-05-07 11:44:11 +00:00
appel_c b03b90a86a fix: fix range checks in Mo1Bragg and IonizationChamber 2025-05-07 13:38:43 +02:00
appel_c 74e0b01b02 fix: temporary comment, issue created #16 2025-05-07 12:52:50 +02:00
gac-x01da 7b7a24b6c8 refactor: formatting 2025-05-07 12:40:13 +02:00
gac-x01da 31ff28236b fix: update config, remove cameras for the moment 2025-05-07 12:38:41 +02:00
gac-x01da 002a3323a0 fix(ion-chambers): fix ion chamber code at beamline 2025-05-07 12:38:04 +02:00
gac-x01da 24d81bb180 build: update black dependency to ~=25.0 2025-05-07 12:37:33 +02:00
gac-x01da 32e24cd92a fix: fix occasional crash of mo1_bragg for scan; closes #11 2025-05-07 11:15:21 +02:00
appel_c 03e3b1c605 fix: fix imports in basler_cam 2025-05-06 16:26:27 +02:00
appel_c 6ab1a2941c fix: fix typo in device config mo1_bragg 2025-05-06 11:19:18 +02:00
appel_c b8a050c424 tests: fix DeviceMessages for tests 2025-05-05 15:14:11 +02:00
gac-x01da a8e7325f0f fix(mo1-bragg): fix error upon fresh start, not yet working. 2025-05-05 14:31:03 +02:00
gac-x01da ae50cbdfd1 refactor(device-config): extend device config 2025-05-05 14:31:03 +02:00
gac-x01da c164414631 fix(camera): add throttled update to cameras 2025-05-05 14:31:03 +02:00
gac-x01da c074240444 tpying fix 2025-05-05 14:30:36 +02:00
gac-x01da 759636cf2c fix type reffoil changer 2025-05-05 14:30:36 +02:00
gac-x01da 510073d2f2 fix(reffoil-changer): add scaninfo to __init__ signature 2025-05-05 14:30:36 +02:00
gac-x01da 17b671dd4b Introduction of reference foil changer 2025-05-05 14:30:36 +02:00
gac-x01da fe3e8b6291 fix: update devices in configs, to be checked 2025-05-05 14:30:36 +02:00
gac-x01da b7f72f8e44 wip support BEC core scans 2025-05-05 14:28:55 +02:00
gac-x01da 8cad42920b Changed readout priority from async to baseline for new devices 2025-05-05 14:28:39 +02:00
gac-x01da c43ca4aaa8 fix (mo1-bragg): fix code after test with hardware at the beamline 2025-05-05 14:28:26 +02:00
appel_c ce046f55f6 refactor (mo1-bragg): refactored Mo1 Bragg class with new base class PSIDeviceBase 2025-05-05 14:28:26 +02:00
gac-x01da 849b2d2bdb feat: add camera and power supply ophyd classes 2025-05-05 14:27:43 +02:00
appel_c d3ab7316ac fix: formatting 2025-03-18 19:28:18 +01:00
gac-x01da 01a17cbe3a feat: add support BEC core scans 2025-03-18 19:28:18 +01:00
gac-x01da edcf00a55c fix: adapt nidaq to PSIDeviceBase 2025-03-18 19:28:18 +01:00
gac-x01da 81bca16f67 refactor: moved nidaq to subfolder 2025-03-18 19:28:18 +01:00
appel_c cddc231d53 refactor (mo1-bragg): refactored Mo1 Bragg class with new base class PSIDeviceBase 2025-03-18 19:28:18 +01:00
gac-x01da 6999837d6b refactor: ES0Filter device with EpicsSignalWithRBVBit 2025-03-18 19:24:15 +01:00
gac-x01da 7c5bb1e963 fix: fix basler_camera with matching AD ophyd classes 2025-03-18 19:24:15 +01:00
gac-x01da 2a0b1d7453 feat: add camera and power supply ophyd classes 2025-03-18 19:24:15 +01:00
ci_update_bot d58553f9e7 docs: Update device list 2025-03-11 09:19:38 +00:00
appel_c fae7b93805 tests (mo1_bragg):
fix fests after update of trigger settings on IOC
2025-03-11 10:17:31 +01:00
gac-x01da 7f07f4a3dd Added devices and updated config 2025-03-11 10:17:31 +01:00
gac-x01da 87ab69b335 Adding classes for trigger enum signals 2025-03-11 10:17:31 +01:00
gac-x01da 3e36274f55 Added missing motors from experimental station 2025-03-11 10:17:31 +01:00
gac-x01da fe6040cd91 Added PVs for trigger signals 2025-03-11 10:17:31 +01:00
gac-x01da 6eabb4cb3a Bugfix: Change order of parameters 2025-03-11 10:17:31 +01:00
gac-x01da 79e5e158c1 Removed old numpy version request 2025-03-11 10:17:31 +01:00
gac-x01da dc3e0685d8 Updated naming scheme of trigger names 2025-03-11 10:17:31 +01:00
ci_update_bot 8e1d0b8536 docs: Update device list 2025-03-06 09:17:05 +00:00
wakonig_k 1a193a39ca tests(mono_bragg): fixed for pre_scan 2025-03-06 10:12:29 +01:00
wakonig_k 2ad35be182 build: removed fixed bec versions 2025-03-06 10:02:40 +01:00
gac-x01da e49fc6af41 Added config function for nidaq 2025-03-06 09:57:12 +01:00
gac-x01da 23dd8c11e0 Update of state names 2025-03-06 09:57:12 +01:00
gac-x01da 6d43a08bd5 feat: add nidaq draft for ophyd 2025-03-06 09:57:12 +01:00
gac-x01da 9b9db93677 fix: add dependencies to pyproject.toml 2025-03-06 09:57:12 +01:00
gac-x01da 985a4228de fix: minor bugfixes for XASSimpleScan, missing yield and "wait" for stubs.complete 2025-03-06 09:57:12 +01:00
appel_c a55f8bf705 test: fix ScanStatusMessage 2025-01-20 13:46:24 +01:00
wakonig_k d50519f3d3 refactor(scans): renamed primary to monitored 2024-11-12 21:24:55 +01:00
wakonig_k 7061aaf450 fix(scans): fixes for bec v3 2024-11-08 10:28:28 +01:00
wakonig_k 42ca7ed9a4 added license 2024-10-01 10:23:47 +02:00
ci_update_bot b85b8e6a2f docs: Update device list 2024-09-13 13:48:26 +00:00
gac-x01da ab70895fe6 refactor: cleanup of mo1_bragg 2024-09-13 15:17:37 +02:00
gac-x01da cc51aefb57 fix: cleanup test 2024-09-13 12:52:13 +02:00
gac-x01da 676c3ba97e fix: cleanup test, add scipy dependency 2024-09-13 11:49:47 +02:00
gac-x01da 0fbc700f1b Moved spline computation to separate file
Added tests for spline computation
Refactoring
2024-09-13 11:47:47 +02:00
gac-x01da 0062da5a6b Refactored advanced scan.
Added calculator function to convert energy/angle.
2024-09-13 11:44:28 +02:00
gac-x01da (Resp. Clark Adam Hugh) 31dd582648 Added tests for advanced scans 2024-09-13 11:44:28 +02:00
gac-x01da (Resp. Clark Adam Hugh) 38bee8c5c7 Changed advanced scan to be used with energy inputs (not angle) 2024-09-13 11:44:28 +02:00
gac-x01da (Resp. Clark Adam Hugh) 5b46464ef9 Added scan classes for both advanced scans 2024-09-13 11:44:27 +02:00
gac-x01da (Resp. Clark Adam Hugh) 6d9f48c8dd Added set_advanced_xas_settings function to mo1_bragg device
Added info to readme file
2024-09-13 11:44:27 +02:00
appel_c 58f6510d86 fix: add wait for kickoff call, ensures complete to not be called too early 2024-08-07 22:00:13 +02:00
108 changed files with 12130 additions and 2426 deletions
+9
View File
@@ -0,0 +1,9 @@
# Do not edit this file!
# It is needed to track the repo template version, and editing may break things.
# This file will be overwritten by copier on template updates.
_commit: v1.4.0
_src_path: https://github.com/bec-project/plugin_copier_template.git
make_commit: false
project_name: debye_bec
widget_plugins_input: []
+102
View File
@@ -0,0 +1,102 @@
name: CI for debye_bec
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: "Branch of BEC Widgets to install"
required: false
type: string
default: "main"
BEC_CORE_BRANCH:
description: "Branch of BEC Core to install"
required: false
type: string
default: "main"
OPHYD_DEVICES_BRANCH:
description: "Branch of Ophyd Devices to install"
required: false
type: string
default: "main"
BEC_PLUGIN_REPO_BRANCH:
description: "Branch of the BEC Plugin Repository to install"
required: false
type: string
default: "main"
PYTHON_VERSION:
description: "Python version to use"
required: false
type: string
default: "3.12"
permissions:
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
env:
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ inputs.PYTHON_VERSION || '3.12' }}"
- name: Checkout BEC Plugin Repository
uses: actions/checkout@v4
with:
repository: bec/debye_bec
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
path: ./debye_bec
- name: Lint for merge conflicts from template updates
shell: bash
# Find all Copier conflicts except this line
run: '! grep -r "<<<<<<< before updating" | grep -v "grep -r \"<<<<<<< before updating"'
- name: Checkout BEC Core
uses: actions/checkout@v4
with:
repository: bec/bec
ref: "${{ inputs.BEC_CORE_BRANCH || 'main' }}"
path: ./bec
- name: Checkout Ophyd Devices
uses: actions/checkout@v4
with:
repository: bec/ophyd_devices
ref: "${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}"
path: ./ophyd_devices
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec/bec_widgets
ref: "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}"
path: ./bec_widgets
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
- name: Install Python dependencies
shell: bash
run: |
pip install uv
uv pip install --system -e ./ophyd_devices
uv pip install --system -e ./bec/bec_lib[dev]
uv pip install --system -e ./bec/bec_ipython_client
uv pip install --system -e ./bec/bec_server[dev]
uv pip install --system -e ./bec_widgets[dev,pyside6]
uv pip install --system -e ./debye_bec
- name: Run Pytest with Coverage
id: coverage
run: pytest --random-order --cov=./debye_bec --cov-config=./debye_bec/pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./debye_bec/tests/ || test $? -eq 5
+70
View File
@@ -0,0 +1,70 @@
name: Create template upgrade PR for debye_bec
on:
workflow_dispatch:
permissions:
pull-requests: write
jobs:
create_update_branch_and_pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Checkout
uses: actions/checkout@v4
- name: Create virtualenv
run: |
python -m virtualenv .venv
- name: Install tools
run: |
source .venv/bin/activate
pip install copier PySide6 bec_lib
- name: Perform update
run: |
source .venv/bin/activate
git config --global user.email "bec_ci_staging@psi.ch"
git config --global user.name "BEC automated CI"
branch="chore/update-template-$(python -m uuid)"
echo "switching to branch $branch"
git checkout -b $branch
echo "Running copier update..."
copier update --trust --defaults --conflict inline 2>&1 | tee copier.log
status=${PIPESTATUS[0]}
output="$(cat copier.log)"
echo $output
msg="$(printf '%s\n' "$output" | head -n 1)"
if ! grep -q "make_commit: true" .copier-answers.yml ; then
echo "Autocommit not made, committing..."
git add -A
git commit -a -m "$msg"
fi
if diff-index --quiet HEAD ; then
echo "No changes detected"
exit 0
fi
git push -u origin $branch
curl -X POST "https://gitea.psi.ch/api/v1/repos/${{ gitea.repository }}/pulls" \
-H "Authorization: token ${{ secrets.CI_REPO_WRITE }}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Template: $(echo $msg)\",
\"body\": \"This PR was created by Gitea Actions\",
\"head\": \"$(echo $branch)\",
\"base\": \"main\"
}"
-7
View File
@@ -1,7 +0,0 @@
include:
- project: bec/awi_utils
file: /templates/plugin-repo-template.yml
inputs:
name: "debye"
target: "debye_bec"
branch: $CHILD_PIPELINE_BRANCH
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2025, Paul Scherrer Institute
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+63 -1
View File
@@ -1,3 +1,65 @@
# Debye BEC
Debye-specific plugins and configs for BEC
Debye-specific plugins and configs for BEC
## How to
### Visual studio code
To open
```
ssh x01da-bec-001
cd /data/test/x01da-test-bec/bec_deployment
code
```
To run tests directly in vs code terminal
```
. /data/test/x01da-test-bec/bec_deployment/bec_venv/bin/activate
cd /data/test/x01da-test-bec/bec_deployment/debye_bec
pytest -vv ./tests
```
### Git
```
git pull
git push origin feat/add_advanced_scan_modes
git status
```
If git claims to not know the author identity
```
git config --global user.email "you@example.com"
git config --global user.name "gac-x01da"
```
### BEC Server
```
ssh x01da-bec-001
cd /data/test/x01da-test-bec/bec_deployment
. /data/test/x01da-test-bec/bec_deployment/bec_venv/bin/activate
bec-server start
bec-server restart
bec-server stop
bec-server attach
```
To restart individual server modules:
- ctrl-c + ctrl-c to stop for example scan server or device server module
- restart server module(s)
### BEC Client
```
ssh x01da-bec-001
cd /data/test/x01da-test-bec/bec_deployment
bec
```
#### Useful commands in bec
Update Session with specific config:
```
bec.config.update_session_with_file("debye_bec/debye_bec/device_configs/x01da_test_config.yaml")
```
Define folder and sample name for written files:
```
bec.system_config.file_directory="test"
bec.system_config.file_suffix ="sampleA"
```
+1
View File
@@ -0,0 +1 @@
# Add anything you don't want to check in to git, e.g. very large files
@@ -0,0 +1,82 @@
from __future__ import annotations
import builtins
from typing import TYPE_CHECKING
from bec_lib import bec_logger
from debye_bec.devices.absorber import STATUS as ABS_STATUS
logger = bec_logger.logger
# import builtins to avoid linter errors
dev = builtins.__dict__.get("dev")
class MoveToLabelError(Exception):
"""Exception for the MoveToLabel function"""
def move_to_label():
"""
Function to move several motors to a specific position defined in the label dict.
"""
label = get_device_conditions(label="digitalTwin")
# Get absorber status and close if open
logger.info("Check Frontend Absorber Status")
abs_was_open = dev.abs.status.get() == ABS_STATUS.OPEN
if abs_was_open:
logger.info(" Close Frontend Absorber")
status = dev.abs.close()
status.wait()
# Move Frontend Slits
logger.info("Move Frontend Slits into position")
devices = ["sldi_centerx", "sldi_centery", "sldi_gapx", "sldi_gapy"]
matches = {key: label[key] for key in devices if key in label}
statuses = []
for device in matches.values():
statuses.append(device['device'].move(device['value']))
for status in statuses:
status.wait(timeout=30)
# Move Collimating mirror
logger.info("Move Collimating Mirror into position")
if "cm_rotx" in label: # pitch
logger.info(" Move pitch into position")
surveyed_movement(
axis=label['cm_rotx'],
surveyed_axes= [
{'device': dev.cm_rotz, 'abs_tol': 0.1},
]
)
# Restore absorber position
logger.info("Restore Frontend Absorber Status")
if abs_was_open:
status = dev.abs.open()
status.wait()
def surveyed_movement(axis, surveyed_axes):
"""
Moves an axis while surverying a set of axes.
Args:
axis (DeviceCondition): Device condition
surveyed_axes (list): List of dicts (same format as DeviceCondition)
Raises:
If during movement of axis, one of the surveyed axes moves out of tolerance.
"""
for surv_ax in surveyed_axes:
surv_ax['old_value'] = surv_ax['device'].read()
status = axis['device'].move(axis['value'])
while status.status == 'RUNNING':
for surv_ax in surveyed_axes:
if abs(surv_ax['device'].read() - surv_ax['old_value']) > surv_ax['abs_tol']:
axis['device'].stop()
raise MoveToLabelError(
f"During movement of {axis['device'].name}, {surv_ax['device'].name} " +
f"started to move unexpectedly (old pos: {surv_ax['old_value']}, " +
f"current pos: {surv_ax['device'].read()})"
)
@@ -10,7 +10,7 @@ While command-line arguments have to be set in the pre-startup script, the
post-startup script can be used to load beamline specific information and
to setup the prompts.
from bec_lib import bec_logger
from bec_lib.logger import bec_logger
logger = bec_logger.logger
@@ -1,10 +1,14 @@
"""
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.
is started. It can be used to add additional command line arguments.
"""
import os
from bec_lib.service_config import ServiceConfig
import debye_bec
def extend_command_line_args(parser):
"""
@@ -18,6 +22,11 @@ def extend_command_line_args(parser):
def get_config() -> ServiceConfig:
"""
Create and return the service configuration.
Create and return the ServiceConfig for the plugin repository
"""
return ServiceConfig(redis={"host": "x01da-bec-001", "port": 6379})
deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(debye_bec.__file__)))
files = os.listdir(deployment_path)
if "bec_config.yaml" in files:
return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
else:
return ServiceConfig(redis={"host": "localhost", "port": 6379})
+41
View File
@@ -0,0 +1,41 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
logger = bec_logger.logger
# pylint: skip-file
_Widgets = {
"DigitalTwin": "DigitalTwin",
}
class DigitalTwin(RPCBase):
"""Main widget of Digital Twin"""
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.digital_twin.digital_twin"
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
@rpc_call
def attach(self):
"""
None
"""
@rpc_call
def detach(self):
"""
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
"""
@@ -0,0 +1,13 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
# pylint: skip-file
designer_plugins = {
"DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
}
widget_icons = {
"DigitalTwin": "lightbulb",
}
@@ -0,0 +1,259 @@
"""
Calculates the positions of axes based on a beamline config
"""
import numpy as np
from bec_lib import bec_logger
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
logger = bec_logger.logger
def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
"""
Calculates the positions of axes based on a beamline config.
Args:
cfg(ConfigDict): Dictionary with beamline config
Returns:
dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries
containing a "value" key with the corresponding float value (position).
"""
pos = {}
## FE slits
trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]
trxw = (
(np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1])
/ bl.feSlits.center1[1]
* bl.feSlits.center2[1]
)
tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]
tryt = (
(np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1])
/ bl.feSlits.center1[1]
* bl.feSlits.center2[1]
)
xgap = trxw - trxr
ygap = tryt - tryb
pos["sldi_gapx"] = {"value": xgap}
pos["sldi_gapy"] = {"value": ygap}
## Collimating Mirror
obj_dist = bl.cm.center[1] # object distance
beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM
# TRX
if cfg["cm_stripe"] in bl.cm.surface:
index = bl.cm.surface.index(cfg["cm_stripe"])
else:
raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!")
cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2
pos["cm_trx"] = {"value": cm_trx}
# TRY
height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"])
pos["cm_try"] = {"value": height}
# Pitch
pos["cm_rotx"] = {
"value": -cfg["cm_pitch"] * 1e3
} # invert and convert to mrad (same as EGU of rotx axis)
# Bending Radius
radius = (
2.0 * obj_dist / np.sin(cfg["cm_pitch"])
) # Elements of modern X-ray Physics, page 108 ff.
pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
## Monochromator
if cfg["mo1_mode"] == "Monochromatic":
# Add 2x CM pitch to the bragg angle
bragg = cfg["mo1_bragg"]
elif cfg["mo1_mode"] == "Pinkbeam":
# Align xtal surfaces parallel to beam
bragg = 0
else:
raise ValueError("Monochromator mode not supported")
pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg
# TRY, Height
l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"])
yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"]))
yver = yhor * np.tan(2.0 * cfg["cm_pitch"])
if cfg["mo1_mode"] == "Monochromatic":
beam_offset_mo1 = (
l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver
) # Resultat ist korrekt!
elif cfg["mo1_mode"] == "Pinkbeam":
beam_offset_mo1 = 0
else:
raise ValueError("Monochromator mode not supported")
def csc(a):
return 1 / np.sin(a)
def cot(a):
return 1 / np.tan(a)
# calculate height of center of first crystal surface
f = bl.mo1.rotOffset # rotation offset, mm
d = bl.mo1.heightOffset # xtal height offset, mm
c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"])
# Calculate height of center of rotation
b = np.sqrt(
d**2 * csc(cfg["mo1_bragg"]) ** 2
- 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"])
+ f**2 * cot(cfg["mo1_bragg"]) ** 2
+ f**2
)
h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b
h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan(2 * cfg["cm_pitch"])
height_mo1_real = (
h + h2
) # per design, the height should not change if the pitch of the CM is not changed!
if cfg["mo1_mode"] == "Monochromatic":
pass
elif cfg["mo1_mode"] == "Pinkbeam":
height_mo1_real = (
height_mo1_real - 13
) # Move down to let beam pass between both crystal without touching copper cooler
else:
raise ValueError("Monochromator mode not supported")
pos["mo1_try"] = {"value": height_mo1_real}
# TRX, Crystal selection
if cfg["mo1_mode"] == "Monochromatic":
xtal = cfg["mo1_xtal"].translate(
str.maketrans("", "", "()")
) # Remove brackets from xtal name to conform with parameters
if xtal in bl.mo1.xtal:
index = bl.mo1.xtal.index(xtal)
else:
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]}
else:
pos["mo1_trx"] = {"value": 0}
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
## Slits 1
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
sl1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
pos["sl1_centery"] = {"value": sl1_beam_height}
pos["sl1_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 1
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
pos["bm1_try"] = {"value": bm1_beam_height}
## Focusing Mirror
p = bl.fm.center[1]
q = cfg["smpl"] - bl.fm.center[1]
f = (p * q) / (p + q) # focal length
# Bender radius
if cfg["fm_qy"] is None:
radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam
else:
radius = (
2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"])
) # ideal bending radius for unfocused beam
pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
# Pitch
d = bl.fm.center[1] - bl.cm.center[1] - dz
fm_rotx = (
2 * cfg["cm_pitch"] - cfg["fm_rotx"]
) # calculate pitch in absolute values (according to horizontal plane)
pos["fm_rotx"] = {
"value": -fm_rotx * 1e3
} # invert and convert to mrad (same as EGU of rotx axis)
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
# TRY
if cfg["fm_stripe"] in "Rh (toroid)":
r = bl.fm.r[0]
h_cyl = bl.fm.hToroid[0]
else: # PT toroid
r = bl.fm.r[1]
h_cyl = bl.fm.hToroid[1]
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3)
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
h = r - (r * np.cos(alpha / 2))
fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[
"fm_gain_height"
]
pos["fm_try"] = {"value": fm_height}
# TRX
if cfg["fm_stripe"] in "Rh (toroid)":
x_cyl = -bl.fm.xToroid[0]
else:
x_cyl = -bl.fm.xToroid[1]
pos["fm_trx"] = {"value": x_cyl}
elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"):
# TRY
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
fm_beam_height = fm_height
pos["fm_try"] = {"value": fm_height}
# TRX
if cfg["fm_stripe"] in "Rh (flat)":
x_flat = -bl.fm.xFlat[0]
else:
x_flat = -bl.fm.xFlat[1]
pos["fm_trx"] = {"value": x_flat}
else:
raise ValueError("FM Stripe selection not valid")
pos["fm_roty"] = {"value": 0}
pos["fm_rotz"] = {"value": 0}
## Slits 2
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["sl2_centery"] = {"value": sl2_beam_height}
pos["sl2_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
## Beam Monitor 2
d = bl.opBM2.center[1] - bl.fm.center[1]
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["bm2_try"] = {"value": bm2_beam_height}
## Optical Table
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["ot_try"] = {"value": ot_height}
# Pitch
ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
# TRZ ES1
ot_es1_trz = cfg["smpl"]
pos["ot_es1_trz"] = {"value": ot_es1_trz}
# ES0 exit window
pos["es0wi_try"] = {
"value": 5
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
return pos
@@ -0,0 +1,70 @@
"""
Calculates the sideview coordinates based on a beamline config.
"""
import numpy as np
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, DataDict
def calc_sideview(cfg: ConfigDict) -> DataDict:
"""
Calculates the sideview coordinates based on a beamline config.
Args:
cfg(ConfigDict): Dictionary with beamline config
Returns:
DataDict: Sideview data
"""
beam: DataDict = {"x": [], "y": []}
beam["x"] = []
beam["y"] = []
beam["x"].append(0) # Source
beam["y"].append(bl.sourceHeight)
beam["x"].append(bl.cm.center[1]) # CM
beam["y"].append(bl.sourceHeight)
if cfg["mo1_mode"] in "Monochromatic":
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1
beam["y"].append(
bl.sourceHeight
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
)
beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2
beam["y"].append(
bl.sourceHeight
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
+ dy
)
beam["x"].append(bl.fm.center[1]) # FM
beam["y"].append(
bl.sourceHeight
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
+ dy
)
beam["x"].append(cfg["smpl"]) # Experiment
beam["y"].append(
bl.sourceHeight
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
+ dy
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
)
elif cfg["mo1_mode"] == "Pinkbeam":
beam["x"].append(bl.fm.center[1]) # FM
beam["y"].append(
bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
)
beam["x"].append(cfg["smpl"]) # Experiment
beam["y"].append(
bl.sourceHeight
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
)
return beam
@@ -0,0 +1,157 @@
"""
Calculates the surface coordinates based on a beamline config.
"""
import re
import numpy as np
from bec_lib import bec_logger
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict, SurfaceDict
logger = bec_logger.logger
def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
"""
Calculates the surface coordinates based on a beamline config.
Args:
cfg(ConfigDict): Dictionary with beamline config
Returns:
SurfaceDict: Surface data
"""
out: SurfaceDict = {
"cm": {"x": [], "y": []},
"mo1_1": {"x": [], "y": []},
"mo1_2": {"x": [], "y": []},
"fm": {"x": [], "y": []},
}
# Collimating mirror
l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"])
w1 = 2 * (bl.cm.center[1] - l / 2) * np.tan(cfg["h_acc"])
w2 = 2 * (bl.cm.center[1] + l / 2) * np.tan(cfg["h_acc"])
index = bl.cm.surface.index(cfg["cm_stripe"])
cen = -cfg["cm_trx"]
out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2]
out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2]
# Monochromator
# calculate height of center of first crystal surface
c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan(
cfg["mo1_bragg"]
)
e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c
xtal = cfg["mo1_xtal"].translate(
str.maketrans("", "", "()")
) # Remove brackets from xtal name to conform with parameters
index = bl.mo1.xtal.index(xtal)
xtal_pos = bl.mo1.xtalOffsetX[index]
xtal_length_1 = bl.mo1.xtalLength1[index]
xtal_length_2 = bl.mo1.xtalLength2[index]
width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"])
height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"])
w = height_beam / np.sin(cfg["mo1_bragg"])
if cfg["mo1_mode"] in "Monochromatic":
out["mo1_1"]["x"] = [
xtal_pos - width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos - width_beam / 2,
]
out["mo1_1"]["y"] = [
xtal_length_1 / 2 - c - w / 2,
xtal_length_1 / 2 - c - w / 2,
xtal_length_1 / 2 - c + w / 2,
xtal_length_1 / 2 - c + w / 2,
]
out["mo1_2"]["x"] = [
xtal_pos - width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos + width_beam / 2,
xtal_pos - width_beam / 2,
]
out["mo1_2"]["y"] = [
-xtal_length_2 / 2 + e - w / 2,
-xtal_length_2 / 2 + e - w / 2,
-xtal_length_2 / 2 + e + w / 2,
-xtal_length_2 / 2 + e + w / 2,
]
else: # Pinkbeam
out["mo1_1"]["x"] = []
out["mo1_1"]["y"] = []
out["mo1_2"]["x"] = []
out["mo1_2"]["y"] = []
# Focusing mirror
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
surface = bl.fm.surfaceToroid
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
index = surface.index(stripe)
r = bl.fm.r[index]
else:
surface = bl.fm.surfaceFlat
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
index = surface.index(stripe)
r = bl.fm.r[index]
off = -cfg["fm_trx"]
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"])
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
l = height_beam / np.sin(cfg["fm_rotx"])
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
h = r - (r * np.cos(alpha / 2))
z = h / np.tan(cfg["fm_rotx"])
x = [off - width_beam / 2, off - width_beam / 2]
y = [l / 2 - z / 2, -l / 2 - z / 2]
res = 20
x_elipse = np.linspace(0, np.pi, res)
y_elipse = np.linspace(0, np.pi, res)
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse]
x.extend(x_elipse)
y.extend(y_elipse)
x.extend([off + width_beam / 2, off + width_beam / 2])
y.extend([-l / 2 - z / 2, l / 2 - z / 2])
res = 50
x_elipse = np.linspace(np.pi, 0, res)
y_elipse = np.linspace(np.pi, 0, res)
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse]
x.extend(x_elipse)
y.extend(y_elipse)
out["fm"]["x"] = x
out["fm"]["y"] = y
else: # flat surface, no toroid
l = height_beam / np.sin(cfg["fm_rotx"])
w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"])
w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"])
out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2]
out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2]
return out
@@ -0,0 +1,418 @@
"""
Various calculations for the digital twin
"""
import re
from typing import Literal, cast
import numpy as np
from bec_lib import bec_logger
from scipy.interpolate import UnivariateSpline
from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
logger = bec_logger.logger
H = 6.62606957e-34
E = 1.602176634e-19
C = 299792458
RE = 2.8179e-15
def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]:
"""
Calculate the slits acceptance based on the gap values
Args:
sldi_gapx(float): GAPX value of the slits in mm
sldi_gapy(float): GAPY value of the slits in mm
Returns:
tuple[float, float]: Horizontal and vertical acceptance in rad
"""
d1 = bl.feSlits.center1[1]
d2 = bl.feSlits.center2[1]
h_acc = np.tan(sldi_gapx / (d2 + d1))
v_acc = np.tan(sldi_gapy / (d2 + d1))
return h_acc, v_acc
def cm_trx_to_stripe(cm_trx: float) -> str | None:
"""
Based on the trx value of the collimating mirror, return
the correct stripe
Args:
cm_trx(float): Collimating mirror trx value
Returns
str | None: Stripe of the mirror, None if not found
"""
cm_stripe = None
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
if low <= cm_trx <= high:
cm_stripe = name
return cm_stripe
def cm_stripe_to_trx(cm_stripe: str) -> float | None:
"""
Based on the stripe of the collimating mirror, return
the trx value
Args:
cm_stripe(str): Stripe of the collimating mirror
Returns:
float | None: TRX value of the stripe. None if not found
"""
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
if cm_stripe == name:
return -(low + high) / 2
return None
def fm_trx_to_stripe(fm_trx: float) -> str | None:
"""
Based on the trx value of the focusing mirror, return
the correct stripe
Args:
fm_trx(float): focusing mirror trx value
Returns
str | None: Stripe of the mirror, None if not found
"""
fm_stripe = None
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if low <= fm_trx <= high:
fm_stripe = name + " (flat)"
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
if low <= fm_trx <= high:
fm_stripe = name + " (toroid)"
return fm_stripe
def fm_stripe_to_trx(fm_stripe: str) -> float | None:
"""
Based on the stripe of the focusing mirror, return
the trx value
Args:
fm_stripe(str): Stripe of the focusing mirror
Returns:
float | None: TRX value of the stripe. None if not found
"""
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if fm_stripe == name + " (flat)":
return (low + high) / 2
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
if fm_stripe == name + " (toroid)":
return -(low + high) / 2
return None
def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float:
"""
Calculate the energy resolution of the monochromator
Args:
xtal(str): Xtal name. "Si111" or "Si311"
energy(float): Energy in eV
Returns:
float: Energy resolution in eV
"""
index = bl.mo1.xtal.index(xtal)
crystal = bl.mo1.material1[index]
dtheta = np.linspace(-30, 90, 601)
theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6
refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal
refl2 = refl**2 # DCM with parallel crystals
# FWHM of the DCM curve
spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0)
roots = cast(np.ndarray, spline.roots())
r1, r2 = float(roots[0]), float(roots[1])
fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad
# Energy resolution
theta_b = crystal.get_Bragg_angle(energy)
de_over_e = fwhm_rad / np.tan(theta_b)
de = de_over_e * energy
# logger.info(f"DCM FWHM : {r2-r1:.2f} µrad")
# logger.info(f"ΔE/E : {dE_over_E:.2e}")
# logger.info(f"ΔE : {dE:.3f} eV at {E} eV")
return de
def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float:
"""
Calculate the reflectivity of the mirror stripe based
on the pitch and energy.
Args:
cm_stripe(str): Mirror stripe
cm_pitch(float): Pitch of the mirror (beam incidence angle)
energy(float): Energy of the beam in eV
Returns:
float: Reflectivity [0-1]
"""
index = bl.cm.surface.index(cm_stripe)
rs, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
refl = abs(rs) ** 2
return refl
def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float:
"""
Calculate the reflectivity of the mirror stripe based
on the pitch and energy.
Args:
cm_stripe(str): Mirror stripe
cm_pitch(float): Pitch of the mirror (beam incidence angle)
energy(float): Energy of the beam in eV
Returns:
float: Reflectivity [0-1]
"""
if fm_stripe in ("Rh (toroid)", "Pt (toroid)"):
surface = bl.fm.surfaceToroid
material = bl.fm.materialToroid
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
index = surface.index(stripe)
else:
surface = bl.fm.surfaceFlat
material = bl.fm.materialFlat
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
index = surface.index(stripe)
rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2]
refl = abs(rs) ** 2
return refl
def mo1_bragg_angle(
mo_mode: Literal["Monochromatic", "Pinkbeam"], d_spacing: float, energy: float, cm_pitch: float
) -> tuple[float, float]:
"""
Calculate the bragg angle of the monochromator.
Corrects for the collimating mirror pitch.
Args:
mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam"
d_spacing(float): D-spacing of the crystal in Angstrom
energy(float): Energy of the beam in eV
cm_pitch(float): Pitch of collimating mirror in rad
Returns:
tuple[float, float]: Bragg angle and corrected bragg angle
"""
wl = C * H / (E * energy)
val = wl / (2 * d_spacing * 1e-10)
bragg_angle = 0
if val > -1 and val < 1:
bragg_angle = np.asin(val)
if mo_mode == "Monochromatic":
# Add 2x CM pitch to the bragg angle
bragg_angle_cor = (2 * cm_pitch) + bragg_angle
else:
# Align xtal surfaces parallel to beam
bragg_angle_cor = 2 * cm_pitch
return bragg_angle, bragg_angle_cor
def fm_ideal_pitch(
fm_focus: Literal["Defocused", "Focused", "Manual"],
fm_stripe: str,
smpl: float,
sldi_hacc: float | None = None,
sldi_vacc: float | None = None,
fm_focx: float | None = None,
fm_focy: float | None = None,
) -> tuple[float, float | None]:
"""
Calculates the ideal pitch for the focusing mirror depending on the
focusing strategy.
If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy
must be provided.
Args:
fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual
fm_stripe(str): Mirror stripe
smpl(float): Sample position in mm from source
sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None
sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None
fm_focx(float): Requested horizontal spot size in mm. Defaults to None
fm_focy(float): Requested vertical spot size in mm. Defaults to None
Returns:
tuple[float, float | None]: Pitch of mirror in rad, qy in mm
"""
p = bl.fm.center[1] # posFM
q = smpl - bl.fm.center[1] # dist posFM to posEX
if fm_focus in "Defocused":
assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode"
assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode"
assert fm_focx is not None, "fm_focx must be provided for Defocused mode"
assert fm_focy is not None, "fm_focy must be provided for Defocused mode"
a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror
b = (
2 * np.tan(sldi_vacc) * bl.cm.center[1]
) # Beam height at focusing mirror (collimated beam)
x = fm_focx
y = fm_focy
qx = q + x * p / a
qy = q + y * p / b
f = (p * qx) / (p + qx) # focal length
else: # Calculate for focused beam on sample in "manual" and "focused" mode
qy = None
f = (p * q) / (p + q) # focal length
pitch = 0
if "Rh" in fm_stripe:
pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM
if "Pt" in fm_stripe:
pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM
return pitch, qy
def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float:
"""
Calculate the critical angle of the mirror stripe
Args:
cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh"
energy(float): Energy in eV
Returns:
float: Critical angle in rad
"""
if cm_stripe in "Si":
stripe = bl.stripeSi
elif cm_stripe in "Pt":
stripe = bl.stripePt
else:
stripe = bl.stripeRh
w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m]
f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy))
number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3)
critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi)
return critical_angle
def mirror_surface_geometries(
mirror: Literal["cm", "fm_toroid", "fm_flat"],
) -> dict[str, tuple[float, float, float, float]]:
"""
Return the mirror stripe geometries
Args:
mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat"
Returns:
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
names to tuples of (x, y, width, height).
"""
if mirror in "cm":
surface = bl.cm.surface
lim_opt_x = bl.cm.limOptX
lim_opt_y = bl.cm.limOptY
elif mirror in "fm_toroid":
surface = bl.fm.surfaceToroid
lim_opt_x = bl.fm.limOptXToroid
lim_opt_y = bl.fm.limOptYToroid
elif mirror in "fm_flat":
surface = bl.fm.surfaceFlat
lim_opt_x = bl.fm.limOptXFlat
lim_opt_y = bl.fm.limOptYFlat
else:
raise ValueError(f"Requested mirror {mirror} not available!")
geom = {}
for sf, lx, hx, ly, hy in zip(surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1]):
geom[sf] = (lx, ly, hx - lx, hy - ly)
return geom
def mo_surface_geometries(
mo: Literal["mo1"], plane: Literal[0, 1]
) -> dict[str, tuple[float, float, float, float]]:
"""
Return the monochromator xtal geometries
Args:
mo(str): Monochromator. Only "mo1" implemented
plane(int): Surface of xtal. 0 and 1 (First and second)
Returns:
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
names to tuples of (x, y, width, height).
"""
if mo in "mo1":
xtal = bl.mo1.xtal
xtal_width = bl.mo1.xtalWidth
xtal_offset_x = bl.mo1.xtalOffsetX
if plane == 0:
xtal_length = bl.mo1.xtalLength1
else:
xtal_length = bl.mo1.xtalLength2
else:
return {}
geom = {}
for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length):
geom[sf] = (offx - w / 2, -length / 2, w, length)
return geom
def wall_geometries() -> list[list[float]]:
"""
Return the wall geometries
Returns:
list[list[float]]: List of [x, y, width, height] geometry values for each wall.
"""
geom = []
for i, _ in enumerate(bl.walls.start):
geom.append(
[
bl.walls.start[i],
bl.walls.height[i][0],
bl.walls.end[i] - bl.walls.start[i],
bl.walls.height[i][1] - bl.walls.height[i][0],
]
)
return geom
def pipe_geometries() -> list[dict[str, np.ndarray]]:
"""
Return the wall geometries
Returns:
list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y",
each containing a numpy array of two float values representing
the start and end coordinates of the pipe top and bottom edges.
"""
pipes = []
for i, _ in enumerate(bl.vacuum_pipes.center):
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
pipes.append(
{
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
"y": np.array([top, top]),
}
)
pipes.append(
{
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
"y": np.array([bottom, bottom]),
}
)
return pipes
@@ -0,0 +1,813 @@
"""
Digital Twin: Custom BEC widget to support the beamline alignment.
"""
import sys
from pathlib import Path
from typing import Literal, cast
import numpy as np
import yaml
from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme, get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
# pylint: disable=E0611
from qtpy.QtCore import Qt, QTimer
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QStyle,
QVBoxLayout,
QWidget,
)
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_positions import calc_positions
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_sideview import calc_sideview
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_surfaces import calc_surfaces
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import (
cm_critical_angle,
cm_reflectivity,
cm_stripe_to_trx,
cm_trx_to_stripe,
fm_ideal_pitch,
fm_reflectivity,
fm_stripe_to_trx,
fm_trx_to_stripe,
mo1_bragg_angle,
mo1_energy_resolution,
sldi_gap_to_acc,
)
from debye_bec.bec_widgets.widgets.digital_twin.panels.input_panel import InputPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.mover_panel import MoverPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.plots import SideviewPlot, SurfacePlots
from debye_bec.bec_widgets.widgets.digital_twin.panels.settings_panel import SettingsPanel
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
logger = bec_logger.logger
OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml"
class DigitalTwin(BECWidget, QWidget):
"""
Main widget of Digital Twin
"""
PLUGIN = True
ICON_NAME = "lightbulb"
def __init__(self, *arg, parent=None, **kwargs):
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
self.get_bec_shortcuts()
# Check if devices are all in config
self.check_config()
self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update())
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.input_widget = QWidget()
self.input_layout = QVBoxLayout(self.input_widget)
self.input = InputPanel()
self.settings = SettingsPanel()
self.input_layout.addWidget(self.input)
self.input_layout.addWidget(self.settings)
self.plot_widget = QWidget()
self.plot_layout = QVBoxLayout(self.plot_widget)
self.sideview_plot = SideviewPlot()
self.surface_plots = SurfacePlots()
self.plot_layout.addWidget(self.sideview_plot)
self.plot_layout.addWidget(self.surface_plots)
self.mover = MoverPanel(self.dev)
self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignmentFlag.AlignTop)
self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignmentFlag.AlignTop)
self.root_layout.addWidget(self.mover, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(self.root_layout)
self.setWindowTitle("Digital Twin")
self.resize(1800, 800)
self.input.energy.value_changed_connect(self.calc_assistant)
self.input.sldi_hacc.value_changed_connect(self.calc_assistant)
self.input.sldi_vacc.value_changed_connect(self.calc_assistant)
self.input.cm_stripe.activated_connect(self.calc_assistant)
self.input.cm_pitch.value_changed_connect(self.calc_assistant)
self.input.mo1_mode.activated_connect(self.calc_assistant)
self.input.mo1_xtal.activated_connect(self.calc_assistant)
self.input.fm_stripe.activated_connect(self.calc_assistant)
self.input.fm_focus.activated_connect(self.calc_assistant)
self.input.fm_rotx.value_changed_connect(self.calc_assistant)
self.input.fm_focx.value_changed_connect(self.calc_assistant)
self.input.fm_focy.value_changed_connect(self.calc_assistant)
self.input.smpl.value_changed_connect(self.calc_assistant)
self.input.adapt_reality.clicked_connect(self.adapt_reality)
self.settings.load_offsets.clicked_connect(self.load_offsets)
self.settings.show_offsets.clicked_connect(self.show_offsets)
self.bragg_angle = 0.0
self.qy = 0.0
self.offsets = {}
# Initialize all values
self.load_offsets(recalculate=False)
self.calc_assistant(identifier="init")
# Timer: update plots every 1 second
self._timer = QTimer(self)
self._timer.setInterval(100)
self._timer.timeout.connect(self.calc_reality)
self._timer.start()
def apply_theme(self, theme: Literal["dark", "light"]):
"""
Apply the theme
Args:
theme (str): Theme, either "dark" or "light"
"""
self.sideview_plot.apply_theme(theme)
self.surface_plots.apply_theme(theme)
self.mover.apply_theme(theme)
@SafeSlot()
def check_config(self, *args):
"""
Checks the BEC config and opens a window if not all necessary
devices are loaded in the config. If called from a slot from
BEC dispatcher whenever there is a config update, stop the timer
that updates the plot in the background.
"""
reload = (args[0] if args else {}).get("action") == "reload"
if reload:
self._timer.stop()
devices = [
"abs",
"sldi_gapx",
"sldi_gapy",
"cm_trx",
"cm_try",
"cm_bnd_radius",
"cm_rotx",
"mo1_bragg",
"mo1_trx",
"mo1_try",
"sl1_centery",
"sl1_gapy",
"bm1_try",
"fm_trx",
"fm_try",
"fm_bnd_radius",
"fm_rotx",
"fm_roty",
"fm_rotz",
"sl2_centery",
"sl2_gapy",
"bm2_try",
"ot_try",
"ot_rotx",
"es0wi_try",
"ot_es1_trz",
]
while True:
missing = [d for d in devices if d not in self.dev]
if not missing:
break
dialog = QDialog()
dialog.setWindowTitle("Digital Twin - Config Check")
dialog.setFixedWidth(400)
layout = QVBoxLayout()
top = QHBoxLayout()
icon = QLabel()
icon_pixmap = (
QApplication.style()
.standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
.pixmap(48, 48)
)
icon.setPixmap(icon_pixmap)
icon.setAlignment(Qt.AlignmentFlag.AlignTop)
top.addWidget(icon)
text = QLabel(
"The current config does not include all required devices to run Digital Twin."
+ "Reload the config with the correct devices."
)
text.setWordWrap(True)
text.setAlignment(Qt.AlignmentFlag.AlignTop)
top.addWidget(text, stretch=1)
layout.addLayout(top)
info = QLabel("Missing devices:\n" + ", ".join(missing))
info.setWordWrap(True)
info.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.addWidget(info)
layout.addStretch()
buttons = QHBoxLayout()
check_again = QPushButton("Check Again")
close_app = QPushButton("Close Application")
check_again.clicked.connect(dialog.accept)
close_app.clicked.connect(dialog.reject)
buttons.addWidget(check_again)
buttons.addWidget(close_app)
layout.addLayout(buttons)
dialog.setLayout(layout)
dialog.show()
info.setMinimumHeight(info.heightForWidth(info.width()))
if dialog.exec_() == QDialog.DialogCode.Rejected:
running_app = QApplication.instance()
if running_app is not None:
running_app.exit(0)
if reload:
self._timer.start()
@SafeSlot()
def calc_assistant(self, *_, **kwargs):
"""
Calculates various values for the assistant.
If called from a qt slot, the identifier represents
the button pressed / value changed. Based on the identifier,
calculate different values.
Note: identifier=init calculates all values
"""
identifier = kwargs["identifier"]
match identifier:
case "init":
self.update_mo1_mode()
self.calc_mo1_bragg_angle()
self.calc_cm_crit_pitch()
self.calc_cm_reflectivity()
self.update_fm_mode()
self.calc_fm_reflectivity()
self.calc_cm_fm_harm_suppr()
self.calc_fm_ideal_pitch()
self.calc_mo1_energy_resolution()
case "energy":
self.calc_mo1_bragg_angle()
self.calc_cm_crit_pitch()
self.calc_cm_reflectivity()
self.calc_fm_reflectivity()
self.calc_cm_fm_harm_suppr()
self.calc_mo1_energy_resolution()
case "cm_stripe":
self.calc_cm_crit_pitch()
self.calc_cm_reflectivity()
self.calc_cm_fm_harm_suppr()
case "cm_pitch":
self.calc_cm_reflectivity()
self.calc_cm_fm_harm_suppr()
case "mo1_mode":
self.update_mo1_mode()
case "mo1_xtal":
self.calc_mo1_bragg_angle()
self.calc_mo1_energy_resolution()
case "fm_focus":
self.update_fm_mode()
self.calc_fm_ideal_pitch()
case "fm_focx":
self.calc_fm_ideal_pitch()
case "fm_focy":
self.calc_fm_ideal_pitch()
case "fm_rotx":
self.calc_fm_reflectivity()
self.calc_cm_fm_harm_suppr()
case "fm_stripe":
self.calc_fm_reflectivity()
self.calc_cm_fm_harm_suppr()
self.calc_fm_ideal_pitch()
case "smpl":
self.calc_fm_ideal_pitch()
self.calc_positions()
self.calc_assistant_sideview()
self.calc_assistant_surfaces()
def get_assistant_config(self, apply_offset: bool = False) -> ConfigDict:
"""
Assembles the digital twin config from the assistants input.
Args:
apply_offset(bool): Applies the offset values to the config.
Defaults to False
Returns:
ConfigDict: config of the assistant
"""
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
fm_rotx = self.input.fm_rotx.value()
fm_qy = None
elif fm_focus in "Focused":
fm_rotx = self.input.fm_rotx_ideal.value()
fm_qy = None
else: # Focused
fm_rotx = self.input.fm_rotx_ideal.value()
fm_qy = self.qy
cm_stripe = self.input.cm_stripe.currentText()
cm_trx = cm_stripe_to_trx(cm_stripe)
fm_stripe = self.input.fm_stripe.currentText()
fm_trx = fm_stripe_to_trx(fm_stripe)
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
config: ConfigDict = {
"energy": self.input.energy.value(),
"h_acc": self.input.sldi_hacc.value(),
"v_acc": self.input.sldi_vacc.value(),
"cm_pitch": -self.input.cm_pitch.value(),
"cm_stripe": cm_stripe,
"cm_trx": cm_trx,
"mo1_mode": self.input.mo1_mode.currentText(),
"mo1_xtal": self.input.mo1_xtal.currentText(),
"mo1_bragg": self.bragg_angle,
"fm_rotx": -fm_rotx,
"fm_stripe": fm_stripe,
"fm_trx": fm_trx,
"fm_qy": fm_qy,
"fm_gain_height": 1,
"smpl": self.input.smpl.value(),
}
# Apply offsets
if apply_offset:
for axis, _ in config.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
if "modifier" in axis_offsets and "offset" in axis_offsets:
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]:
config[axis] += axis_offsets["offset"][idx]
break
elif "offset" in axis_offsets:
config[axis] += axis_offsets["offset"]
# Convert to SI units!
config["h_acc"] *= 1e-3
config["v_acc"] *= 1e-3
config["cm_pitch"] *= 1e-3
config["fm_rotx"] *= 1e-3
# logger.info(f'Config created: {config}')
return config
def get_reality_config(self) -> ConfigDict:
"""
Assembles the digital twin config based on the real axis positions.
Returns:
ConfigDict: config of the reality
"""
mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
if abs(mo1_trx) > 5:
mo1_mode = "Monochromatic"
else:
mo1_mode = "Pinkbeam"
mo1_bragg = self.dev.mo1_bragg.read(cached=True)
sldi_gapx = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"]
sldi_gapy = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"]
h_acc, v_acc = sldi_gap_to_acc(sldi_gapx, sldi_gapy)
cm_trx = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"]
cm_stripe = cm_trx_to_stripe(-cm_trx)
cm_pitch = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"]
fm_trx = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
fm_stripe = fm_trx_to_stripe(-fm_trx)
fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
fm_rotx_real = 2 * cm_pitch - fm_rotx
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
raw = { # Config in SI units!
"energy": mo1_bragg["mo1_bragg"]["value"],
"h_acc": h_acc,
"v_acc": v_acc,
"cm_pitch": -cm_pitch * 1e-3,
"cm_stripe": cm_stripe,
"cm_trx": cm_trx,
"mo1_mode": mo1_mode,
"mo1_xtal": mo1_bragg["mo1_bragg_crystal_current_xtal_string"]["value"],
"mo1_bragg": mo1_bragg["mo1_bragg_angle"]["value"] / 180 * np.pi,
"fm_rotx": -fm_rotx_real * 1e-3,
"fm_stripe": fm_stripe,
"fm_trx": fm_trx,
"fm_qy": None,
"fm_gain_height": 1,
"smpl": smpl,
}
config = cast(ConfigDict, raw)
# logger.info(f'Config created: {config}')
abs_open = self.dev.abs.read(cached=True)["abs_status_string"]["value"] == "OPEN"
if not abs_open:
ready = True
for mover in self.mover.mover_widgets:
if mover.status in ("moving", "error"):
ready = False
if ready:
self.mover.abs.enable_open(True) # Enable open button
else:
self.mover.abs.enable_open(False) # Disable open button
else:
self.mover.abs.enable_open(False) # Disable open button
self.mover.sldi_gapx.set_feedback(sldi_gapx)
self.mover.sldi_gapy.set_feedback(sldi_gapy)
self.mover.cm_trx.set_feedback(cm_trx)
self.mover.cm_try.set_feedback(self.dev.cm_try.read(cached=True)["cm_try"]["value"])
self.mover.cm_bnd.set_feedback(
self.dev.cm_bnd_radius.read(cached=True)["cm_bnd_radius"]["value"]
)
self.mover.cm_rotx.set_feedback(cm_pitch)
self.mover.mo1_bragg_angle.set_feedback(mo1_bragg["mo1_bragg_angle"]["value"])
self.mover.mo1_trx.set_feedback(mo1_trx)
self.mover.mo1_try.set_feedback(self.dev.mo1_try.read(cached=True)["mo1_try"]["value"])
self.mover.sl1_centery.set_feedback(
self.dev.sl1_centery.read(cached=True)["sl1_centery"]["value"]
)
self.mover.sl1_gapy.set_feedback(self.dev.sl1_gapy.read(cached=True)["sl1_gapy"]["value"])
self.mover.bm1_try.set_feedback(self.dev.bm1_try.read(cached=True)["bm1_try"]["value"])
self.mover.fm_trx.set_feedback(fm_trx)
self.mover.fm_try.set_feedback(self.dev.fm_try.read(cached=True)["fm_try"]["value"])
self.mover.fm_bnd.set_feedback(
self.dev.fm_bnd_radius.read(cached=True)["fm_bnd_radius"]["value"]
)
self.mover.fm_rotx.set_feedback(fm_rotx)
self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)["fm_roty"]["value"])
self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)["fm_rotz"]["value"])
self.mover.sl2_centery.set_feedback(
self.dev.sl2_centery.read(cached=True)["sl2_centery"]["value"]
)
self.mover.sl2_gapy.set_feedback(self.dev.sl2_gapy.read(cached=True)["sl2_gapy"]["value"])
self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)["bm2_try"]["value"])
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"])
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_feedback(smpl)
self.mover.es0wi_try.set_feedback(
self.dev.es0wi_try.read(cached=True)["es0wi_try"]["value"]
)
self.mover.abs.set_feedback(abs_open)
return config
@SafeSlot()
def adapt_reality(self, *_):
"""
Based on the real axis positions, adjust the assistant to reflect
the reality.
"""
pos = {}
pos["sldi_gapx"] = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"]
pos["sldi_gapy"] = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"]
pos["cm_trx"] = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"]
pos["cm_rotx"] = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"]
pos["mo1_trx"] = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
pos["fm_trx"] = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
pos["fm_rotx"] = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
# Removing offsets
for axis, _ in pos.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
if "modifier" in axis_offsets and "offset" in axis_offsets:
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
if rng[0] < pos[axis_offsets["modifier"]["axis"]] < rng[1]:
pos[axis] -= axis_offsets["offset"][idx]
break
elif "offset" in axis_offsets:
pos[axis] -= axis_offsets["offset"]
self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)["mo1_bragg"]["value"])
h_acc, v_acc = sldi_gap_to_acc(pos["sldi_gapx"], pos["sldi_gapy"])
self.input.sldi_hacc.set_number(h_acc * 1e3)
self.input.sldi_vacc.set_number(v_acc * 1e3)
self.input.cm_stripe.set_current_text(cm_trx_to_stripe(-pos["cm_trx"]))
self.input.cm_pitch.set_number(pos["cm_rotx"])
if abs(pos["mo1_trx"]) > 5:
mo1_mode = "Monochromatic"
else:
mo1_mode = "Pinkbeam"
self.input.mo1_mode.set_current_text(mo1_mode)
self.input.mo1_xtal.set_current_text(
self.dev.mo1_bragg.read(cached=True)["mo1_bragg_crystal_current_xtal_string"]["value"]
)
self.input.fm_stripe.set_current_text(fm_trx_to_stripe(-pos["fm_trx"]))
self.input.fm_focus.set_current_text("Manual")
fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"]
self.input.fm_rotx.set_number(fm_rotx_real)
self.input.smpl.set_number(pos["ot_es1_trz"])
self.calc_assistant(identifier="init")
@SafeSlot()
def load_offsets(self, *_, recalculate: bool = True):
"""
Loads or unloads the offsets from the file
Args:
recalculate(bool): Recalculates the assistant values after loading.
Defaults to True
"""
if self.offsets == {}:
# Load offsets
file = Path(OFFSET_FILE)
if not file.exists():
raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}")
with file.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
self.offsets = data
if recalculate:
self.calc_assistant(identifier="init")
self.settings.load_offsets.setText("Unload")
self.settings.offsets_status.setText("Loaded and applied")
self.settings.offsets_status.setColor(get_accent_colors().success.name())
self.settings.show_offsets.enable_button(True)
else:
# Unload offsets
self.offsets = {}
self.calc_assistant(identifier="init")
self.settings.load_offsets.setText("Load")
self.settings.offsets_status.setText("No offsets")
self.settings.offsets_status.setColor(get_accent_colors().default.name())
self.settings.show_offsets.enable_button(False)
@SafeSlot()
def show_offsets(self, *_):
"""
Shows the offsets in a popup window
"""
dialog = QDialog()
dialog.setWindowTitle("Digital Twin - Offsets")
dialog.setFixedWidth(500)
layout = QVBoxLayout(dialog)
layout.setSpacing(12)
layout.setContentsMargins(20, 20, 20, 20)
intro_label = QLabel("The offsets are saved in the digital twin BEC widget folder:")
intro_label.setWordWrap(True)
layout.addWidget(intro_label)
file = QLabel(OFFSET_FILE)
file.setWordWrap(True)
font = QFont()
font.setItalic(True)
file.setFont(font)
layout.addWidget(file)
text_edit = QPlainTextEdit()
text_edit.setReadOnly(True)
text_edit.setFont(QFont("Consolas", 9))
class InlineListDumper(yaml.Dumper):
"""YAML dumper that renders all sequences on a single line."""
def represent_sequence(self, tag, sequence, *_):
return super().represent_sequence(tag, sequence, flow_style=True)
text_edit.setPlainText(yaml.dump(self.offsets, Dumper=InlineListDumper, sort_keys=False))
layout.addWidget(text_edit)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.exec()
def update_fm_mode(self):
"""
Updates the focusing mirror input group based on the
selection of the focus strategy.
"""
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
self.input.fm_rotx.setVisible(True)
self.input.fm_rotx_ideal.setVisible(True)
self.input.fm_focx.setVisible(False)
self.input.fm_focy.setVisible(False)
self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam")
elif fm_focus in "Focused":
self.input.fm_rotx.setVisible(False)
self.input.fm_rotx_ideal.setVisible(True)
self.input.fm_focx.setVisible(False)
self.input.fm_focy.setVisible(False)
self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam")
else: # Defocused
self.input.fm_rotx.setVisible(False)
self.input.fm_rotx_ideal.setVisible(True)
self.input.fm_focx.setVisible(True)
self.input.fm_focy.setVisible(True)
self.input.fm_rotx_ideal.setLabel("Incidence Angle for defocused beam")
@SafeSlot()
def calc_reality(self):
"""
Updates the plots for the reality scene
"""
config = self.get_reality_config()
data = calc_sideview(config)
self.sideview_plot.update_curves("reality", data=data)
surfaces = calc_surfaces(config)
self.surface_plots.update_surfaces(scene="reality", data=surfaces)
def calc_mo1_energy_resolution(self):
"""
Calculates the energy resolution of the monochromator
"""
xtal = self.input.mo1_xtal.currentText().translate(
str.maketrans("", "", "()")
) # Remove brackets from xtal name to conform with parameters
xtal = cast(Literal["Si111", "Si311"], xtal)
energy = self.input.energy.value()
self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy))
def calc_cm_reflectivity(self):
"""
Calculates the collimating mirror reflectivity
"""
cm_stripe = self.input.cm_stripe.currentText()
cm_pitch = -self.input.cm_pitch.value() * 1e-3
energy = self.input.energy.value()
self.input.cm_refl.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, energy))
self.input.cm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV")
self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3 * energy))
self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
def calc_fm_reflectivity(self):
"""
Calculates the focusing mirror reflectivity
"""
fm_stripe = self.input.fm_stripe.currentText()
fm_focus = self.input.fm_focus.currentText()
if fm_focus in "Manual":
fm_rotx = -self.input.fm_rotx.value() * 1e-3
else:
fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3
energy = self.input.energy.value()
self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, energy))
self.input.fm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV")
self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3 * energy))
self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
def calc_cm_fm_harm_suppr(self):
"""
Calculates the combined harmonics suppression of both mirrors
"""
harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (
self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value()
)
self.input.cm_fm_harm_suppr.setValue(harm_suppr)
self.input.cm_fm_harm_suppr.setLabel(
f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV"
)
def calc_assistant_sideview(self):
"""
Updates the sideview plot based on the assistant values
"""
config = self.get_assistant_config(apply_offset=True)
data = calc_sideview(config)
self.sideview_plot.update_curves("assistant", data)
def calc_assistant_surfaces(self):
"""
Updates the surface plot based on the assistant values
"""
surfaces = calc_surfaces(self.get_assistant_config())
self.surface_plots.update_surfaces(scene="assistant", data=surfaces)
def calc_positions(self):
"""
Calculates the positions for the axes based on the assistant values
"""
out = calc_positions(self.get_assistant_config())
# Apply offsets
for axis, axis_data in out.items():
if axis in self.offsets:
axis_offsets = self.offsets[axis]
if "modifier" in axis_offsets and "offset" in axis_offsets:
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
if rng[0] < out[axis_offsets["modifier"]["axis"]]["value"] < rng[1]:
axis_data["value"] += axis_offsets["offset"][idx]
break
elif "offset" in axis_offsets:
axis_data["value"] += axis_offsets["offset"]
self.mover.sldi_gapx.set_target(out["sldi_gapx"]["value"])
self.mover.sldi_gapy.set_target(out["sldi_gapy"]["value"])
self.mover.cm_trx.set_target(out["cm_trx"]["value"])
self.mover.cm_try.set_target(out["cm_try"]["value"])
self.mover.cm_bnd.set_target(out["cm_bnd_radius"]["value"])
self.mover.cm_rotx.set_target(out["cm_rotx"]["value"])
self.mover.mo1_bragg_angle.set_target(out["mo1_bragg_angle"]["value"])
self.mover.mo1_trx.set_target(out["mo1_trx"]["value"])
self.mover.mo1_try.set_target(out["mo1_try"]["value"])
self.mover.sl1_centery.set_target(out["sl1_centery"]["value"])
self.mover.sl1_gapy.set_target(out["sl1_gapy"]["value"])
self.mover.bm1_try.set_target(out["bm1_try"]["value"])
self.mover.fm_trx.set_target(out["fm_trx"]["value"])
self.mover.fm_try.set_target(out["fm_try"]["value"])
self.mover.fm_bnd.set_target(out["fm_bnd_radius"]["value"])
self.mover.fm_rotx.set_target(out["fm_rotx"]["value"])
self.mover.fm_roty.set_target(out["fm_roty"]["value"])
self.mover.fm_rotz.set_target(out["fm_rotz"]["value"])
self.mover.sl2_centery.set_target(out["sl2_centery"]["value"])
self.mover.sl2_gapy.set_target(out["sl2_gapy"]["value"])
self.mover.bm2_try.set_target(out["bm2_try"]["value"])
self.mover.ot_try.set_target(out["ot_try"]["value"])
self.mover.ot_rotx.set_target(out["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"])
self.mover.es0wi_try.set_target(out["es0wi_try"]["value"])
def calc_mo1_bragg_angle(self):
"""
Calculates bragg angle in rad
"""
xtal = self.input.mo1_xtal.currentText()
if xtal in "Si(111)":
d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)[
"mo1_bragg_crystal_d_spacing_si111"
]["value"]
elif xtal in "Si(311)":
d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)[
"mo1_bragg_crystal_d_spacing_si311"
]["value"]
else:
raise ValueError(f"Invalid xtal selection: {xtal}")
cm_pitch = -self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] * 1e-3
mo1_mode = cast(Literal["Monochromatic", "Pinkbeam"], self.input.mo1_mode.currentText())
energy = self.input.energy.value()
theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch)
self.bragg_angle = theta
self.input.mo1_bragg_angle.setValue(theta / np.pi * 180)
def update_mo1_mode(self):
"""
Updates the monochromator input group based on the
selection of the mode.
"""
if self.input.mo1_mode.currentText() in "Monochromatic":
self.input.mo1_xtal.setVisible(True)
self.input.mo1_bragg_angle.setVisible(True)
self.input.mo1_eres.setVisible(True)
else:
self.input.mo1_xtal.setVisible(False)
self.input.mo1_bragg_angle.setVisible(False)
self.input.mo1_eres.setVisible(False)
def calc_fm_ideal_pitch(self):
"""
Calculate the ideal pitch for the focusing mirror.
"""
fm_focus = cast(
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
)
fm_stripe = self.input.fm_stripe.currentText()
smpl = self.input.smpl.value()
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
fm_focx = self.input.fm_focx.value()
fm_focy = self.input.fm_focy.value()
fm_rotx, qy = fm_ideal_pitch(
fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy
)
self.qy = qy
self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3)
def calc_cm_crit_pitch(self):
"""
Calculate the critical pitch for the collimating mirror
"""
cm_stripe = cast(Literal["Si", "Pt", "Rh"], self.input.cm_stripe.currentText())
energy = self.input.energy.value()
self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3)
if __name__ == "__main__":
app = QApplication(sys.argv)
apply_theme("light")
dispatcher = BECDispatcher(gui_id="digital_twin")
win = DigitalTwin()
win.show()
sys.exit(app.exec_())
@@ -0,0 +1 @@
{'files': ['digital_twin.py']}
@@ -0,0 +1,57 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from bec_widgets.utils.bec_designer import designer_material_icon
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from debye_bec.bec_widgets.widgets.digital_twin.digital_twin import DigitalTwin
DOM_XML = """
<ui language='c++'>
<widget class='DigitalTwin' name='digital_twin'>
</widget>
</ui>
"""
class DigitalTwinPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = DigitalTwin(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return designer_material_icon(DigitalTwin.ICON_NAME)
def includeFile(self):
return "digital_twin"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "DigitalTwin"
def toolTip(self):
return "DigitalTwin"
def whatsThis(self):
return self.toolTip()
@@ -0,0 +1,174 @@
"""
Panel for user inputs of the digital twin widget
"""
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
ComboBox,
Group,
InputNumberField,
NumberIndicator,
)
class InputPanel(QWidget):
"""Panel for user inputs of the digital twin widget"""
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# Adapt to reality
self.adapt_reality = Button(label_button="Adapt to reality", enabled=True)
# Energy
self.energy = InputNumberField(
"energy", "Energy", unit="eV", init=8979, decimals=0, single_step=100, ll=4000, hl=65000
)
# FE Slits Acceptance
self.sldi_hacc = InputNumberField(
"h_acc",
"Horizontal",
unit="mrad",
prefix="±",
init=0.25,
decimals=3,
single_step=0.01,
ll=-0.1,
hl=0.9,
)
self.sldi_vacc = InputNumberField(
"v_acc",
"Vertical",
unit="mrad",
prefix="±",
init=0.1,
decimals=3,
single_step=0.01,
ll=-0.1,
hl=0.5,
)
self.sldi_ass_group = Group("FE Slits Acceptance", [self.sldi_hacc, self.sldi_vacc])
# Collimating mirror
self.cm_stripe = ComboBox("cm_stripe", "Stripe", ["Si", "Rh", "Pt"])
self.cm_pitch = InputNumberField(
"cm_pitch",
"Pitch",
unit="mrad",
init=-2.391,
decimals=3,
single_step=0.01,
ll=-4.6,
hl=-1.2,
)
self.cm_pitch_critical = NumberIndicator("Critical Pitch", "mrad", decimals=3)
self.cm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
self.cm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
self.cm_ass_group = Group(
"Collimating Mirror",
[
self.cm_stripe,
self.cm_pitch,
self.cm_pitch_critical,
self.cm_refl,
self.cm_refl_harm,
],
)
# Monochromator
self.mo1_mode = ComboBox("mo1_mode", "Mode", ["Monochromatic", "Pinkbeam"])
self.mo1_xtal = ComboBox("mo1_xtal", "Crystal", ["Si(111)", "Si(311)"])
self.mo1_bragg_angle = NumberIndicator("Bragg Angle", "deg", decimals=1)
self.mo1_eres = NumberIndicator("Energy Resolution", "eV", decimals=2)
self.mo1_ass_group = Group(
"Monochromator", [self.mo1_mode, self.mo1_xtal, self.mo1_bragg_angle, self.mo1_eres]
)
# Focusing Mirror
self.fm_stripe = ComboBox(
"fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"]
)
self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"])
self.fm_rotx = InputNumberField(
"fm_rotx",
"Incidence Angle",
unit="mrad",
init=-2.391,
decimals=3,
single_step=0.01,
ll=-10,
hl=2,
)
self.fm_focx = InputNumberField(
"fm_focx",
"Beam Size Horizontal",
unit="mm",
init=1,
decimals=1,
single_step=0.1,
ll=0,
hl=30,
)
self.fm_focy = InputNumberField(
"fm_focy",
"Beam Size Vertical",
unit="mm",
init=1,
decimals=1,
single_step=0.1,
ll=0,
hl=10,
)
self.fm_rotx_ideal = NumberIndicator("Incidence Angle for focused beam", "mrad", decimals=3)
self.fm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
self.fm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
self.fm_ass_group = Group(
"Focusing Mirror",
[
self.fm_stripe,
self.fm_focus,
self.fm_rotx,
self.fm_focx,
self.fm_focy,
self.fm_rotx_ideal,
self.fm_refl,
self.fm_refl_harm,
],
)
# Sample
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
self.smpl = InputNumberField(
"smpl",
"Sample Position",
unit="mm",
init=23511,
decimals=0,
single_step=100,
ll=23000,
hl=30000,
)
# Assemble complete assitant group
self.input_group = Group(
"User Input",
[
self.adapt_reality,
self.energy,
self.sldi_ass_group,
self.cm_ass_group,
self.mo1_ass_group,
self.fm_ass_group,
self.cm_fm_harm_suppr,
self.smpl,
],
)
self._layout.addWidget(self.input_group)
self._layout.addStretch()
@@ -0,0 +1,232 @@
"""
Panel to move an axis to a certain position
"""
from typing import Literal
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import (
AbsorberWidget,
MoveWidget,
)
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group
class MoverPanel(QWidget):
""" "Panel to move an axis to a certain position"""
def __init__(self, dev, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self.mover_widgets = []
# FE Slits
self.sldi_gapx = MoveWidget(
dev=dev, motor="sldi_gapx", label="GAPX", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.sldi_gapx)
self.sldi_gapy = MoveWidget(
dev=dev, motor="sldi_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.sldi_gapy)
self.sldi_mov_group = Group("FE Slits", [self.sldi_gapx, self.sldi_gapy])
# Absorber
self.abs = AbsorberWidget(absorber=dev.abs, label="")
self.abs_group = Group("Absorber", [self.abs])
# Collimating mirror
self.cm_trx = MoveWidget(
dev=dev, motor="cm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.cm_trx)
self.cm_try = MoveWidget(
dev=dev, motor="cm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.cm_try)
self.cm_bnd = MoveWidget(
dev=dev, motor="cm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
)
self.mover_widgets.append(self.cm_bnd)
self.cm_rotx = MoveWidget(
dev=dev, motor="cm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
)
self.mover_widgets.append(self.cm_rotx)
self.cm_mov_group = Group(
"Collimating Mirror", [self.cm_trx, self.cm_try, self.cm_bnd, self.cm_rotx]
)
# Monochromator
self.mo1_bragg_angle = MoveWidget(
dev=dev,
motor="mo1_bragg_angle",
label="Bragg Angle",
unit="deg",
decimals=3,
deadband=0.01,
)
self.mover_widgets.append(self.mo1_bragg_angle)
self.mo1_trx = MoveWidget(
dev=dev, motor="mo1_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.mo1_trx)
self.mo1_try = MoveWidget(
dev=dev, motor="mo1_try", label="TRY", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.mo1_try)
self.mo1_mov_group = Group(
"Monochromator", [self.mo1_bragg_angle, self.mo1_trx, self.mo1_try]
)
# OP Slits 1
self.sl1_centery = MoveWidget(
dev=dev, motor="sl1_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl1_centery)
self.sl1_gapy = MoveWidget(
dev=dev, motor="sl1_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl1_gapy)
self.sl1_mov_group = Group("OP Slits 1", [self.sl1_centery, self.sl1_gapy])
# OP Beam Monitor 1
self.bm1_try = MoveWidget(
dev=dev, motor="bm1_try", label="TRY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.bm1_try)
self.bm1_mov_group = Group("OP Beam Monitor 1", [self.bm1_try])
# Focusing Mirror
self.fm_trx = MoveWidget(
dev=dev, motor="fm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.fm_trx)
self.fm_try = MoveWidget(
dev=dev, motor="fm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
)
self.mover_widgets.append(self.fm_try)
self.fm_bnd = MoveWidget(
dev=dev, motor="fm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
)
self.mover_widgets.append(self.fm_bnd)
self.fm_rotx = MoveWidget(
dev=dev, motor="fm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
)
self.mover_widgets.append(self.fm_rotx)
self.fm_roty = MoveWidget(
dev=dev, motor="fm_roty", label="YAW", unit="mrad", decimals=3, deadband=0.01
)
self.mover_widgets.append(self.fm_roty)
self.fm_rotz = MoveWidget(
dev=dev, motor="fm_rotz", label="ROLL", unit="mrad", decimals=3, deadband=0.01
)
self.mover_widgets.append(self.fm_rotz)
self.fm_mov_group = Group(
"Focusing Mirror",
[self.fm_trx, self.fm_try, self.fm_bnd, self.fm_rotx, self.fm_roty, self.fm_rotz],
)
# OP Slits 2
self.sl2_centery = MoveWidget(
dev=dev, motor="sl2_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl2_centery)
self.sl2_gapy = MoveWidget(
dev=dev, motor="sl2_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl2_gapy)
self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy])
# OP Beam Monitor 2
self.bm2_try = MoveWidget(
dev=dev, motor="bm2_try", label="TRY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.bm2_try)
self.bm2_mov_group = Group("OP Beam Monitor 2", [self.bm2_try])
# Optical Table
self.ot_try = MoveWidget(
dev=dev, motor="ot_try", label="TRY", unit="mm", decimals=2, deadband=0.2
)
self.mover_widgets.append(self.ot_try)
self.ot_rotx = MoveWidget(
dev=dev, motor="ot_rotx", label="ROTX", unit="mrad", decimals=3, deadband=0.05
)
self.mover_widgets.append(self.ot_rotx)
self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx])
# Experimental Station 0
self.es0wi_try = MoveWidget(
dev=dev, motor="es0wi_try", label="ES0 WI", unit="mm", decimals=0, deadband=0.1
)
self.mover_widgets.append(self.es0wi_try)
self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try])
# Experimental Station 1
self.ot_es1_trz = MoveWidget(
dev=dev, motor="ot_es1_trz", label="ES1 TRZ", unit="mm", decimals=0, deadband=5
)
self.mover_widgets.append(self.ot_es1_trz)
self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz])
# Assemble complete mover group
self.mover_group = Group(
"Mover",
[
self.sldi_mov_group,
self.abs_group,
self.cm_mov_group,
self.mo1_mov_group,
self.sl1_mov_group,
self.bm1_mov_group,
self.fm_mov_group,
self.sl2_mov_group,
self.bm2_mov_group,
self.ot_mov_group,
self.es0_mov_group,
self.es1_mov_group,
],
)
self._layout.addWidget(self.mover_group)
self._layout.addStretch()
def apply_theme(self, theme: Literal["dark", "light"]):
"""
Apply the theme
Args:
theme (str): Theme, either "dark" or "light"
"""
for widget in self.mover_widgets:
widget.apply_theme(theme)
@@ -0,0 +1,330 @@
"""
Two plot classes to plot side-view and surface-view
"""
from typing import Literal, Optional, cast
import numpy as np
import pyqtgraph as pg
from bec_lib import bec_logger
# pylint: disable=E0611
from qtpy.QtCore import Qt
from qtpy.QtGui import QBrush, QColor
# pylint: disable=E0611
from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import (
mirror_surface_geometries,
mo_surface_geometries,
pipe_geometries,
wall_geometries,
)
from debye_bec.bec_widgets.widgets.digital_twin.types import DataDict, SurfaceDict
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import Group
logger = bec_logger.logger
class SurfacePlots(QWidget):
"""Plot widget with two curves and legend."""
def __init__(self, parent=None):
super().__init__(parent=parent)
self._layout = QHBoxLayout(self)
self.surfaces: dict[str, SurfaceDict] = {
"assistant": {
"cm": {"x": [], "y": []},
"mo1_1": {"x": [], "y": []},
"mo1_2": {"x": [], "y": []},
"fm": {"x": [], "y": []},
},
"reality": {
"cm": {"x": [], "y": []},
"mo1_1": {"x": [], "y": []},
"mo1_2": {"x": [], "y": []},
"fm": {"x": [], "y": []},
},
}
self.plots = {"fm": {}, "mo1_2": {}, "mo1_1": {}, "cm": {}}
self.color_impenetrable = (0, 0, 0)
self.colors = [(255, 255, 0), (255, 0, 255)]
self.text_color = (255, 255, 255)
# Create plot widgets
for name, widget in self.plots.items():
plot_widget = pg.PlotWidget()
plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
plot_group = Group("Surface " + name, [plot_widget])
plot_widget.setLabel("left", "Z [mm]")
plot_widget.setLabel("bottom", "X [mm]")
plot_widget.setMouseEnabled(x=False, y=False)
plot_widget.setMenuEnabled(False)
plot_widget.hideButtons()
widget["widget"] = plot_widget
self._layout.addWidget(plot_group)
# Create surfaces
for idx, scene in enumerate(self.surfaces):
for name, _ in self.surfaces[scene].items():
if scene in "assistant":
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
pen = pg.mkPen(
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
)
z_value = 2
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
z_value = 1
widget = self.plots[name]
self.plots[name][scene] = widget["widget"].plot(
[], [], pen=pen, name=scene, brush=brush, fillLevel=0
)
self.plots[name][scene].setZValue(z_value)
self.walls = []
self.texts = []
self.plot_walls()
self.apply_theme()
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
"""
Apply the theme
Args:
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
"""
if theme is None:
app = QApplication.instance()
theme = app.theme.theme # type: ignore
bg_color = pg.getConfigOption("background")
fg_color = pg.getConfigOption("foreground")
for _, plot in self.plots.items():
# Background
plot["widget"].setBackground(bg_color)
# Axes (tick marks, tick labels, axis line)
for axis in ["left", "bottom", "right", "top"]:
ax = plot["widget"].getAxis(axis)
ax.setPen(pg.mkPen(color=fg_color))
ax.setTextPen(pg.mkPen(color=fg_color))
if theme == "light":
self.color_impenetrable = (30, 30, 30)
self.colors = [(79, 163, 224), (240, 128, 60)]
self.text_color = (255, 255, 255)
else: # dark theme
self.color_impenetrable = (180, 180, 180)
self.colors = [(26, 111, 173), (212, 83, 10)]
self.text_color = (0, 0, 0)
for idx, scene in enumerate(self.surfaces):
for name, _ in self.surfaces[scene].items():
if scene in "assistant":
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
pen = pg.mkPen(
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
)
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0)
self.plots[name][scene].setPen(pen)
self.plots[name][scene].setBrush(brush)
for wall in self.walls:
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
for text in self.texts:
text.setColor(self.text_color)
def plot_walls(self):
"""Plot walls"""
def plot_surface(widget, surfaces):
for name, surface in surfaces.items():
rect = QGraphicsRectItem(*surface)
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
widget.addItem(rect)
text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5))
widget.addItem(text)
text.setPos(surface[0] + surface[2] / 2, surface[1] + surface[3] / 2)
text.setZValue(10)
self.walls.append(rect)
self.texts.append(text)
for name, plot in self.plots.items():
if name in "cm":
plot_surface(plot["widget"], mirror_surface_geometries("cm"))
elif name in "mo1_1":
plot_surface(plot["widget"], mo_surface_geometries("mo1", 0))
elif name in "mo1_2":
plot_surface(plot["widget"], mo_surface_geometries("mo1", 1))
elif name in "fm":
plot_surface(plot["widget"], mirror_surface_geometries("fm_flat"))
plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid"))
else:
raise ValueError(f"Plot {name} not found!")
for name, plot in self.plots.items():
plot["widget"].disableAutoRange()
def update_surfaces(self, scene: Literal["assistant", "reality"], data: SurfaceDict):
"""Update the curves of the plot
Args:
scene (str): The scene to update, either "assistant" or "reality".
data (DataDict): The new data to plot, with keys "x" and "y",
each containing a list of values.
"""
self.surfaces[scene] = data
for name, device in self.surfaces[scene].items():
device = cast(DataDict, device)
plot = self.plots[name][scene]
x = np.array(device["x"] + [device["x"][0]]) if len(device["x"]) != 0 else np.array([])
y = np.array(device["y"] + [device["y"][0]]) if len(device["y"]) != 0 else np.array([])
plot.setData(x=x, y=y)
class SideviewPlot(QWidget):
"""Plot widget with two curves and legend."""
def __init__(self, parent=None):
super().__init__(parent=parent)
self._layout = QVBoxLayout(self)
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self.plot_widget = pg.PlotWidget()
self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
self.plot_widget.invertX(True)
self.plot_widget.addLegend()
self.color_impenetrable = (0, 0, 0)
self.colors = [(255, 255, 0), (255, 0, 255)]
self.data: dict[str, DataDict] = {
"assistant": {"x": [0, 1000, 2000], "y": [0, 20, 30]},
"reality": {"x": [0, 1000, 2000], "y": [0, 15, 50]},
}
self.plots = {}
self.pipes = []
self.walls = []
for idx, scene in enumerate(self.data.keys()):
if scene in "assistant":
pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine)
z_value = 2
else:
pen = pg.mkPen(color=self.colors[idx], width=2)
z_value = 1
self.plots[scene] = self.plot_widget.plot([], [], pen=pen, name=scene)
self.plots[scene].setZValue(z_value)
self.plot_group = Group("Side View", [self.plot_widget])
self.plot_widget.setLabel("left", "Height [mm]")
self.plot_widget.setLabel("bottom", "Distance [mm]")
self.plot_widget.setMouseEnabled(x=False, y=False)
self.plot_widget.setXRange(0, 25000, 0.1) # pylint: disable=E1121 # type: ignore
self.plot_widget.setYRange(-20, 120, 0.1) # pylint: disable=E1121 # type: ignore
self.plot_widget.setMenuEnabled(False)
self.plot_widget.hideButtons()
self._layout.addWidget(self.plot_group)
self._layout.addStretch()
self.plot_vacuum_pipes()
self.plot_walls()
self.apply_theme()
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
"""
Apply the theme
Args:
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
"""
if theme is None:
app = QApplication.instance()
theme = app.theme.theme # type: ignore
bg_color = pg.getConfigOption("background")
fg_color = pg.getConfigOption("foreground")
# Background
self.plot_widget.setBackground(bg_color)
# Axes (tick marks, tick labels, axis line)
for axis in ["left", "bottom", "right", "top"]:
ax = self.plot_widget.getAxis(axis)
ax.setPen(pg.mkPen(color=fg_color))
ax.setTextPen(pg.mkPen(color=fg_color))
if theme == "light":
self.color_impenetrable = (30, 30, 30)
self.colors = [(79, 163, 224), (240, 128, 60)]
self.text_color = (255, 255, 255)
else: # dark theme
self.color_impenetrable = (180, 180, 180)
self.colors = [(26, 111, 173), (212, 83, 10)]
self.text_color = (0, 0, 0)
for idx, scene in enumerate(self.data):
if scene in "assistant":
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.PenStyle.DashLine)
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3)
self.plots[scene].setPen(pen)
self.plots[scene].setBrush(brush)
for wall in self.walls:
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
for pipe in self.pipes:
pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
def plot_vacuum_pipes(self):
"""Plot vacuum pipes"""
pipes = pipe_geometries()
for pipe in pipes:
self.pipes.append(
self.plot_widget.plot(
x=pipe["x"], y=pipe["y"], pen=pg.mkPen(color=self.color_impenetrable, width=2)
)
)
def plot_walls(self):
"""Plot walls"""
walls = wall_geometries()
for wall in walls:
rect = QGraphicsRectItem(wall[0], wall[1], wall[2], wall[3])
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
self.plot_widget.addItem(rect)
self.walls.append(rect)
def update_curves(self, scene: Literal["assistant", "reality"], data: DataDict):
"""Update the curves of the plot
Args:
scene (str): The scene to update, either "assistant" or "reality".
data (DataDict): The new data to plot, with keys "x" and "y",
each containing a list of values.
"""
self.data[scene] = data
plot = self.plots[scene]
plot.setData(x=self.data[scene]["x"], y=self.data[scene]["y"])
@@ -0,0 +1,34 @@
"""
Settings panel for the digital twin widget
"""
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
Group,
TextIndicator,
)
class SettingsPanel(QWidget):
"""Settings panel for the digital twin widget"""
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# Reload offsets
self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True)
self.offsets_status = TextIndicator(label="Offsets")
self.show_offsets = Button(label="Show Offsets", label_button="Show", enabled=True)
# Assemble complete offset group
self.offset_group = Group(
"Axes Offsets", [self.load_offsets, self.offsets_status, self.show_offsets]
)
self._layout.addWidget(self.offset_group)
self._layout.addStretch()
@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from debye_bec.bec_widgets.widgets.digital_twin.digital_twin_plugin import DigitalTwinPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DigitalTwinPlugin())
if __name__ == "__main__": # pragma: no cover
main()
@@ -0,0 +1,73 @@
"""Types used for the beamline config and for plotting data"""
from typing import TypedDict
class ConfigDict(TypedDict):
"""
Typed dictionary representing the beamline configuration.
Attributes:
energy (float): Beam energy.
h_acc (float): Horizontal acceptance.
v_acc (float): Vertical acceptance.
cm_pitch (float): CM pitch angle.
cm_stripe (str): CM stripe name.
cm_trx (float): CM translation x.
mo1_mode (str): MO1 mode.
mo1_xtal (str): MO1 crystal.
mo1_bragg (float): MO1 Bragg angle.
fm_rotx (float): FM rotation x.
fm_stripe (str): FM stripe name.
fm_trx (float): FM translation x.
fm_qy (float): FM qy value.
fm_gain_height (int): FM gain height.
smpl (float): Sample value.
"""
energy: float
h_acc: float
v_acc: float
cm_pitch: float
cm_stripe: str
cm_trx: float
mo1_mode: str
mo1_xtal: str
mo1_bragg: float
fm_rotx: float
fm_stripe: str
fm_trx: float
fm_qy: None | float
fm_gain_height: int
smpl: float
class DataDict(TypedDict):
"""
Typed dictionary representing plot data.
Attributes:
x (list[float]): List of x-axis values.
y (list[float]): List of y-axis values.
"""
x: list
y: list
class SurfaceDict(TypedDict):
"""
Typed dictionary representing the surfaces of a scene,
grouping plot data by surface type.
Attributes:
cm (DataDict): Data for the cm surface.
mo1_1 (DataDict): Data for the mo1_1 surface.
mo1_2 (DataDict): Data for the mo1_2 surface.
fm (DataDict): Data for the fm surface.
"""
cm: DataDict
mo1_1: DataDict
mo1_2: DataDict
fm: DataDict
@@ -0,0 +1,594 @@
"""Move widget to display an axis and also move it through BEC"""
import threading
import time
from typing import Literal, Optional
from bec_lib import bec_logger
from bec_qthemes import material_icon
from bec_widgets.utils.colors import get_accent_colors
# pylint: disable=E0611
from qtpy.QtCore import Property # type: ignore[attr-defined]
from qtpy.QtCore import Signal # type: ignore[attr-defined]
from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread
from qtpy.QtGui import QTransform
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget
from debye_bec.devices.absorber import STATUS as ABS_STATUS
logger = bec_logger.logger
class Status:
"""Status class for the axis"""
IN_POSITION = "in_position" # green mdi.check-circle
NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle
MOVING = "moving" # blue mdi.loading (spinning)
ERROR = "error" # red mdi.alert-circle
class StatusIcon(QWidget):
"""
Displays a status icon using bec_qthemes Material Design Icons.
Handles its own spin animation for the MOVING state via QPropertyAnimation.
"""
ICON_SIZE = 20
_ICON_MAP = {
Status.IN_POSITION: ("check_circle", "#27ae60"),
Status.NOT_IN_POSITION: ("cancel", "#e6d922"),
Status.ERROR: ("warning", "#e74c3c"),
Status.MOVING: ("cycle", "#2980b9"),
}
def __init__(self, parent=None):
super().__init__(parent=parent)
self._status = None
self._rotation = 0.0
self._label = QLabel(self)
self._label.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
self._spin_anim = QPropertyAnimation(self, b"rotation") # type: ignore[call-arg]
self._spin_anim.setStartValue(0)
self._spin_anim.setEndValue(360)
self._spin_anim.setDuration(1000)
self._spin_anim.setLoopCount(-1) # Loop indefinitely
self.set_status(Status.NOT_IN_POSITION)
def get_rotation(self) -> float:
"""
Return the current rotation angle in degrees.
Returns:
float: Rotation angle in deg
"""
return self._rotation
def set_rotation(self, angle: float):
"""
Set the rotation angle and update the displayed pixmap.
Rotates the current base pixmap around its center point using a smooth
transformation. Has no effect on the display if no base pixmap is set.
Args:
angle (float): Rotation angle in degrees, clockwise.
"""
self._rotation = angle
if self._current_pixmap_base is not None:
cx = self._current_pixmap_base.width() / 2
cy = self._current_pixmap_base.height() / 2
t = QTransform().translate(cx, cy).rotate(angle).translate(-cx, -cy)
self._label.setPixmap(
self._current_pixmap_base.transformed(t, Qt.TransformationMode.SmoothTransformation)
)
rotation = Property(float, get_rotation, set_rotation) # type: ignore[call-arg]
def set_status(self, status: str):
"""
Update the widget's status and refresh the displayed icon accordingly.
Looks up the icon name and color associated with the given status from
``_ICON_MAP``, renders a new pixmap, and starts or stops the spin
animation depending on whether the status is ``Status.MOVING``. Returns
early without any updates if the status has not changed.
Args:
status (str): The new status value. Must be a key in ``_ICON_MAP``.
"""
if status == self._status:
return
self._status = status
icon_name, color = self._ICON_MAP[status]
icon = material_icon(
icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True
)
self._current_pixmap_base = icon
if status == Status.MOVING:
self._spin_anim.start()
else:
self._spin_anim.stop()
self._label.setPixmap(icon)
class MotionWorker(QObject):
"""
Executes motion on the specified motor and includes some safety during
motion for certain motors.
"""
position_changed = Signal(float)
error = Signal(bool) # True = error
finished = Signal(bool) # True = reached target, False = stopped
def __init__(self, dev, motor, target_pos: float):
super().__init__()
self.dev = dev
self.motor = motor
self._target = target_pos
self._stop_flag = threading.Event()
def stop(self):
"""Sets the stop flag"""
self._stop_flag.set()
def run(self):
"""Prepares the movement based on the axis (motor)"""
match self.motor:
case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery":
self.motion()
case "cm_trx":
self.motion(
abs_closed=True,
surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}],
)
case "cm_roty":
self.motion(
abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}]
)
case "cm_try":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
],
)
case "cm_rotx":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["cm_try"], "abs_tol": 0.05},
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
],
)
case "cm_rotz":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["cm_try"], "abs_tol": 0.05},
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
],
)
case "cm_bnd":
p1 = (
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
) / 2e-6
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
self._target = p2 - p1
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
case "mo1_try" | "mo1_trx" | "mo1_roty":
self.motion(abs_closed=True)
case "mo1_bragg_angle":
self.motion()
case "sl1_centery" | "sl1_gapy" | "bm1_try":
self.motion()
case "fm_trx":
self.motion(
abs_closed=True,
surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}],
)
case "fm_roty":
self.motion(
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
)
case "fm_try":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
],
)
case "fm_rotx":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_try"], "abs_tol": 0.05},
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
],
)
case "fm_rotz":
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_try"], "abs_tol": 0.05},
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
],
)
case "fm_bnd":
p1 = (
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3) + 4.28e-5
) / 1.84e-9
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
self._target = p2 - p1
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
case "sl2_centery" | "sl2_gapy" | "bm2_try":
self.motion()
case "ot_try" | "ot_rotx" | "ot_es1_trz":
self.motion()
case _:
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
def motion(self, abs_closed: bool = False, relative: bool = False, rb=None, surveyed_axes=None):
"""
Moves an axis while surverying a set of axes (if set).
Example surveyed_axes:
[{'device': bec_device_object, 'abs_tol': 0.1},]
Args:
surveyed_axes (list): List of dictionaries of devices
"""
if abs_closed:
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
status = self.dev.abs.close()
# TODO Set timeout to 0.001 and check if it actually raises
# (it should not start motion).
# Check of behavior of digital twin afterwards.
status.wait(timeout=5)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
surv_ax["name"] = surv_ax["device"].dotted_name
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if rb is not None:
rb["name"] = rb["device"].dotted_name
status = self.dev[self.motor].move(self._target, relative=relative)
last_check = time.time()
update_interval = 0.1
while status.status == "RUNNING":
now = time.time()
if time.time() - last_check < update_interval:
time.sleep(0.01)
last_check = now
if self._stop_flag.is_set():
self.dev[self.motor].stop()
self._stop_flag.clear()
if rb is not None:
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
else:
self.position_changed.emit(
self.dev[self.motor].read(cached=True)[self.motor]["value"]
)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
self.dev[self.motor].stop()
self.error.emit(1)
break
self.finished.emit()
class MoveWidget(QWidget):
"""
One motor stage control group containing:
- Target label (target position)
- Feedback label (current position)
- Status icon (bec_qthemes)
- Start / Stop button
"""
def __init__(self, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0):
super().__init__()
self.fb = 0.0
self.target = 0
self.dev = dev
self.motor = motor
self.deadband = deadband
self.status = Status.IN_POSITION
self._thread: QThread | None = None
self._worker: MotionWorker | None = None
self.text_color = (0, 0, 0)
self.unit = unit
self.decimals = decimals
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
# Name
self.label = QLabel(label)
self.label.setFixedWidth(100)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
# Target
self.target_label = QLabel("-")
self.target_label.setFixedWidth(100)
layout.addWidget(self.target_label)
# Feedback
self.fb_label = QLabel("-")
self.fb_label.setFixedWidth(100)
layout.addWidget(self.fb_label)
# Status icon
self.status_icon = StatusIcon()
self.status_icon.setFixedWidth(30)
self.status_icon.setContentsMargins(0, 0, 10, 0)
layout.addWidget(self.status_icon)
# Start / Stop button
self.btn_action = QPushButton("Move")
self.btn_action.setFixedWidth(90)
self.btn_action.setFixedHeight(20)
self.btn_action.clicked.connect(self._on_button_clicked)
layout.addWidget(self.btn_action)
self.btn_mode = "start"
self._apply_button_style("start")
self.apply_theme()
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
"""
Apply the theme
Args:
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
"""
if theme is None:
app = QApplication.instance()
theme = app.theme.theme # type: ignore
if theme == "light":
self.text_color = {"target": (79, 163, 224), "fb": (240, 128, 60)}
else: # dark theme
self.text_color = {"target": (26, 111, 173), "fb": (212, 83, 10)}
r, g, b = self.text_color["target"]
self.target_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
r, g, b = self.text_color["fb"]
self.fb_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
if self.btn_mode == "start":
self.btn_action.setStyleSheet(
"QPushButton "
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
)
else:
self.btn_action.setStyleSheet(
"QPushButton "
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
)
def set_target(self, target):
"""Change the target value in the ui"""
self.target = target
text = f"{target:.{int(self.decimals)}f}"
if self.unit is not None:
text = text + " " + self.unit
self.target_label.setText(text)
self._on_target_or_fb_changed()
def set_feedback(self, fb):
"""Change the feedback value in the ui"""
if self.status != Status.MOVING:
self.fb = fb
text = f"{fb:.{int(self.decimals)}f}"
if self.unit is not None:
text = text + " " + self.unit
self.fb_label.setText(text)
self._on_target_or_fb_changed()
def _apply_button_style(self, mode: str):
"""Apply a button style depending on if the button shows start or stop"""
self.btn_mode = mode
if mode == "start":
self.btn_action.setText("Move")
self.btn_action.setStyleSheet(
"QPushButton "
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
)
else: # stop
self.btn_action.setText("Stop")
self.btn_action.setStyleSheet(
"QPushButton "
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
)
def _set_status(self, status: str):
"""Set the current status icon in the ui"""
self.status = status
self.status_icon.set_status(status)
def _on_target_or_fb_changed(self):
"""Re-evaluate in-position status whenever the target value changes."""
if self.status in (Status.ERROR, Status.MOVING):
return
if abs(self.fb - self.target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
def _on_button_clicked(self):
"""Starts or stops motion depending on current situation"""
if self._thread and self._thread.isRunning():
self._stop_motion()
else:
self._start_motion()
def _start_motion(self):
"""Start a motion"""
target = self.target
if abs(target - self.fb) <= self.deadband:
self._set_status(Status.IN_POSITION)
return
self._set_status(Status.MOVING)
self._apply_button_style("stop")
self._worker = MotionWorker(self.dev, self.motor, target)
self._thread = QThread()
self._worker.moveToThread(self._thread)
self._thread.started.connect(self._worker.run)
self._worker.position_changed.connect(self._on_position_changed)
self._worker.error.connect(self._on_error)
self._worker.error.connect(self._thread.quit)
self._worker.finished.connect(self._on_motion_finished)
self._worker.finished.connect(self._thread.quit)
self._thread.finished.connect(self._cleanup_thread)
self._thread.start()
def _on_error(self):
"""Called when an error occurs"""
self._set_status(Status.ERROR)
self._apply_button_style("start")
def _stop_motion(self):
"""Attempts to stop the motion"""
if self._worker:
self._worker.stop()
def _on_position_changed(self, pos: float):
"""Change the feedback value in the ui"""
self.fb = pos
text = f"{pos:.{int(self.decimals)}f}"
if self.unit is not None:
text = text + " " + self.unit
self.fb_label.setText(text)
def _on_motion_finished(self):
"""Finished a movement"""
target = self.target
if self.status not in Status.ERROR:
if abs(self.fb - target) <= self.deadband:
self._set_status(Status.IN_POSITION)
else:
self._set_status(Status.NOT_IN_POSITION)
self._apply_button_style("start")
def _cleanup_thread(self):
"""Cleaning up of the mover thread"""
if self._thread:
self._thread.deleteLater()
self._thread = None
if self._worker:
self._worker.deleteLater()
self._worker = None
def shutdown(self):
"""Cleaning up of the mover when shutting down the application"""
if self._worker:
self._worker.stop()
if self._thread:
self._thread.quit()
self._thread.wait(2000) # max 2 s grace period
class AbsorberWidget(QWidget):
"""
Control of the frontend absorber (only open)
"""
def __init__(self, absorber, label: str = "Absorber"):
super().__init__()
self.absorber = absorber
self.fb = False
self.text_color = (0, 0, 0)
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
# Name
self.label = QLabel(label)
self.label.setFixedWidth(100)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
# Blank
self.blank_label = QLabel("")
self.blank_label.setFixedWidth(100)
layout.addWidget(self.blank_label)
# Feedback
self.fb_label = QLabel("-")
self.fb_label.setFixedWidth(100)
layout.addWidget(self.fb_label)
# Blank icon
self.blank_icon = QLabel("")
self.blank_icon.setFixedWidth(30)
self.blank_icon.setContentsMargins(0, 0, 10, 0)
layout.addWidget(self.blank_icon)
# Open
self.btn_action = QPushButton("Open")
self.btn_action.setFixedWidth(90)
self.btn_action.setFixedHeight(20)
self.btn_action.clicked.connect(self._on_button_clicked)
layout.addWidget(self.btn_action)
def set_feedback(self, fb: bool):
"""
Displays the status of the absober in the ui
Args:
fb (bool): True will set the button to Open, False to Closed
"""
self.fb = fb
if fb:
self.fb_label.setText("Open")
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().success.name()}}}")
else:
self.fb_label.setText("Closed")
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().emergency.name()}}}")
def enable_open(self, enable: bool = False):
"""
Enable or disable the open/close button
Args:
enable (bool): Enables and disables the button
"""
if enable:
self.btn_action.setStyleSheet(
"QPushButton "
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
)
self.btn_action.setEnabled(True)
else: # disabled
self.btn_action.setStyleSheet(
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
)
self.btn_action.setDisabled(True)
def _on_button_clicked(self):
"""Open absorber"""
self.absorber.open()
@@ -0,0 +1,306 @@
"""
Universal Qt widgets
"""
from functools import partial
from bec_widgets.utils.colors import get_accent_colors
from qtpy.QtCore import Qt
# pylint: disable=E0611
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QDoubleSpinBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
class Group(QGroupBox):
def __init__(self, label, widgets):
super().__init__(label)
self.layout = QVBoxLayout(self) # type: ignore
for widget in widgets:
self.layout.addWidget(widget) # type: ignore
class NumberIndicator(QWidget):
def __init__(self, label="", unit=None, highlight=False, decimals=3):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QLabel("-")
self.val.setAlignment(Qt.AlignTop) # type: ignore
# self.val.setFixedWidth(140)
layout.addWidget(self.val)
self.unit = unit
self.highlight = highlight
self.decimals = decimals
self.number = 0
if highlight:
font = QFont()
font.setBold(True)
font.setPointSize(14)
self.label.setFont(font)
self.val.setFont(font)
def value(self) -> float:
return self.number
def setLabel(self, label) -> None:
self.label.setText(label)
def setValue(self, number):
self.number = number
text = f"{number:.{int(self.decimals)}f}"
if self.unit is not None:
text = text + " " + self.unit
self.val.setText(text)
class InputNumberField(QWidget):
def __init__(
self,
identifier="",
label="",
unit=None,
prefix=None,
init=0.0,
decimals=1,
single_step=0.1,
ll=-1e6,
hl=1e6,
):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.identifier = identifier
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QDoubleSpinBox()
self.val.setRange(ll, hl)
self.val.setDecimals(decimals)
self.val.setSingleStep(single_step)
self.val.setValue(init)
if unit is not None:
self.val.setSuffix(" " + unit)
if prefix is not None:
self.val.setPrefix(prefix + " ")
# self.val.setFixedWidth(140)
layout.addWidget(self.val)
def set_number(self, number):
self.val.setValue(number)
def has_focus(self) -> bool:
return self.val.hasFocus()
def value(self) -> float:
return self.val.value()
def value_changed_connect(self, func):
"""Connect a function to the Enter/Return key press."""
self.val.valueChanged.connect(
partial(
func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value()
)
)
class ComboBox(QWidget):
def __init__(self, identifier="", label="", enums=[]):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.identifier = identifier
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.value = QComboBox()
for entry in enums:
self.value.addItem(entry)
layout.addWidget(self.value)
def set_current_text(self, text):
self.value.setCurrentText(text)
def currentText(self) -> str:
return self.value.currentText()
def has_focus(self) -> bool:
return QApplication.focusWidget() is self.value.view()
def activated_connect(self, func):
"""Connect a function to the Enter/Return key press."""
self.value.activated.connect(
partial(
func,
identifier=self.identifier,
value_obj=self.value,
value=lambda: self.value.currentText(),
)
)
def setDisabled(self, disable):
self.value.setDisabled(disable)
class Button(QWidget):
def __init__(self, label=None, label_button: str = "", enabled=False):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
if label is not None:
self.label = QLabel(label)
self.label.setFixedWidth(140)
layout.addWidget(self.label)
self.button = QPushButton(label_button)
if label is not None:
self.button.setFixedWidth(160)
self.enable_button(enabled)
layout.addWidget(self.button)
def clicked_connect(self, func):
"""Connect a function to the button press."""
self.button.clicked.connect(func)
def enable_button(self, enable: bool = False):
if enable:
self.button.setStyleSheet(
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
)
self.button.setEnabled(True)
else: # disabled
self.button.setStyleSheet(
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
)
self.button.setDisabled(True)
def setText(self, text):
self.button.setText(text)
class TextIndicator(QWidget):
def __init__(self, label):
super().__init__()
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.text = QLabel("-")
self.text.setAlignment(Qt.AlignTop) # type: ignore
layout.addWidget(self.text)
def setLabel(self, label) -> None:
self.label.setText(label)
def setText(self, text):
self.text.setText(text)
def setColor(self, color: str):
self.text.setStyleSheet(f"QLabel {{color:{color}}}")
# class Button(QWidget):
# def __init__(self, label, label_button):
# super().__init__()
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(150)
# layout.addWidget(self.label)
# self.button = QPushButton(label_button)
# self.button.setStyleSheet("color: black; background-color: dodgerblue;")
# self.button.setFixedWidth(160)
# layout.addWidget(self.button)
# def set_on_press(self, func):
# """Connect a function to the button press."""
# self.button.clicked.connect(func)
# def enable_button(self):
# self.button.setEnabled(True)
# self.button.setStyleSheet("color: black; background-color: dodgerblue;")
# def disable_button(self):
# self.button.setEnabled(False)
# self.button.setStyleSheet("color: black; background-color: grey;")
# def set_button_text(self, text):
# self.button.setText(text)
# class LED(QWidget):
# def __init__(self, states, colors, label):
# super().__init__()
# self.states = states
# self.colors = colors
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(150)
# layout.addWidget(self.label)
# self.led = QLabel()
# self.led.setFixedWidth(160)
# layout.addWidget(self.led)
# def apply_color(self, val):
# color = self.colors[self.states.index(val)]
# self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;")
# class InputTextField(QWidget):
# def __init__(self, topic, label):
# super().__init__()
# self.topic = topic
# layout = QHBoxLayout(self)
# layout.setContentsMargins(10, 0, 0, 0)
# layout.setSpacing(0)
# self.label = QLabel(label)
# self.label.setFixedWidth(140)
# self.label.setContentsMargins(0, 0, 10, 0)
# self.label.setWordWrap(True)
# layout.addWidget(self.label)
# self.val = QLineEdit()
# self.val.setPlaceholderText('0')
# # self.val.setFixedWidth(140)
# layout.addWidget(self.val)
# def set_text(self, text):
# self.val.setText(text)
# def has_focus(self) -> bool:
# return self.val.hasFocus()
# def text(self) -> str:
# return self.val.text()
# def set_on_return(self, func):
# """Connect a function to the Enter/Return key press."""
# self.val.returnPressed.connect(
# partial(func, self.val, self.topic, lambda: self.val.text())
# )
@@ -0,0 +1,50 @@
cm_try:
offset: 0.15
mo1_trx:
modifier:
axis: mo1_trx
range: [[-30, -0.1], [0.1, 30]]
offset: [0, 2.21]
mo1_try:
modifier:
axis: mo1_trx
range: [[-30, -0.1], [0.1, 30]]
offset: [0, -1.6]
sl1_centery:
offset: -1.8
fm_trx:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.16]
fm_try:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.45]
fm_rotx:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, 0.063]
fm_roty:
modifier:
axis: fm_trx
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
offset: [0, 0, 0, -0.04]
sl2_centery:
offset: 1.2
ot_try:
offset: 0
ot_rotx:
offset: 0
@@ -0,0 +1,321 @@
"""
X01DA / Debye Beamline Parameters.
This file describes the parameter of each component of the Debye beamline
to be used for raytracing and geometrical calculations.
"""
from collections import namedtuple
import numpy as np
import xrt.backends.raycing.materials as rm
# XRT definitions
filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType]
stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType]
stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType]
stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType]
stripePyrex = rm.Material(
"Si", rho=2.20
) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
filterSi3N4 = rm.Material(
["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate"
) # pyright: ignore[reportArgumentType]
filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType]
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
# General parameters
sourceHeight = 0
# Synchrotron
synchrotron = namedtuple(
"synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"]
)
sls1 = synchrotron(
eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4
)
sls2 = synchrotron(
eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6
)
# Source
bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"])
sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4)
sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1)
sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5)
sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0)
# FE slits
fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"])
feSlits = fe_slits(
name="FE-SLITS",
center=(0, 6117, sourceHeight),
center1=(0, 5045, sourceHeight),
center2=(0, 5289.5, sourceHeight),
maxDivH=1.8e-3,
maxDivV=0.8e-3,
)
# FE Window
filt = namedtuple(
"filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"]
)
feWindow = filt(
name="FE-WINDOW",
center=(0.0, 7020, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-6, 6),
limPhysY=(-3.0, 3.0),
surface="None",
material=filterDiamond,
thickness=0.1,
)
feWindow = feWindow._replace(surface=f"CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m")
# Collimating mirror
collimatingMirror = namedtuple(
"collimatingMirror",
[
"name",
"center",
"surface",
"material",
"limPhysX",
"limPhysY",
"limOptX",
"limOptY",
"R",
"pitch",
"jack1",
"jack2",
"jack3",
"tx1",
"tx2",
],
)
cm = collimatingMirror(
name="FE-CM",
center=[0, 6890, sourceHeight],
surface=("Si", "Pt", "Rh"),
material=(stripeSi, stripePt, stripeRh),
limPhysX=(-34, 34),
limPhysY=(-600, 600),
limOptX=((-21, -7, 14), (-11, 11, 23)),
limOptY=((-500, -500, -500), (500, 500, 500)),
R=[3e6, 15e6],
pitch=[-5.0e-3, -0.0e-3],
jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global)
jack2=[-210.0, 8310.0, 0.0],
jack3=[210.0, 8310.0, 0.0],
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
tx2=[0.0, 575],
) # X-Stage 2
apertures = namedtuple("apertures", ["name", "center", "opening"])
fePS = apertures(
name="FE-PS", center=[0, 8815, sourceHeight], opening=[-20.0, 20.0, -20.0 + 12.5, 20.0 + 12.5]
) # left, right, bottom, top
opWbBsBlock = apertures(
name="OP-WB-BS-BLOCK", center=[0.0, 13860, sourceHeight], opening=[-18.0, 18.0, 25, 85.5]
) # left, right, bottom, top
# opening=[-18., 18., 42, 76], # X10DA
# Monochromator
monochromator = namedtuple(
"monochromator",
[
"name",
"center",
"xtal",
"material1",
"material2",
"xtalWidth",
"xtalOffsetX",
"xtalLength1",
"xtalLength2",
"xtalGap",
"rotOffset",
"heightOffset",
"braggLim",
"jack1",
"jack2",
"jack3",
"tx",
],
)
mo1 = monochromator(
name="OP-MO1",
center=[0.0, 11750, sourceHeight],
xtal=("Si311", "Si111"),
material1=(si311_1, si111_1),
material2=(si311_2, si111_2),
xtalWidth=(24, 24),
xtalOffsetX=(-21.2, 21.2),
xtalLength1=(55, 55),
xtalLength2=(105, 105),
xtalGap=(8, 8),
rotOffset=6,
heightOffset=8.5,
braggLim=[3.6, 33],
jack1=[0.0, 11350.0, 0.0], # Tripod maybe not available!
jack2=[-400.0, 12350.0, 0.0],
jack3=[400.0, 12350.0, 0.0],
tx=0.0,
) # X-Stage [x]
mo2 = monochromator(
name="OP-CCM2",
center=[0.0, 13250, sourceHeight],
xtal=("Si311", "Si111"),
material1=(si311_1, si111_1),
material2=(si311_2, si111_2),
xtalWidth=(24, 24),
xtalOffsetX=(-21, 21),
xtalLength1=(55, 55),
xtalLength2=(105, 105),
xtalGap=(8, 8),
rotOffset=6,
heightOffset=8.5,
braggLim=[3.6, 33],
jack1=[0.0, 13350.0, 0.0], # Tripod maybe not available!
jack2=[-400.0, 14350.0, 0.0],
jack3=[400.0, 14350.0, 0.0],
tx=0.0,
) # X-Stage [x]
# OP Slits
op_slits = namedtuple("op_slits", ["name", "center"])
opSlits1 = op_slits(name="OP-SLITS 1", center=(0, 14349.6, sourceHeight))
opSlits2 = op_slits(name="OP-SLITS 2", center=(0, 18134.8, sourceHeight))
# OP Beam Monitors
op_bm = namedtuple("op_bm", ["name", "center"])
opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14599.6, sourceHeight))
opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 18384.8, sourceHeight))
# Focusing mirror
focusingMirror = namedtuple(
"focusingMirror",
[
"name",
"center",
"surfaceToroid",
"materialToroid",
"surfaceFlat",
"materialFlat",
"limPhysXToroid",
"limPhysYToroid",
"limPhysXFlat",
"limPhysYFlat",
"limOptXToroid",
"limOptYToroid",
"limOptXFlat",
"limOptYFlat",
"R",
"pitch",
"r",
"xToroid",
"xFlat",
"hToroid",
"jack1",
"jack2",
"jack3",
"tx1",
"tx2",
],
)
fm = focusingMirror(
name="OP-FM",
center=[0.0, 15670, sourceHeight], # nominal height 58 mm above ring, SLS1!
surfaceToroid=("Rh", "Pt"),
materialToroid=(stripeRh, stripePt),
surfaceFlat=("Rh", "Pt"),
materialFlat=(stripeRh, stripePt),
limPhysXToroid=(-79.0, 79.0),
limPhysYToroid=(-575.0, 575.0),
limPhysXFlat=(-79.0, 79.0),
limPhysYFlat=(-575.0, 575.0),
limOptXToroid=((-38, 66), (-66, 31)),
limOptYToroid=((-500.0, -500.0), (500.0, 500.0)),
limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)),
limOptYFlat=((-500.0, -500.0), (500.0, 500.0)),
R=[3e6, 15e6],
pitch=[-5.0e-3, 0e-3],
r=[35.510, 24.986],
xToroid=[-52, 48.5], # offset in local x
xFlat=[-20.95, 8.55],
hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
jack1=[-130.0, 15535 - 538.0, 0.0],
jack2=[130.0, 15535 + 538.0, 0.0],
jack3=[0.0, 15535 + 538.0, 0.0],
tx1=[0.0, -575.0], # X-Stage 1 [x, y]
tx2=[0.0, 575.0],
) # X-Stage 2 [x, y]
# EH Window
ehWindow = filt(
name="EH-WINDOW",
center=(0.0, 19998.3, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-20.0, 20.0),
limPhysY=(-4, 4),
surface="None",
material=filterSi3N4,
thickness=0.002,
)
ehWindow = ehWindow._replace(surface=f"Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m")
# Sample
sample = namedtuple("sample", ["name", "center"])
smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight])
smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight])
# Vacuum pipes
# DN40CF ID = 35 mm oder 37 mm
# DN50CF ID = 47.5 mm
# DN63CF ID = 60.2 mm oder 66 mm
# DN100CF ID = 97.4 mm oder 104 mm
pipe = namedtuple("pipes", ["center", "diameter", "start", "end"])
vacuum_pipes = pipe(
center=[27.5, (37.5 + 27.5) / 2, 37.5, 62.5, 72.5],
diameter=[97.4, 97.4, 97.4, 97.4, 97.4],
start=[10952.88, 11750 + 250, mo2.center[1] + 250, 14000, fm.center[1]],
end=[11750 - 250, mo2.center[1] - 250, 14000, fm.center[1], ehWindow.center[1]],
)
Walls = namedtuple("walls", ["start", "end", "height"])
walls = Walls(start=[13999.30], end=[13999 + 75.5 + 30], height=[[-20, 25]])
@@ -0,0 +1,34 @@
###################################
## Beam Monitors ##
###################################
beam_monitor_1:
readoutPriority: async
description: Beam monitor 1
deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam
deviceConfig:
prefix: "X01DA-OP-GIGE01:"
onFailure: retry
enabled: true
softwareTrigger: false
beam_monitor_2:
readoutPriority: async
description: Beam monitor 2
deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam
deviceConfig:
prefix: "X01DA-OP-GIGE02:"
onFailure: retry
enabled: true
softwareTrigger: false
xray_eye:
readoutPriority: async
description: X-ray eye
deviceClass: debye_bec.devices.cameras.basler_cam.BaslerCam
deviceConfig:
prefix: "X01DA-ES-XRAYEYE:"
onFailure: retry
enabled: true
softwareTrigger: false
@@ -1,692 +0,0 @@
###################
#### FRONT END ####
###################
## Slit Diaphragm -- Physical positioners
sldi_trxr:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Ring-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
sldi_trxw:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Wall-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryb:
readoutPriority: baseline
description: Front-end slit diaphragm Y-translation Bottom-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryt:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Top-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
## Slit Diaphragm -- Virtual positioners
sldi_centerx:
readoutPriority: baseline
description: Front-end slit diaphragm X-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapx:
readoutPriority: baseline
description: Front-end slit diaphragm X-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_centery:
readoutPriority: baseline
description: Front-end slit diaphragm Y-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapy:
readoutPriority: baseline
description: Front-end slit diaphragm Y-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
## Collimating Mirror -- Physical Positioners
cm_trxu:
readoutPriority: baseline
description: Collimating Mirror X-translation upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRXU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trxd:
readoutPriority: baseline
description: Collimating Mirror X-translation downstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRXD
onFailure: retry
enabled: true
softwareTrigger: false
cm_tryu:
readoutPriority: baseline
description: Collimating Mirror Y-translation upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydr:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream ring
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYDR
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydw:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream wall
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYDW
onFailure: retry
enabled: true
softwareTrigger: false
cm_bnd:
readoutPriority: baseline
description: Collimating Mirror bender
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:BND
onFailure: retry
enabled: true
softwareTrigger: false
## Collimating Mirror -- Virtual Positioners
cm_rotx:
readoutPriority: baseline
description: Collimating Morror Pitch
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
cm_roty:
readoutPriority: baseline
description: Collimating Morror Yaw
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
cm_rotz:
readoutPriority: baseline
description: Collimating Morror Roll
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTZ
onFailure: retry
enabled: true
softwareTrigger: false
cm_xctp:
readoutPriority: baseline
description: Collimating Morror Center Point X
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:XTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_ytcp:
readoutPriority: baseline
description: Collimating Morror Center Point Y
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:YTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_ztcp:
readoutPriority: baseline
description: Collimating Morror Center Point Z
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ZTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_xstripe:
readoutPriority: baseline
description: Collimating Morror X Stripe
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:XSTRIPE
onFailure: retry
enabled: true
softwareTrigger: false
###################
###### OPTICS #####
###################
## Bragg Monochromator
mo1_bragg:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.Mo1Bragg
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
enabled: true
softwareTrigger: false
## Monochromator -- Physical Positioners
mo_try:
readoutPriority: baseline
description: Monochromator Y Translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
mo_trx:
readoutPriority: baseline
description: Monochromator X Translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
mo_roty:
readoutPriority: baseline
description: Monochromator Yaw
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
## Focusing Mirror -- Physical Positioners
fm_trxu:
readoutPriority: baseline
description: Focusing Mirror X-translation upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:TRXU
onFailure: retry
enabled: true
softwareTrigger: false
fm_trxd:
readoutPriority: baseline
description: Focusing Mirror X-translation downstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:TRXD
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryd:
readoutPriority: baseline
description: Focusing Mirror Y-translation downstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:TRYD
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryur:
readoutPriority: baseline
description: Focusing Mirror Y-translation upstream ring
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:TRYUR
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryuw:
readoutPriority: baseline
description: Focusing Mirror Y-translation upstream wall
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:TRYUW
onFailure: retry
enabled: true
softwareTrigger: false
fm_bnd:
readoutPriority: baseline
description: Focusing Mirror bender
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:BND
onFailure: retry
enabled: true
softwareTrigger: false
## Focusing Mirror -- Virtual Positioners
fm_rotx:
readoutPriority: baseline
description: Focusing Morror Pitch
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
fm_roty:
readoutPriority: baseline
description: Focusing Morror Yaw
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
fm_rotz:
readoutPriority: baseline
description: Focusing Morror Roll
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:ROTZ
onFailure: retry
enabled: true
softwareTrigger: false
fm_xctp:
readoutPriority: baseline
description: Focusing Morror Center Point X
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:XTCP
onFailure: retry
enabled: true
softwareTrigger: false
fm_ytcp:
readoutPriority: baseline
description: Focusing Morror Center Point Y
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:YTCP
onFailure: retry
enabled: true
softwareTrigger: false
fm_ztcp:
readoutPriority: baseline
description: Focusing Morror Center Point Z
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:ZTCP
onFailure: retry
enabled: true
softwareTrigger: false
fm_xstripe:
readoutPriority: baseline
description: Focusing Morror X Stripe
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-FM:XSTRIPE
onFailure: retry
enabled: true
softwareTrigger: false
## Optics Slits 1 -- Physical positioners
sl1_trxr:
readoutPriority: baseline
description: Optics slits 1 X-translation Ring-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
sl1_trxw:
readoutPriority: baseline
description: Optics slits 1 X-translation Wall-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
sl1_tryb:
readoutPriority: baseline
description: Optics slits 1 Y-translation Bottom-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
sl1_tryt:
readoutPriority: baseline
description: Optics slits 1 X-translation Top-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
bm1_try:
readoutPriority: baseline
description: Beam Monitor 1 Y-translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-BM1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
## Optics Slits 1 -- Virtual positioners
sl1_centerx:
readoutPriority: baseline
description: Optics slits 1 X-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
sl1_gapx:
readoutPriority: baseline
description: Optics slits 1 X-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
sl1_centery:
readoutPriority: baseline
description: Optics slits 1 Y-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
sl1_gapy:
readoutPriority: baseline
description: Optics slits 1 Y-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL1:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
## Optics Slits 2 -- Physical positioners
sl2_trxr:
readoutPriority: baseline
description: Optics slits 2 X-translation Ring-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
sl2_trxw:
readoutPriority: baseline
description: Optics slits 2 X-translation Wall-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
sl2_tryb:
readoutPriority: baseline
description: Optics slits 2 Y-translation Bottom-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
sl2_tryt:
readoutPriority: baseline
description: Optics slits 2 X-translation Top-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
bm2_try:
readoutPriority: baseline
description: Beam Monitor 2 Y-translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-BM2:TRY
onFailure: retry
enabled: true
softwareTrigger: false
## Optics Slits 2 -- Virtual positioners
sl2_centerx:
readoutPriority: baseline
description: Optics slits 2 X-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
sl2_gapx:
readoutPriority: baseline
description: Optics slits 2 X-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
sl2_centery:
readoutPriority: baseline
description: Optics slits 2 Y-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
sl2_gapy:
readoutPriority: baseline
description: Optics slits 2 Y-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-SL2:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
###############################
###### EXPERIMENTAL HUTCH #####
###############################
###########################################
## Optical Table -- Physical Positioners ##
###########################################
ot_tryu:
readoutPriority: baseline
description: Optical Table Y-Translation Upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES-OT:TRYU
onFailure: retry
enabled: true
softwareTrigger: false
ot_tryd:
readoutPriority: baseline
description: Optical Table Y-Translation Downstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES-OT:TRYD
onFailure: retry
enabled: true
softwareTrigger: false
############################################
## Optical Table -- Virtual Positioners ###
############################################
ot_try:
readoutPriority: baseline
description: Optical Table Y-Translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES-OT:TRY
onFailure: retry
enabled: true
softwareTrigger: false
ot_pitch:
readoutPriority: baseline
description: Optical Table Pitch
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES-OT:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
###############################################
## End Station Slits -- Physical Positioners ##
###############################################
es0sl_trxr:
readoutPriority: baseline
description: End Station slits X-translation Ring-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_trxw:
readoutPriority: baseline
description: End Station slits X-translation Wall-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_tryb:
readoutPriority: baseline
description: End Station slits Y-translation Bottom-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_tryt:
readoutPriority: baseline
description: End Station slits X-translation Top-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
##############################################
## End Station Slits -- Virtual positioners ##
##############################################
es0sl_center:
readoutPriority: baseline
description: End Station slits X-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_gapx:
readoutPriority: baseline
description: End Station slits X-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_centery:
readoutPriority: baseline
description: End Station slits Y-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_gapy:
readoutPriority: baseline
description: End Station slits Y-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-SL:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
#########################################
## Exit Window -- Physical Positioners ##
#########################################
es0wi_try:
readoutPriority: baseline
description: End Station 0 Exit Window Y-translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-ES0-WI:TRY
onFailure: retry
enabled: true
softwareTrigger: false
@@ -0,0 +1,449 @@
###################################
## Optical Table ##
###################################
ot_tryu:
readoutPriority: baseline
description: Optical Table Y-Translation Upstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES-OT:TRYU
onFailure: retry
enabled: true
softwareTrigger: false
ot_tryd:
readoutPriority: baseline
description: Optical Table Y-Translation Downstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES-OT:TRYD
onFailure: retry
enabled: true
softwareTrigger: false
ot_es1_trz:
readoutPriority: baseline
description: Optical Table ES1 Z-Translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-OT:TRZ
onFailure: retry
enabled: true
softwareTrigger: false
ot_es2_trz:
readoutPriority: baseline
description: Optical Table ES2 Z-Translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-OT:TRZ
onFailure: retry
enabled: true
softwareTrigger: false
ot_try:
readoutPriority: baseline
description: Optical Table Y-Translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES-OT:TRY
onFailure: retry
enabled: true
softwareTrigger: false
ot_rotx:
readoutPriority: baseline
description: Optical Table Pitch
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES-OT:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Exit Window ##
###################################
es0wi_try:
readoutPriority: baseline
description: End Station 0 Exit Window Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-WI:TRY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## ES0 Filter ##
###################################
es0filter:
readoutPriority: baseline
description: ES0 filter station
deviceClass: debye_bec.devices.es0filter.ES0Filter
deviceConfig:
prefix: "X01DA-ES0-FI:"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Slits ES0 ##
###################################
es0sl_trxr:
readoutPriority: baseline
description: End Station slits X-translation Ring-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_trxw:
readoutPriority: baseline
description: End Station slits X-translation Wall-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_tryb:
readoutPriority: baseline
description: End Station slits Y-translation Bottom-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_tryt:
readoutPriority: baseline
description: End Station slits X-translation Top-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_center:
readoutPriority: baseline
description: End Station slits X-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_gapx:
readoutPriority: baseline
description: End Station slits X-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_centery:
readoutPriority: baseline
description: End Station slits Y-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
es0sl_gapy:
readoutPriority: baseline
description: End Station slits Y-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES0-SL:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Alignment Laser ##
###################################
es1_alignment_laser:
readoutPriority: baseline
description: ES1 alignment laser
deviceClass: ophyd.EpicsSignal
deviceConfig:
read_pv: "X01DA-ES1-LAS:Relay"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Sample Manipulator ##
###################################
es1man_trx:
readoutPriority: baseline
description: End Station sample manipulator X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-MAN1:TRX
onFailure: retry
enabled: true
softwareTrigger: false
es1man_try:
readoutPriority: baseline
description: End Station sample manipulator Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-MAN1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
es1man_trz:
readoutPriority: baseline
description: End Station sample manipulator Z-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-MAN1:TRZ
onFailure: retry
enabled: true
softwareTrigger: false
es1man_roty:
readoutPriority: baseline
description: End Station sample manipulator Y-rotation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-MAN1:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Segmented Arc ##
###################################
es1arc_roty:
readoutPriority: baseline
description: End Station segmented arc Y-rotation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-ARC:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
es1det1_trx:
readoutPriority: baseline
description: End Station SDD 1 X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-DET1:TRX
onFailure: retry
enabled: true
softwareTrigger: false
es1bm1_trx:
readoutPriority: baseline
description: End Station X-ray Eye X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-BM1:TRX
onFailure: retry
enabled: true
softwareTrigger: false
es1det2_trx:
readoutPriority: baseline
description: End Station SDD 2 X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-DET2:TRX
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## IC1 + IC2 Manipulator ##
###################################
es2ma2_try:
readoutPriority: baseline
description: End Station ionization chamber 1+2 Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-MA2:TRY
onFailure: retry
enabled: true
softwareTrigger: false
es2ma2_trz:
readoutPriority: baseline
description: End Station ionization chamber 1+2 Z-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-MA2:TRZ
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## XRD Detector Manipulator ##
###################################
es2ma3_try:
readoutPriority: baseline
description: End Station XRD detector Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-MA3:TRY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Hutch Env. Sensors + Light ##
###################################
es_temperature1:
readoutPriority: baseline
description: ES temperature sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH1:TEMP"
onFailure: retry
enabled: true
softwareTrigger: false
es_humidity1:
readoutPriority: baseline
description: ES humidity sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH1:HUMIREL"
onFailure: retry
enabled: true
softwareTrigger: false
es_pressure1:
readoutPriority: baseline
description: ES ambient pressure sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH1:PRES"
onFailure: retry
enabled: true
softwareTrigger: false
es_temperature2:
readoutPriority: baseline
description: ES temperature sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH2:TEMP"
onFailure: retry
enabled: true
softwareTrigger: false
es_humidity2:
readoutPriority: baseline
description: ES humidity sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH2:HUMIREL"
onFailure: retry
enabled: true
softwareTrigger: false
es_pressure2:
readoutPriority: baseline
description: ES ambient pressure sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-PC-I2C:_CH2:PRES"
onFailure: retry
enabled: true
softwareTrigger: false
es_light_toggle:
readoutPriority: baseline
description: ES light toggle
deviceClass: ophyd.EpicsSignal
deviceConfig:
read_pv: "X01DA-EH-LIGHT:TOGGLE"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_o2:
readoutPriority: baseline
description: ES Gas Sensor O2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-O2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2s:
readoutPriority: baseline
description: ES Gas Sensor H2S
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2S"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_no2:
readoutPriority: baseline
description: ES Gas Sensor NO2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NO2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_co:
readoutPriority: baseline
description: ES Gas Sensor CO
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-CO"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2:
readoutPriority: baseline
description: ES Gas Sensor H2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_nh3:
readoutPriority: baseline
description: ES Gas Sensor NH3
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NH3"
onFailure: retry
enabled: true
softwareTrigger: false
@@ -0,0 +1,243 @@
###################################
## Frontend Absorber ##
###################################
abs:
readoutPriority: baseline
description: Frontend Absorber
deviceClass: debye_bec.devices.absorber.Absorber
deviceConfig:
prefix: "X01DA-FE-ABS1:"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Frontend Slits ##
###################################
sldi_trxr:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Ring-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
sldi_trxw:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Wall-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryb:
readoutPriority: baseline
description: Front-end slit diaphragm Y-translation Bottom-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryt:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Top-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
sldi_centerx:
readoutPriority: baseline
description: Front-end slit diaphragm X-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapx:
readoutPriority: baseline
description: Front-end slit diaphragm X-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_centery:
readoutPriority: baseline
description: Front-end slit diaphragm Y-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapy:
readoutPriority: baseline
description: Front-end slit diaphragm Y-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-SLDI:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Collimating Mirror ##
###################################
cm_trxu:
readoutPriority: baseline
description: Collimating Mirror X-translation upstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:TRXU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trxd:
readoutPriority: baseline
description: Collimating Mirror X-translation downstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:TRXD
onFailure: retry
enabled: true
softwareTrigger: false
cm_tryu:
readoutPriority: baseline
description: Collimating Mirror Y-translation upstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:TRYU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydr:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream ring
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:TRYDR
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydw:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream wall
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:TRYDW
onFailure: retry
enabled: true
softwareTrigger: false
cm_bnd:
readoutPriority: baseline
description: Collimating Mirror bender
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:BND
onFailure: retry
enabled: true
softwareTrigger: false
cm_bnd_radius:
readoutPriority: baseline
description: Collimating Mirror Bending Radius
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: X01DA-CPCL-CM:BNDFORCE
onFailure: retry
readOnly: true
enabled: true
softwareTrigger: false
cm_rotx:
readoutPriority: baseline
description: Collimating Morror Pitch
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
cm_roty:
readoutPriority: baseline
description: Collimating Morror Yaw
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
cm_rotz:
readoutPriority: baseline
description: Collimating Morror Roll
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:ROTZ
onFailure: retry
enabled: true
softwareTrigger: false
cm_trx:
readoutPriority: baseline
description: Collimating Morror Center Point X
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:XTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_try:
readoutPriority: baseline
description: Collimating Morror Center Point Y
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:YTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_ztcp:
readoutPriority: baseline
description: Collimating Morror Center Point Z
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:ZTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_xstripe:
readoutPriority: baseline
description: Collimating Morror X Stripe
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-FE-CM:XSTRIPE
onFailure: retry
enabled: true
softwareTrigger: false
@@ -0,0 +1,34 @@
###################################
## Hutch Cameras ##
###################################
hutch_cam_1:
readoutPriority: baseline
description: Hutch Camera 1
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085420"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_2:
readoutPriority: baseline
description: Hutch Camera 2
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085436"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_3:
readoutPriority: baseline
description: Hutch Camera 3
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085435"
onFailure: retry
enabled: true
softwareTrigger: false
@@ -0,0 +1,18 @@
###################################
## SLS Machine ##
###################################
curr:
readoutPriority: baseline
description: SLS ring current
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
auto_monitor: true
read_pv: AGEBD-DBPM3CURR:CURRENT-AVG
deviceTags:
- machine
onFailure: buffer
enabled: true
readOnly: true
softwareTrigger: false
+411
View File
@@ -0,0 +1,411 @@
###################################
## Monochromator ##
###################################
mo1_try:
readoutPriority: baseline
description: Monochromator Y Translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-MO1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
mo1_trx:
readoutPriority: baseline
description: Monochromator X Translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-MO1:TRX
onFailure: retry
enabled: true
softwareTrigger: false
mo1_roty:
readoutPriority: baseline
description: Monochromator Yaw
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-MO1:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Optics Slits + Beam Monitor 1 ##
###################################
sl1_trxr:
readoutPriority: baseline
description: Optics slits 1 X-translation Ring-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_trxw:
readoutPriority: baseline
description: Optics slits 1 X-translation Wall-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_tryb:
readoutPriority: baseline
description: Optics slits 1 Y-translation Bottom-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_tryt:
readoutPriority: baseline
description: Optics slits 1 X-translation Top-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
bm1_try:
readoutPriority: baseline
description: Beam Monitor 1 Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-BM1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_centerx:
readoutPriority: baseline
description: Optics slits 1 X-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_gapx:
readoutPriority: baseline
description: Optics slits 1 X-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_centery:
readoutPriority: baseline
description: Optics slits 1 Y-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl1_gapy:
readoutPriority: baseline
description: Optics slits 1 Y-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL1:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
###################################
## Focusing Mirror ##
###################################
fm_trxu:
readoutPriority: baseline
description: Focusing Mirror X-translation upstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:TRXU
onFailure: retry
enabled: true
softwareTrigger: false
fm_trxd:
readoutPriority: baseline
description: Focusing Mirror X-translation downstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:TRXD
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryd:
readoutPriority: baseline
description: Focusing Mirror Y-translation downstream
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:TRYD
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryur:
readoutPriority: baseline
description: Focusing Mirror Y-translation upstream ring
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:TRYUR
onFailure: retry
enabled: true
softwareTrigger: false
fm_tryuw:
readoutPriority: baseline
description: Focusing Mirror Y-translation upstream wall
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:TRYUW
onFailure: retry
enabled: true
softwareTrigger: false
fm_bnd:
readoutPriority: baseline
description: Focusing Mirror bender
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:BND
onFailure: retry
enabled: true
softwareTrigger: false
fm_bnd_radius:
readoutPriority: baseline
description: Focusing Mirror Bending Radius
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: X01DA-CPCL-FM:BNDFORCE
onFailure: retry
readOnly: true
enabled: true
softwareTrigger: false
fm_rotx:
readoutPriority: baseline
description: Focusing Morror Pitch
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
fm_roty:
readoutPriority: baseline
description: Focusing Morror Yaw
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
fm_rotz:
readoutPriority: baseline
description: Focusing Morror Roll
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:ROTZ
onFailure: retry
enabled: true
softwareTrigger: false
fm_trx:
readoutPriority: baseline
description: Focusing Morror Center Point X
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:XTCP
onFailure: retry
enabled: true
softwareTrigger: false
fm_try:
readoutPriority: baseline
description: Focusing Morror Center Point Y
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:YTCP
onFailure: retry
enabled: true
softwareTrigger: false
fm_ztcp:
readoutPriority: baseline
description: Focusing Morror Center Point Z
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-FM:ZTCP
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Optics Slits + Beam Monitor 2 ##
###################################
sl2_trxr:
readoutPriority: baseline
description: Optics slits 2 X-translation Ring-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_trxw:
readoutPriority: baseline
description: Optics slits 2 X-translation Wall-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_tryb:
readoutPriority: baseline
description: Optics slits 2 Y-translation Bottom-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_tryt:
readoutPriority: baseline
description: Optics slits 2 X-translation Top-edge
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
bm2_try:
readoutPriority: baseline
description: Beam Monitor 2 Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-BM2:TRY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_centerx:
readoutPriority: baseline
description: Optics slits 2 X-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_gapx:
readoutPriority: baseline
description: Optics slits 2 X-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_centery:
readoutPriority: baseline
description: Optics slits 2 Y-center
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
sl2_gapy:
readoutPriority: baseline
description: Optics slits 2 Y-gap
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-OP-SL2:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
deviceTags:
- optics
- slits
@@ -0,0 +1,80 @@
###################################
## General ##
###################################
## SLS Machine
machine_config:
- !include ./x01da_machine.yaml
## Beam Monitors OP + EH
beam_monitors_config:
- !include ./x01da_beam_monitors.yaml
###################################
## Frontend ##
###################################
## Frontend
frontend_config:
- !include ./x01da_frontend.yaml
###################################
## Optics Hutch ##
###################################
## Bragg Monochromator
mo1_bragg:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
enabled: true
softwareTrigger: false
mo1_bragg_angle:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg_angle.Mo1BraggAngle
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
enabled: true
softwareTrigger: false
## Remaining optics hutch
optics_config:
- !include ./x01da_optics.yaml
###################################
## Experimental Hutch ##
###################################
# ## NIDAQ
nidaq:
readoutPriority: monitored
description: NIDAQ backend for data reading for debye scans
deviceClass: debye_bec.devices.nidaq.nidaq.Nidaq
deviceConfig:
prefix: "X01DA-PC-SCANSERVER:"
onFailure: retry
enabled: true
softwareTrigger: false
## XAS (ICx, SDD, ref foils)
xas_config:
- !include ./x01da_xas.yaml
## XRD (Pilatus, pinhole, beamstop)
#xrd_config:
# - !include ./x01da_xrd.yaml
# Commented out because too slow
## Hutch cameras
# hutch_cams:
# - !include ./x01da_hutch_cameras.yaml
## Remaining experimental hutch
es_config:
- !include ./x01da_experimental_hutch.yaml
@@ -1,19 +0,0 @@
## Bragg Monochromator
mo1_bragg:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.Mo1Bragg
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
enabled: true
softwareTrigger: false
dummy_pv:
readoutPriority: monitored
description: Heartbeat of Bragg
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-OP-MO1:BRAGG:heartbeat_RBV"
onFailure: retry
enabled: true
softwareTrigger: false
+83
View File
@@ -0,0 +1,83 @@
###################################
## Ionization Chambers ##
###################################
ic0:
readoutPriority: baseline
description: Ionization chamber 0
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
ic1:
readoutPriority: baseline
description: Ionization chamber 1
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
ic2:
readoutPriority: baseline
description: Ionization chamber 2
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
pips:
readoutPriority: baseline
description: Pips diode
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.Pips
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Reference Foil Changer ##
###################################
reffoilchanger:
readoutPriority: baseline
description: ES2 reference foil changer
deviceClass: debye_bec.devices.reffoilchanger.Reffoilchanger
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## SDD Sensors ##
###################################
sdd1_temperature:
readoutPriority: baseline
description: SDD1 temperature sensor
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-ES1-DET1:Temperature"
onFailure: retry
enabled: true
softwareTrigger: false
sdd1_humidity:
readoutPriority: baseline
description: SDD1 humidity sensor
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-ES1-DET1:Humidity"
kind: "config"
onFailure: retry
enabled: true
softwareTrigger: false
+108
View File
@@ -0,0 +1,108 @@
###################################
## Pinhole ##
###################################
pin1_trx:
readoutPriority: baseline
description: Pinhole X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-PIN1:TRX
onFailure: retry
enabled: true
softwareTrigger: false
tags: Endstation
pin1_try:
readoutPriority: baseline
description: Pinhole Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-PIN1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
tags: Endstation
pin1_rotx:
readoutPriority: baseline
description: Pinhole X-rotation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-PIN1:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
tags: Endstation
pin1_roty:
readoutPriority: baseline
description: Pinhole Y-rotation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES1-PIN1:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
tags: Endstation
###################################
## Beam Stop ##
###################################
es2bs_trx:
readoutPriority: baseline
description: End Station beamstop X-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-BS:TRX
onFailure: retry
enabled: true
softwareTrigger: false
es2bs_try:
readoutPriority: baseline
description: End Station beamstop Y-translation
deviceClass: ophyd_devices.EpicsMotorEC
deviceConfig:
prefix: X01DA-ES2-BS:TRY
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Pilatus ##
###################################
pilatus_curtain:
readoutPriority: baseline
description: Pilatus Curtain
deviceClass: debye_bec.devices.pilatus_curtain.PilatusCurtain
deviceConfig:
prefix: "X01DA-ES2-DET3:TRY-"
onFailure: retry
enabled: true
softwareTrigger: false
pilatus:
readoutPriority: baseline
description: Pilatus
deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus
deviceTags:
- detector
deviceConfig:
prefix: "X01DA-ES2-PIL:"
onFailure: retry
enabled: true
softwareTrigger: true
pilatus_smpl:
readoutPriority: baseline
description: Sample to pilatus distance
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-ES2-DET:SMPLDIST"
onFailure: retry
enabled: true
softwareTrigger: false
+72
View File
@@ -0,0 +1,72 @@
"""Frontend Absorber"""
from __future__ import annotations
import enum
from typing import TYPE_CHECKING
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO
from ophyd_devices import CompareStatus, DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
class AbsorberError(Exception):
"""Absorber specific exception"""
class STATUS(int, enum.Enum):
"""Absorber States"""
MOVING_CLOSE = 0
OPEN = 1
MOVING_OPEN = 2
CLOSED = 3
NOT_ENABLED = 4
TIMEOUT_CLOSE = 5
TIMEOUT_OPEN = 6
CLOSE_LS_LOST = 7
OPEN_LS_LOST = 8
CLOSE_LS_NOT_FREE = 9
OPEN_LS_NOT_FREE = 10
ERROR_LS = 11
TO_CONNECT = 12
MAN_OPEN = 13
UNDEFINED = 14
class Absorber(PSIDeviceBase):
"""Class for the Frontend Absorber"""
USER_ACCESS = ["open", "close"]
request = Cpt(EpicsSignal, suffix="REQUEST", kind="config", doc="Open/Close Absorber")
status = Cpt(EpicsSignalRO, suffix="STATUS", kind="normal", doc="Absorber Status")
status_string = Cpt(EpicsSignalRO, suffix="STATUS", kind="normal", string=True, doc="Absorber Status")
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.timeout_for_move = 10
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
def open(self) -> DeviceStatus | None:
"""Open the Absorber"""
if self.status.get() == STATUS.CLOSED:
self.request.put(1)
status_open = CompareStatus(self.status, STATUS.OPEN, timeout=self.timeout_for_move)
status = status_open
return status
else:
return None
def close(self) -> DeviceStatus | None:
"""Close the Absorber"""
if self.status.get() == STATUS.OPEN:
self.request.put(1)
status_close = CompareStatus(self.status, STATUS.CLOSED, timeout=self.timeout_for_move)
status = status_close
return status
else:
return None
+46
View File
@@ -0,0 +1,46 @@
"""Basler camera class for Debye BEC."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal
from ophyd_devices.devices.areadetector.cam import AravisDetectorCam
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from debye_bec.devices.cameras.debye_base_cam import DebyeBaseCamera
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
class BaslerCamBase(ADBase):
"""BaslerCam Base class."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(AravisDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")
class BaslerCam(DebyeBaseCamera, BaslerCamBase):
"""Basler camera class at Debye. IOC prefix: X01DA-ES-XRAYEYE:"""
preview = Cpt(
PreviewSignal,
name="preview",
ndim=2,
num_rotation_90=3,
doc="Preview signal for the camera.",
)
+138
View File
@@ -0,0 +1,138 @@
"""Base class for Camera integration at Debye."""
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import DeviceStatus, StatusBase
from ophyd_devices import PreviewSignal
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from typeguard import typechecked
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
logger = bec_logger.logger
class DebyeBaseCamera(PSIDeviceBase):
"""Base class for Debye cameras."""
USER_ACCESS = ["live_mode"]
preview = Cpt(
PreviewSignal,
name="preview",
ndim=2,
num_rotation_90=-1,
doc="Preview signal for the camera.",
)
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.image1: "ImagePlugin_V35"
self._update_frequency = 1 # Hz
self._live_mode = False
self._live_mode_event = None
self._task_status = None
@property
def live_mode(self) -> bool:
"""Live mode status."""
return self._live_mode
@typechecked
@live_mode.setter
def live_mode(self, value: bool) -> None:
"""
Set the live mode status.
Args:
value (bool): True to enable live mode, False to disable.
"""
if value == self._live_mode:
return
self._live_mode = value
if value:
self._start_live_mode()
else:
self._stop_live_mode()
def _start_live_mode(self) -> None:
"""Start live mode."""
if self._live_mode_event is not None: # Kill task if it exists
self._live_mode_event.set()
self._live_mode_event = None
if self._task_status is not None:
self.task_handler.kill_task(task_status=self._task_status)
self._task_status = None
self._live_mode_event = threading.Event()
self._task_status = self.task_handler.submit_task(task=self.emit_to_bec)
def _stop_live_mode(self) -> None:
"""Stop live mode."""
if self._live_mode_event is not None:
self._live_mode_event.set()
self._live_mode_event = None
def emit_to_bec(self):
"""Emit the image data to BEC. If _live_mode_event is set, stop the task."""
while not self._live_mode_event.wait(1 / self._update_frequency):
value = self.image1.array_data.get()
if value is None:
continue
width = self.image1.array_size.width.get()
height = self.image1.array_size.height.get()
# Geometry correction for the image
data = np.reshape(value, (height, width))
self.preview.put(data)
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
self.live_mode = True
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
"""
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device."""
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""Called right before the scan starts on all devices automatically."""
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""Called to inquire if a device has completed a scans."""
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
def on_stop(self) -> None:
"""Called when the device is stopped."""
+86
View File
@@ -0,0 +1,86 @@
"""EH Hutch Cameras"""
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import cv2
from bec_lib.file_utils import get_full_path
from bec_lib.logger import bec_logger
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from ophyd_devices import DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from debye_bec.devices.utils.utils import fetch_scan_info
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from bec_lib.messages import ScanStatusMessage
logger = bec_logger.logger
CAM_USERNAME = "camera_user"
CAM_PASSWORD = "camera_user1"
CAM_PORT = 554
class HutchCam(PSIDeviceBase):
"""Class for the Hutch Cameras"""
# image = Cpt(Signal, name='image', kind='config')
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, scan_info=scan_info, **kwargs)
self.scan_parameters: ScanServerScanInfo = None
self.hostname = prefix
self.status = None
# pylint: disable=E1101
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error(self, "Connection Failed", "Could not connect to the camera stream.")
return
cap.release()
def on_stage(self) -> DeviceStatus:
"""Called while staging the device."""
self.scan_parameters = fetch_scan_info(self.scan_info)
file_path = get_full_path(self.scan_info, name="hutch_cam_" + self.hostname).removesuffix(
"h5"
)
self.status = DeviceStatus(self)
thread = threading.Thread(
target=self._save_picture, args=(file_path, self.status), daemon=True
)
thread.start()
return self.status
def _save_picture(self, file_path, status):
try:
logger.info(f"Capture from camera {self.hostname}")
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error("Connection Failed", "Could not connect to the camera stream.")
return
logger.info(f"Connection to camera {self.hostname} established")
ret, frame = cap.readAsync()
cap.release()
if not ret:
logger.error("Capture Failed", "Failed to capture image from camera.")
return
cv2.imwrite(file_path + "png", frame)
status.set_finished()
logger.info(f"Capture from camera {self.hostname} done")
except Exception as e:
status.set_exception(e)
@@ -0,0 +1,49 @@
"""Prosilica camera class for integration of beam_monitor 1/2 cameras."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal
from ophyd_devices.devices.areadetector.cam import ProsilicaDetectorCam
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from debye_bec.devices.cameras.debye_base_cam import DebyeBaseCamera
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
class ProsilicaCamBase(ADBase):
"""Base class for Prosilica cameras."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(ProsilicaDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")
class ProsilicaCam(DebyeBaseCamera, ProsilicaCamBase):
"""
Prosilica camera class, for integration of beam_monitor 1/2 cameras.
Prefixes are: X01DA-OP-GIGE02: and X01DA-OP-GIGE01:
"""
preview = Cpt(
PreviewSignal,
name="preview",
ndim=2,
num_rotation_90=3,
doc="Preview signal for the camera.",
)
+25 -6
View File
@@ -3,9 +3,28 @@
### debye_bec
| Device | Documentation | Module |
| :----- | :------------- | :------ |
| Mo1Bragg | Class for the Mo1 Bragg positioner of the Debye beamline.<br> The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG: which is connected to<br> the NI motor controller via web sockets.<br> | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| Mo1BraggCrystal | Mo1 Bragg PVs to set the crystal parameters | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| Mo1BraggEncoder | Mo1 Bragg PVs to communicate with the encoder | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| Mo1BraggScanControl | Mo1 Bragg PVs to control the scan after setting the parameters. | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| Mo1BraggScanSettings | Mo1 Bragg PVs to set the scan setttings | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| Mo1BraggStatus | Mo1 Bragg PVs for status monitoring | [debye_bec.devices.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg.py) |
| BaslerCam | Basler camera class at Debye. IOC prefix: X01DA-ES-XRAYEYE: | [debye_bec.devices.cameras.basler_cam](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/cameras/basler_cam.py) |
| BaslerCamBase | BaslerCam Base class. | [debye_bec.devices.cameras.basler_cam](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/cameras/basler_cam.py) |
| DebyeBaseCamera | Base class for Debye cameras. | [debye_bec.devices.cameras.debye_base_cam](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/cameras/debye_base_cam.py) |
| ES0Filter | Class for the ES0 filter station X01DA-ES0-FI: | [debye_bec.devices.es0filter](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/es0filter.py) |
| GasMixSetup | Class for the ES2 Pilatus Curtain | [debye_bec.devices.pilatus_curtain](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/pilatus_curtain.py) |
| GasMixSetupControl | GasMixSetup Control for Inonization Chamber 0 | [debye_bec.devices.ionization_chambers.ionization_chamber](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/ionization_chambers/ionization_chamber.py) |
| HighVoltageSuppliesControl | HighVoltage Supplies Control for Ionization Chamber 0 | [debye_bec.devices.ionization_chambers.ionization_chamber](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/ionization_chambers/ionization_chamber.py) |
| IonizationChamber0 | Ionization Chamber 0, prefix should be 'X01DA-'. | [debye_bec.devices.ionization_chambers.ionization_chamber](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/ionization_chambers/ionization_chamber.py) |
| IonizationChamber1 | Ionization Chamber 1, prefix should be 'X01DA-'. | [debye_bec.devices.ionization_chambers.ionization_chamber](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/ionization_chambers/ionization_chamber.py) |
| IonizationChamber2 | Ionization Chamber 2, prefix should be 'X01DA-'. | [debye_bec.devices.ionization_chambers.ionization_chamber](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/ionization_chambers/ionization_chamber.py) |
| Mo1Bragg | Mo1 Bragg motor for the Debye beamline.<br><br> The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:<br> | [debye_bec.devices.mo1_bragg.mo1_bragg](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg.py) |
| Mo1BraggAngle | Positioner implementation with readback angle of the MO1 Bragg positioner. | [debye_bec.devices.mo1_bragg.mo1_bragg_angle](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_angle.py) |
| Mo1BraggCalculator | Mo1 Bragg PVs to convert angle to energy or vice-versa. | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggCrystal | Mo1 Bragg PVs to set the crystal parameters | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggEncoder | Mo1 Bragg PVs to communicate with the encoder | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggPositioner | <br> Positioner implementation with readback energy of the MO1 Bragg positioner.<br><br> The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:<br> This soft IOC connects to the NI motor and its control loop.<br> | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggScanControl | Mo1 Bragg PVs to control the scan after setting the parameters. | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggScanSettings | Mo1 Bragg PVs to set the scan setttings | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1BraggStatus | Mo1 Bragg PVs for status monitoring | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Mo1TriggerSettings | Mo1 Trigger settings | [debye_bec.devices.mo1_bragg.mo1_bragg_devices](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py) |
| Nidaq | NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05<br><br> Args:<br> prefix (str) : Prefix to the NIDAQ soft ioc, currently X01DA-PC-SCANSERVER:<br> name (str) : Name of the device<br> scan_info (ScanInfo) : ScanInfo object passed by BEC's devicemanager.<br> | [debye_bec.devices.nidaq.nidaq](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/nidaq/nidaq.py) |
| NidaqControl | Nidaq control class with all PVs | [debye_bec.devices.nidaq.nidaq](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/nidaq/nidaq.py) |
| ProsilicaCam | <br> Prosilica camera class, for integration of beam_monitor 1/2 cameras.<br> Prefixes are: X01DA-OP-GIGE02: and X01DA-OP-GIGE01:<br> | [debye_bec.devices.cameras.prosilica_cam](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/cameras/prosilica_cam.py) |
| ProsilicaCamBase | Base class for Prosilica cameras. | [debye_bec.devices.cameras.prosilica_cam](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/cameras/prosilica_cam.py) |
| Reffoilchanger | Class for the ES2 Reference Foil Changer | [debye_bec.devices.reffoilchanger](https://gitlab.psi.ch/bec/debye_bec/-/blob/main/debye_bec/devices/reffoilchanger.py) |
+54
View File
@@ -0,0 +1,54 @@
"""ES0 Filter Station"""
from typing import Literal
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, Kind
from ophyd_devices.utils import bec_utils
from typeguard import typechecked
class EpicsSignalWithRBVBit(EpicsSignal):
def __init__(self, prefix, *, bit: int, **kwargs):
super().__init__(prefix, **kwargs)
self.bit = bit
@typechecked
def put(self, value: Literal[0, 1], **kwargs):
bit_value = super().get()
# convert to int
bit_value = int(bit_value)
if value == 1:
new_value = bit_value | (1 << self.bit)
else:
new_value = bit_value & ~(1 << self.bit)
super().put(new_value, **kwargs)
def get(self, **kwargs) -> Literal[0, 1]:
bit_value = super().get()
# convert to int
bit_value = int(bit_value)
if (bit_value & (1 << self.bit)) == 0:
return 0
return 1
class ES0Filter(Device):
"""Class for the ES0 filter station X01DA-ES0-FI:"""
Mo400 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=1, kind="config", doc="Mo400 filter")
Mo300 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=2, kind="config", doc="Mo300 filter")
Mo200 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=3, kind="config", doc="Mo200 filter")
Zn500 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=4, kind="config", doc="Zn500 filter")
Zn250 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=5, kind="config", doc="Zn250 filter")
Zn125 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=6, kind="config", doc="Zn125 filter")
Zn50 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=7, kind="config", doc="Zn50 filter")
Zn25 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=8, kind="config", doc="Zn25 filter")
Al500 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=9, kind="config", doc="Al500 filter")
Al320 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=10, kind="config", doc="Al320 filter")
Al200 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=11, kind="config", doc="Al200 filter")
Al100 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=12, kind="config", doc="Al100 filter")
Al50 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=13, kind="config", doc="Al50 filter")
Al20 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=14, kind="config", doc="Al20 filter")
Al10 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=15, kind="config", doc="Al10 filter")
@@ -0,0 +1,434 @@
"""Ionization chamber device class"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd_devices import CompareStatus, DeviceStatus, SubscriptionStatus, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from typeguard import typechecked
from debye_bec.devices.ionization_chambers.ionization_chamber_enums import (
AmplifierEnable,
AmplifierFilter,
AmplifierGain,
)
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
class EpicsSignalSplit(EpicsSignal):
"""Wrapper around EpicsSignal with different read and write pv"""
def __init__(self, prefix, **kwargs):
super().__init__(prefix + "-RB", write_pv=prefix + "Set", **kwargs)
class GasMixSetupControl(Device):
"""GasMixSetup Control for Inonization Chamber 0"""
gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="omitted", doc="Gas 1 requirement")
conc1_req = Cpt(
EpicsSignalWithRBV, suffix="Conc1Req", kind="omitted", doc="Concentration 1 requirement"
)
gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="omitted", doc="Gas 2 requirement")
conc2_req = Cpt(
EpicsSignalWithRBV, suffix="Conc2Req", kind="omitted", doc="Concentration 2 requirement"
)
press_req = Cpt(
EpicsSignalWithRBV, suffix="PressReq", kind="omitted", doc="Pressure requirement"
)
fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber")
status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status")
gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1")
gas1_string = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1", string=True)
conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1")
gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2")
gas2_string = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2", string=True)
conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2")
press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure")
class HighVoltageSuppliesControl(Device):
"""HighVoltage Supplies Control for Ionization Chamber 0"""
hv_v = Cpt(EpicsSignalSplit, suffix="HV2-V", kind="config", doc="HV voltage")
hv_i = Cpt(EpicsSignalSplit, suffix="HV2-I", kind="config", doc="HV current")
grid_v = Cpt(EpicsSignalSplit, suffix="HV1-V", kind="config", doc="Grid voltage")
grid_i = Cpt(EpicsSignalSplit, suffix="HV1-I", kind="config", doc="Grid current")
class IonizationChamber0(PSIDeviceBase):
"""Ionization Chamber 0, prefix should be 'X01DA-'."""
USER_ACCESS = ["set_gain", "set_filter", "set_hv", "set_grid", "fill"]
num = 1
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES1-IC0:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES1-IC0:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
self.timeout_for_pvwait = 2.5
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
@typechecked
def set_gain(self, gain: Literal["1e6", "1e7", "5e7", "1e8", "1e9"]) -> None:
"""Configure the gain setting of the specified channel
Args:
gain (Literal['1e6', '1e7', '5e7', '1e8', '1e9']) : Desired gain
"""
if self.amp.cOnOff.get() == AmplifierEnable.OFF:
status = CompareStatus(self.amp.cOnOff, AmplifierEnable.ON)
self.cancel_on_stop(status)
self.amp.cOnOff.put(AmplifierEnable.ON)
status.wait(self.timeout_for_pvwait)
match gain:
case "1e6":
self.amp.cGain_ENUM.put(AmplifierGain.G1E6)
case "1e7":
self.amp.cGain_ENUM.put(AmplifierGain.G1E7)
case "5e7":
self.amp.cGain_ENUM.put(AmplifierGain.G5E7)
case "1e8":
self.amp.cGain_ENUM.put(AmplifierGain.G1E8)
case "1e9":
self.amp.cGain_ENUM.put(AmplifierGain.G1E9)
def set_filter(
self, value: Literal["1us", "3us", "10us", "30us", "100us", "300us", "1ms", "3ms"]
) -> None:
"""Configure the filter setting of the specified channel
Args:
value (Literal['1us','3us','10us','30us','100us','300us','1ms','3ms']) :Desired filter
"""
if self.amp.cOnOff.get() == AmplifierEnable.OFF:
status = CompareStatus(self.amp.cOnOff, AmplifierEnable.ON)
self.cancel_on_stop(status)
self.amp.cOnOff.put(AmplifierEnable.ON)
status.wait(self.timeout_for_pvwait)
match value:
case "1us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F1US)
case "3us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F3US)
case "10us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F10US)
case "30us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F30US)
case "100us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F100US)
case "300us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F300US)
case "1ms":
self.amp.cFilter_ENUM.put(AmplifierFilter.F1MS)
case "3ms":
self.amp.cFilter_ENUM.put(AmplifierFilter.F3MS)
@typechecked
def set_hv(self, hv: float) -> None:
"""Configure the high voltage settings , this will
enable the high voltage (if external enable is active)!
Args:
hv (float) : Desired voltage for the 'HV' terminal. Voltage has to be between 0...3000
"""
if not 0 <= hv <= 3000:
raise ValueError(f"specified HV {hv} not within range [0 .. 3000]")
if not np.isclose(np.abs(hv - self.hv.grid_v.get()), 0, atol=3):
raise ValueError(f"Grid {self.hv.grid_v.get()} must not be higher than HV {hv}!")
if not self.hv_en.ena.get() == 1:
status = CompareStatus(self.hv_en.ena, 1)
self.cancel_on_stop(status)
self.hv_en.ena.put(1)
status.wait(self.timeout_for_pvwait)
# Set current fixed to 3 mA (max)
self.hv.hv_i.put(3)
self.hv.hv_v.put(hv)
@typechecked
def set_grid(self, grid: float) -> None:
"""Configure the high voltage settings , this will
enable the high voltage (if external enable is active)!
Args:
grid (float) : Desired voltage for the 'Grid' terminal,
Grid Voltage has to be between 0...3000
"""
if not 0 <= grid <= 3000:
raise ValueError(f"specified Grid {grid} not within range [0 .. 3000]")
if not np.isclose(np.abs(grid - self.hv.hv_v.get()), 0, atol=3):
raise ValueError(f"Grid {grid} must not be higher than HV {self.hv.hv_v.get()}!")
if not self.hv_en.ena.get() == 1:
status = CompareStatus(self.hv_en.ena, 1)
self.cancel_on_stop(status)
self.hv_en.ena.put(1)
status.wait(self.timeout_for_pvwait)
# Set current fixed to 3 mA (max)
self.hv.grid_i.put(3)
self.hv.grid_v.put(grid)
@typechecked
def fill(
self,
gas1: Literal["He", "N2", "Ar", "Kr"],
conc1: float,
gas2: Literal["He", "N2", "Ar", "Kr"],
conc2: float,
pressure: float,
*,
wait: bool = False,
) -> DeviceStatus | None:
"""Fill an ionization chamber with the specified gas mixture.
Args:
gas1 (Literal['He', 'N2', 'Ar', 'Kr']) : Gas 1 requirement,
conc1 (float) : Concentration 1 requirement in %,
gas2 (Literal['He', 'N2', 'Ar', 'Kr']) : Gas 2 requirement,
conc2 (float) : Concentration 2 requirement in %,
pressure (float) : Required pressure in bar abs,
wait (bool): If you like to wait for the filling to finish.
"""
if not 0 <= conc1 <= 100:
raise ValueError(f"Concentration 1 {conc1} out of range [0 .. 100 %]")
if not 0 <= conc2 <= 100:
raise ValueError(f"Concentration 2 {conc2} out of range [0 .. 100 %]")
if not np.isclose((conc1 + conc2), 100, atol=0.1):
raise ValueError(f"Conc1 {conc1} and conc2 {conc2} must sum to 100 +- 0.1")
if not 0 <= pressure <= 3:
raise ValueError(f"Pressure {pressure} out of range [0 .. 3 bar abs]")
self.gmes.gas1_req.set(gas1).wait(timeout=3)
self.gmes.conc1_req.set(conc1).wait(timeout=3)
self.gmes.gas2_req.set(gas2).wait(timeout=3)
self.gmes.conc2_req.set(conc2).wait(timeout=3)
status = TransitionStatus(self.gmes.status.get(), [0, 1])
self.cancel_on_stop(status)
self.gmes.fill.put(1)
if wait:
status.wait(timeout=360)
else:
return status
class IonizationChamber1(IonizationChamber0):
"""Ionization Chamber 1, prefix should be 'X01DA-'."""
num = 2
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES2-IC12:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
class IonizationChamber2(IonizationChamber0):
"""Ionization Chamber 2, prefix should be 'X01DA-'."""
num = 3
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES2-IC12:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
class Pips(IonizationChamber0):
"""Pips, prefix should be 'X01DA-'."""
USER_ACCESS = ["set_gain", "set_filter"]
num = 4
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = None
gmes_status_msg = None
hv = None
hv_en_signals = None
hv_en = None
@typechecked
def set_hv(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def set_grid(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def fill(self, *_) -> None:
"""Not available for the PIPS"""
return None
@@ -0,0 +1,32 @@
import enum
class AmplifierEnable(int, enum.Enum):
"""Enum class for the enable signal of the channel"""
OFF = 0
STARTUP = 1
ON = 2
class AmplifierGain(int, enum.Enum):
"""Enum class for the gain of the channel"""
G1E6 = 0
G1E7 = 1
G5E7 = 2
G1E8 = 3
G1E9 = 4
class AmplifierFilter(int, enum.Enum):
"""Enum class for the filter of the channel"""
F1US = 0
F3US = 1
F10US = 2
F30US = 3
F100US = 4
F300US = 5
F1MS = 6
F3MS = 7
-891
View File
@@ -1,891 +0,0 @@
""" Module for the Mo1 Bragg positioner of the Debye beamline.
The soft IOC is reachable via the EPICS prefix X01DA-OP-MO1:BRAGG: and connected
to a motor controller via web sockets. The Mo1 Bragg positioner is not only a
positioner, but also a scan controller to setup XAS and XRD scans. A few scan modes
are programmed in the controller, e.g. simple and advanced XAS scans + XRD triggering mode.
Note: For some of the Epics PVs, in particular action buttons, the suffix .PROC and
put_complete=True is used to ensure that the action is executed completely. This is believed
to allow for a more stable execution of the action."""
import enum
import threading
import time
import traceback
from dataclasses import dataclass
from typing import Literal
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import (
Device,
DeviceStatus,
EpicsSignal,
EpicsSignalRO,
EpicsSignalWithRBV,
Kind,
PositionerBase,
Signal,
Staged,
)
from ophyd.utils import LimitError
from ophyd_devices.utils import bec_scaninfo_mixin, bec_utils
from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError
logger = bec_logger.logger
class ScanControlScanStatus(int, enum.Enum):
"""Enum class for the scan status of the Bragg positioner"""
PARAMETER_WRONG = 0
VALIDATION_PENDING = 1
READY = 2
RUNNING = 3
class ScanControlLoadMessage(int, enum.Enum):
"""Enum for validating messages for load message of the Bragg positioner"""
PENDING = 0
STARTED = 1
SUCCESS = 2
ERR_XRD_MEAS_LEN_LOW = 3
ERR_XRD_N_TRIGGERS_LOW = 4
ERR_XRD_TRIGS_EVERY_N_LOW = 5
ERR_XRD_MEAS_LEN_HI = 6
ERR_XRD_N_TRIGGERS_HI = 7
ERR_XRD_TRIGS_EVERY_N_HI = 8
ERR_SCAN_HI_ANGLE_LIMIT = 9
ERR_SCAN_LOW_ANGLE_LIMITS = 10
ERR_SCAN_TIME = 11
ERR_SCAN_VEL_TOO_HI = 12
ERR_SCAN_ANGLE_OUT_OF_LIM = 13
ERR_SCAN_HIGH_VEL_LAR_42 = 14
ERR_SCAN_MODE_INVALID = 15
class Mo1BraggError(Exception):
"""Mo1Bragg specific exception"""
class MoveType(str, enum.Enum):
"""Enum class to switch between move types energy and angle for the Bragg positioner"""
ENERGY = "energy"
ANGLE = "angle"
class ScanControlMode(int, enum.Enum):
"""Enum class for the scan control mode of the Bragg positioner"""
SIMPLE = 0
ADVANCED = 1
class MoveTypeSignal(Signal):
"""Custom Signal to set the move type of the Bragg positioner"""
# pylint: disable=arguments-differ
def set(self, value: str | MoveType) -> None:
"""Returns currently active move method
Args:
value (str | MoveType) : Can be either 'energy' or 'angle'
"""
value = MoveType(value.lower())
self._readback = value.value
class Mo1BraggStatus(Device):
"""Mo1 Bragg PVs for status monitoring"""
error_status = Cpt(EpicsSignalRO, suffix="error_status_RBV", kind="config", auto_monitor=True)
brake_enabled = Cpt(EpicsSignalRO, suffix="brake_enabled_RBV", kind="config", auto_monitor=True)
mot_commutated = Cpt(
EpicsSignalRO, suffix="mot_commutated_RBV", kind="config", auto_monitor=True
)
axis_enabled = Cpt(EpicsSignalRO, suffix="axis_enabled_RBV", kind="config", auto_monitor=True)
enc_initialized = Cpt(
EpicsSignalRO, suffix="enc_initialized_RBV", kind="config", auto_monitor=True
)
heartbeat = Cpt(EpicsSignalRO, suffix="heartbeat_RBV", kind="config", auto_monitor=True)
class Mo1BraggEncoder(Device):
"""Mo1 Bragg PVs to communicate with the encoder"""
enc_reinit = Cpt(EpicsSignal, suffix="enc_reinit.PROC", kind="config")
enc_reinit_done = Cpt(EpicsSignalRO, suffix="enc_reinit_done_RBV", kind="config")
class Mo1BraggCrystal(Device):
"""Mo1 Bragg PVs to set the crystal parameters"""
offset_si111 = Cpt(EpicsSignalWithRBV, suffix="offset_si111", kind="config")
offset_si311 = Cpt(EpicsSignalWithRBV, suffix="offset_si311", kind="config")
xtal_enum = Cpt(EpicsSignalWithRBV, suffix="xtal_ENUM", kind="config")
d_spacing_si111 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si111", kind="config")
d_spacing_si311 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si311", kind="config")
set_offset = Cpt(EpicsSignal, suffix="set_offset.PROC", kind="config", put_complete=True)
current_xtal = Cpt(
EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True
)
class Mo1BraggScanSettings(Device):
"""Mo1 Bragg PVs to set the scan setttings"""
# XRD settings
xrd_select_ref_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_select_ref_ENUM", kind="config")
xrd_enable_hi_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_enable_hi_ENUM", kind="config")
xrd_time_hi = Cpt(EpicsSignalWithRBV, suffix="xrd_time_hi", kind="config")
xrd_n_trigger_hi = Cpt(EpicsSignalWithRBV, suffix="xrd_n_trigger_hi", kind="config")
xrd_every_n_hi = Cpt(EpicsSignalWithRBV, suffix="xrd_every_n_hi", kind="config")
xrd_enable_lo_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_enable_lo_ENUM", kind="config")
xrd_time_lo = Cpt(EpicsSignalWithRBV, suffix="xrd_time_lo", kind="config")
xrd_n_trigger_lo = Cpt(EpicsSignalWithRBV, suffix="xrd_n_trigger_lo", kind="config")
xrd_every_n_lo = Cpt(EpicsSignalWithRBV, suffix="xrd_every_n_lo", kind="config")
# XAS simple scan settings
s_scan_angle_hi = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_hi", kind="config")
s_scan_angle_lo = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_lo", kind="config")
s_scan_energy_lo = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_lo", kind="config", auto_monitor=True
)
s_scan_energy_hi = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_hi", kind="config", auto_monitor=True
)
s_scan_scantime = Cpt(
EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True
)
# XAS advanced scan settings
a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=True)
a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=True)
a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=True)
class Mo1BraggScanControl(Device):
"""Mo1 Bragg PVs to control the scan after setting the parameters."""
scan_mode_enum = Cpt(EpicsSignalWithRBV, suffix="scan_mode_ENUM", kind="config")
scan_duration = Cpt(
EpicsSignalWithRBV, suffix="scan_duration", kind="config", auto_monitor=True
)
scan_load = Cpt(EpicsSignal, suffix="scan_load.PROC", kind="config", put_complete=True)
scan_msg = Cpt(EpicsSignalRO, suffix="scan_msg_ENUM_RBV", kind="config", auto_monitor=True)
scan_start_infinite = Cpt(
EpicsSignal, suffix="scan_start_infinite.PROC", kind="config", put_complete=True
)
scan_start_timer = Cpt(
EpicsSignal, suffix="scan_start_timer.PROC", kind="config", put_complete=True
)
scan_stop = Cpt(EpicsSignal, suffix="scan_stop.PROC", kind="config", put_complete=True)
scan_status = Cpt(
EpicsSignalRO, suffix="scan_status_ENUM_RBV", kind="config", auto_monitor=True
)
scan_time_left = Cpt(
EpicsSignalRO, suffix="scan_time_left_RBV", kind="config", auto_monitor=True
)
scan_done = Cpt(EpicsSignalRO, suffix="scan_done_RBV", kind="config", auto_monitor=True)
scan_val_reset = Cpt(
EpicsSignal, suffix="scan_val_reset.PROC", kind="config", put_complete=True
)
scan_progress = Cpt(EpicsSignalRO, suffix="scan_progress_RBV", kind="config", auto_monitor=True)
scan_spectra_done = Cpt(
EpicsSignalRO, suffix="scan_n_osc_RBV", kind="config", auto_monitor=True
)
scan_spectra_left = Cpt(
EpicsSignalRO, suffix="scan_n_osc_left_RBV", kind="config", auto_monitor=True
)
@dataclass
class ScanParameter:
"""Dataclass to store the scan parameters for the Mo1 Bragg positioner.
This needs to be in sync with the kwargs of the MO1 Bragg scans from Debye, to
ensure that the scan parameters are correctly set. Any changes in the scan kwargs,
i.e. renaming or adding new parameters, need to be represented here as well."""
scan_time: float = None
scan_duration: float = None
xrd_enable_low: bool = None
xrd_enable_high: bool = None
num_trigger_low: int = None
num_trigger_high: int = None
exp_time_low: float = None
exp_time_high: float = None
cycle_low: int = None
cycle_high: int = None
start: float = None
stop: float = None
class Mo1Bragg(Device, PositionerBase):
"""Class for the Mo1 Bragg positioner of the Debye beamline.
The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG: which is connected to
the NI motor controller via web sockets.
"""
USER_ACCESS = ["set_xtal", "stop_scan"]
crystal = Cpt(Mo1BraggCrystal, "")
encoder = Cpt(Mo1BraggEncoder, "")
scan_settings = Cpt(Mo1BraggScanSettings, "")
scan_control = Cpt(Mo1BraggScanControl, "")
status = Cpt(Mo1BraggStatus, "")
# signal to indicate the move type 'energy' or 'angle'
move_type = Cpt(MoveTypeSignal, value=MoveType.ENERGY, kind="config")
# Energy PVs
readback = Cpt(
EpicsSignalRO, suffix="feedback_pos_energy_RBV", kind="hinted", auto_monitor=True
)
setpoint = Cpt(
EpicsSignalWithRBV, suffix="set_abs_pos_energy", kind="normal", auto_monitor=True
)
motor_is_moving = Cpt(
EpicsSignalRO, suffix="move_abs_done_RBV", kind="normal", auto_monitor=True
)
low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_energy_RBV", kind="config", auto_monitor=True)
high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_energy_RBV", kind="config", auto_monitor=True)
velocity = Cpt(EpicsSignalWithRBV, suffix="move_velocity", kind="config", auto_monitor=True)
# Angle PVs
# TODO makd angle motion a pseudo motor
feedback_pos_angle = Cpt(
EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True
)
setpoint_abs_angle = Cpt(
EpicsSignalWithRBV, suffix="set_abs_pos_angle", kind="normal", auto_monitor=True
)
low_limit_angle = Cpt(
EpicsSignalRO, suffix="lo_lim_pos_angle_RBV", kind="config", auto_monitor=True
)
high_limit_angle = Cpt(
EpicsSignalRO, suffix="hi_lim_pos_angle_RBV", kind="config", auto_monitor=True
)
# Execute motion
move_abs = Cpt(EpicsSignal, suffix="move_abs.PROC", kind="config", put_complete=True)
move_stop = Cpt(EpicsSignal, suffix="move_stop.PROC", kind="config", put_complete=True)
SUB_READBACK = "readback"
_default_sub = SUB_READBACK
SUB_PROGRESS = "progress"
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
):
"""Initialize the Mo1 Bragg positioner.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kind (Kind): Kind of the device
device_manager (DeviceManager): Device manager instance
parent (Device): Parent device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs)
self._stopped = False
self.device_manager = device_manager
self._move_thread = None
self.service_cfg = None
self.scaninfo = None
# Init scan parameters
self.scan_parameter = ScanParameter()
self.timeout_for_pvwait = 2.5
self.readback.name = self.name
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
if device_manager:
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
self.connector = self.device_manager.connector
self._update_scaninfo()
self._on_init()
def _on_init(self):
"""Action to be executed on initialization of the device"""
self.scan_control.scan_progress.subscribe(self._progress_update, run=False)
def _progress_update(self, value, **kwargs) -> None:
"""Callback method to update the scan progress, runs a callback to SUB_PROGRESS subscribers, i.e. BEC.
Args:
value (int) : current progress value
"""
max_value = 100
self._run_subs(
sub_type=self.SUB_PROGRESS,
value=value,
max_value=max_value,
done=bool(max_value == value),
)
def _update_scaninfo(self) -> None:
"""Connect to the ScanInfo mixin"""
self.scaninfo = bec_scaninfo_mixin.BecScaninfoMixin(self.device_manager)
self.scaninfo.load_scan_metadata()
@property
def stopped(self) -> bool:
"""Return the stopped flag. If True, the motion is stopped."""
return self._stopped
def stop(self, *, success=False) -> None:
"""Stop any motion on the positioner
Args:
success (bool) : Flag to indicate if the motion was successful
"""
self.move_stop.put(1)
self._stopped = True
if self._move_thread is not None:
self._move_thread.join()
self._move_thread = None
super().stop(success=success)
def stop_scan(self) -> None:
"""Stop the currently running scan gracefully, this finishes the running oscillation."""
self.scan_control.scan_stop.put(1)
# -------------- Positioner specific methods -----------------#
@property
def limits(self) -> tuple:
"""Return limits of the Bragg positioner"""
if self.move_type.get() == MoveType.ENERGY:
return (self.low_lim.get(), self.high_lim.get())
return (self.low_limit_angle.get(), self.high_limit_angle.get())
@property
def low_limit(self) -> float:
"""Return low limit of axis"""
return self.limits[0]
@property
def high_limit(self) -> float:
"""Return high limit of axis"""
return self.limits[1]
@property
def egu(self) -> str:
"""Return the engineering units of the positioner"""
if self.move_type.get() == MoveType.ENERGY:
return "eV"
return "deg"
@property
def position(self) -> float:
"""Return the current position of Mo1Bragg, considering the move type"""
move_type = self.move_type.get()
move_cpt = self.readback if move_type == MoveType.ENERGY else self.feedback_pos_angle
return move_cpt.get()
# pylint: disable=arguments-differ
def check_value(self, value: float) -> None:
"""Method to check if a value is within limits of the positioner. Called by PositionerBase.move()
Args:
value (float) : value to move axis to.
"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not low_limit <= value <= high_limit:
raise LimitError(f"position={value} not within limits {self.limits}")
def _move_and_finish(
self,
target_pos: float,
move_cpt: Cpt,
read_cpt: Cpt,
status: DeviceStatus,
update_frequency: float = 0.1,
) -> None:
"""Method to be called in the move thread to move the Bragg positioner to the target position.
Args:
target_pos (float) : target position for the motion
move_cpt (Cpt) : component to set the target position on the IOC,
either setpoint or setpoint_abs_angle depending on the move type
read_cpt (Cpt) : component to read the current position of the motion, readback or feedback_pos_angle
status (DeviceStatus) : status object to set the status of the motion
update_frequency (float): Optional, frequency to update the current position of the motion, defaults to 0.1s
"""
try:
# Set the target position on IOC
move_cpt.put(target_pos)
self.move_abs.put(1)
# Currently sleep is needed due to delay in updates on PVs, maybe time can be reduced
time.sleep(0.5)
while self.motor_is_moving.get() == 0:
if self.stopped:
raise DeviceStopError(f"Device {self.name} was stopped")
time.sleep(update_frequency)
# pylint: disable=protected-access
status.set_finished()
# pylint: disable=broad-except
except Exception as exc:
content = traceback.format_exc()
logger.error(f"Error in move thread of device {self.name}: {content}")
status.set_exception(exc=exc)
def move(self, value: float, move_type: str | MoveType = None, **kwargs) -> DeviceStatus:
"""Move the Bragg positioner to the specified value, allows to switch between move types angle and energy.
Args:
value (float) : target value for the motion
move_type (str | MoveType) : Optional, specify the type of move, either 'energy' or 'angle'
Returns:
DeviceStatus : status object to track the motion
"""
self._stopped = False
if move_type is not None:
self.move_type.put(move_type)
move_type = self.move_type.get()
move_cpt = self.setpoint if move_type == MoveType.ENERGY else self.setpoint_abs_angle
read_cpt = self.readback if move_type == MoveType.ENERGY else self.feedback_pos_angle
self.check_value(value)
status = DeviceStatus(device=self)
self._move_thread = threading.Thread(
target=self._move_and_finish, args=(value, move_cpt, read_cpt, status, 0.1)
)
self._move_thread.start()
return status
# -------------- End of Positioner specific methods -----------------#
# -------------- MO1 Bragg specific methods -----------------#
def set_xtal(
self,
xtal_enum: Literal["111", "311"],
offset_si111: float = None,
offset_si311: float = None,
d_spacing_si111: float = None,
d_spacing_si311: float = None,
) -> None:
"""Method to set the crystal parameters of the Bragg positioner
Args:
xtal_enum (Literal["111", "311"]) : Enum to set the crystal orientation
offset_si111 (float) : Offset for the 111 crystal
offset_si311 (float) : Offset for the 311 crystal
d_spacing_si111 (float) : d-spacing for the 111 crystal
d_spacing_si311 (float) : d-spacing for the 311 crystal
"""
if offset_si111 is not None:
self.crystal.offset_si111.put(offset_si111)
if offset_si311 is not None:
self.crystal.offset_si311.put(offset_si311)
if d_spacing_si111 is not None:
self.crystal.d_spacing_si111.put(d_spacing_si111)
if d_spacing_si311 is not None:
self.crystal.d_spacing_si311.put(d_spacing_si311)
if xtal_enum == "111":
crystal_set = 0
elif xtal_enum == "311":
crystal_set = 1
else:
raise ValueError(
f"Invalid argument for xtal_enum : {xtal_enum}, choose from '111' or '311'"
)
self.crystal.xtal_enum.put(crystal_set)
self.crystal.set_offset.put(1)
def set_xas_settings(self, low: float, high: float, scan_time: float) -> None:
"""Set XAS parameters for upcoming scan.
Args:
low (float): Low energy/angle value of the scan
high (float): High energy/angle value of the scan
scan_time (float): Time for a half oscillation
"""
move_type = self.move_type.get()
if move_type == MoveType.ENERGY:
self.scan_settings.s_scan_energy_lo.put(low)
self.scan_settings.s_scan_energy_hi.put(high)
else:
self.scan_settings.s_scan_angle_lo.put(low)
self.scan_settings.s_scan_angle_hi.put(high)
self.scan_settings.s_scan_scantime.put(scan_time)
def set_xrd_settings(
self,
enable_low: bool,
enable_high: bool,
num_trigger_low: int,
num_trigger_high: int,
exp_time_low: int,
exp_time_high: int,
cycle_low: int,
cycle_high: int,
) -> None:
"""Set XRD settings for the upcoming scan.
Args:
enable_low (bool): Enable XRD for low energy/angle
enable_high (bool): Enable XRD for high energy/angle
num_trigger_low (int): Number of triggers for low energy/angle
num_trigger_high (int): Number of triggers for high energy/angle
exp_time_low (int): Exposure time for low energy/angle
exp_time_high (int): Exposure time for high energy/angle
cycle_low (int): Cycle for low energy/angle
cycle_high (int): Cycle for high energy/angle
"""
self.scan_settings.xrd_enable_hi_enum.put(int(enable_high))
self.scan_settings.xrd_enable_lo_enum.put(int(enable_low))
self.scan_settings.xrd_n_trigger_hi.put(num_trigger_high)
self.scan_settings.xrd_n_trigger_lo.put(num_trigger_low)
self.scan_settings.xrd_time_hi.put(exp_time_high)
self.scan_settings.xrd_time_lo.put(exp_time_low)
self.scan_settings.xrd_every_n_hi.put(cycle_high)
self.scan_settings.xrd_every_n_lo.put(cycle_low)
def set_scan_control_settings(self, mode: ScanControlMode, scan_duration: float) -> None:
"""Set the scan control settings for the upcoming scan.
Args:
mode (ScanControlMode): Mode for the scan, either simple or advanced
scan_duration (float): Duration of the scan
"""
val = ScanControlMode(mode).value
self.scan_control.scan_mode_enum.put(val)
self.scan_control.scan_duration.put(scan_duration)
def _update_scan_parameter(self):
"""Get the scaninfo parameters for the scan."""
for key, value in self.scaninfo.scan_msg.content["info"]["kwargs"].items():
if hasattr(self.scan_parameter, key):
setattr(self.scan_parameter, key, value)
# -------------- End MO1 Bragg specific methods -----------------#
# -------------- Flyer Interface methods -----------------#
def kickoff(self):
"""Kickoff the device, called from BEC."""
scan_duration = self.scan_control.scan_duration.get()
# TODO implement better logic for infinite scans, at least bring it up with Debye
start_func = (
self.scan_control.scan_start_infinite.put
if scan_duration < 0.1
else self.scan_control.scan_start_timer.put
)
start_func(1)
status = self.wait_with_status(
signal_conditions=[(self.scan_control.scan_status.get, ScanControlScanStatus.RUNNING)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
)
return status
def stage(self) -> list[object]:
"""
Stage the device in preparation for a scan.
Returns:
list(object): list of objects that were staged
"""
if self._staged != Staged.no:
return super().stage()
self._stopped = False
self.scaninfo.load_scan_metadata()
self.on_stage()
return super().stage()
def _check_scan_msg(self, target_state: ScanControlLoadMessage) -> None:
"""Check if the scan message is gettting available
Args:
target_state (ScanControlLoadMessage): Target state to check for
Raises:
TimeoutError: If the scan message is not available after the timeout
"""
state = self.scan_control.scan_msg.get()
if state != target_state:
logger.warning(
f"Resetting scan validation in stage for state: {ScanControlLoadMessage(state)}, "
f"retry .get() on scan_control: {ScanControlLoadMessage(self.scan_control.scan_msg.get())} and sleeping 1s"
)
self.scan_control.scan_val_reset.put(1)
# Sleep to ensure the reset is done
time.sleep(1)
if not self.wait_for_signals(
signal_conditions=[(self.scan_control.scan_msg.get, target_state)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
):
raise TimeoutError(
f"Timeout after {self.timeout_for_pvwait} while waiting for scan status,"
f" current state: {ScanControlScanStatus(self.scan_control.scan_msg.get())}"
)
def on_stage(self) -> None:
"""Actions to be executed when the device is staged."""
if not self.scaninfo.scan_type == "fly":
return
self._check_scan_msg(ScanControlLoadMessage.PENDING)
scan_name = self.scaninfo.scan_msg.content["info"].get("scan_name", "")
self._update_scan_parameter()
if scan_name == "xas_simple_scan":
self.set_xas_settings(
low=self.scan_parameter.start,
high=self.scan_parameter.stop,
scan_time=self.scan_parameter.scan_time,
)
self.set_xrd_settings(
enable_low=False,
enable_high=False,
num_trigger_low=0,
num_trigger_high=0,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration
)
elif scan_name == "xas_simple_scan_with_xrd":
self.set_xas_settings(
low=self.scan_parameter.start,
high=self.scan_parameter.stop,
scan_time=self.scan_parameter.scan_time,
)
self.set_xrd_settings(
enable_low=self.scan_parameter.xrd_enable_low,
enable_high=self.scan_parameter.xrd_enable_high,
num_trigger_low=self.scan_parameter.num_trigger_low,
num_trigger_high=self.scan_parameter.num_trigger_high,
exp_time_low=self.scan_parameter.exp_time_low,
exp_time_high=self.scan_parameter.exp_time_high,
cycle_low=self.scan_parameter.cycle_low,
cycle_high=self.scan_parameter.cycle_high,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration
)
else:
raise Mo1BraggError(
f"Scan mode {scan_name} not implemented for scan_type={self.scaninfo.scan_type} on device {self.name}"
)
# Load the scan parameters to the controller
self.scan_control.scan_load.put(1)
# Wait for params to be checked from controller
if not self.wait_for_signals(
signal_conditions=[(self.scan_control.scan_msg.get, ScanControlLoadMessage.SUCCESS)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
):
raise TimeoutError(
f"Scan parameter validation run into timeout after {self.timeout_for_pvwait} with {ScanControlLoadMessage(self.scan_control.scan_msg.get())}"
)
def complete(self) -> DeviceStatus:
"""Complete the acquisition.
The method returns a DeviceStatus object that resolves to set_finished or set_exception once the acquisition is completed.
"""
status = self.on_complete()
if isinstance(status, DeviceStatus):
return status
status = DeviceStatus(self)
status.set_finished()
return status
def on_complete(self) -> DeviceStatus:
"""Specify actions to be performed for the completion of the acquisition."""
status = self.wait_with_status(
signal_conditions=[(self.scan_control.scan_done.get, 1)],
timeout=None,
check_stopped=True,
)
return status
def unstage(self) -> list[object]:
"""
Unstage device after a scan. It has to be possible to call this multiple times.
Returns:
list(object): list of objects that were unstaged
"""
self.check_scan_id()
self._stopped = False
self.on_unstage()
return super().unstage()
def on_unstage(self) -> None:
"""Actions to be executed when the device is unstaged.
The checks here ensure that the controller resets the Scan_msg to PENDING state."""
if self.wait_for_signals(
signal_conditions=[(self.scan_control.scan_msg.get, ScanControlLoadMessage.PENDING)],
timeout=self.timeout_for_pvwait,
check_stopped=False,
):
return
self.scan_control.scan_val_reset.put(1)
if not self.wait_for_signals(
signal_conditions=[(self.scan_control.scan_msg.get, ScanControlLoadMessage.PENDING)],
timeout=self.timeout_for_pvwait,
check_stopped=False,
):
raise TimeoutError(
f"Timeout after {self.timeout_for_pvwait} while waiting for scan validation"
)
# -------------- End Flyer Interface methods -----------------#
# -------------- Utility methods -----------------#
def check_scan_id(self) -> None:
"""Checks if scan_id has changed and set stopped flagged to True if it has."""
old_scan_id = self.scaninfo.scan_id
self.scaninfo.load_scan_metadata()
if self.scaninfo.scan_id != old_scan_id:
self._stopped = True
def wait_for_signals(
self,
signal_conditions: list[tuple],
timeout: float | None = None,
check_stopped: bool = False,
interval: float = 0.05,
all_signals: bool = False,
) -> bool:
"""Wrapper around a list of conditions that allows waiting for them to become True.
For EPICs PVs, an example usage is pasted at the bottom.
Args:
signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check
timeout (float): timeout in seconds
check_stopped (bool): True if stopped flag should be checked. The function relies on the self.stopped property to be set
interval (float): interval in seconds
all_signals (bool): True if all signals should be True, False if any signal should be True
Returns:
bool: True if all signals are in the desired state, False if timeout is reached
>>> Example usage for EPICS PVs:
>>> self.wait_for_signals(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True)
"""
timer = 0
while True:
checks = [
get_current_state() == condition
for get_current_state, condition in signal_conditions
]
if check_stopped is True and self.stopped is True:
return False
if (all_signals and all(checks)) or (not all_signals and any(checks)):
return True
if timeout and timer > timeout:
return False
time.sleep(interval)
timer += interval
def wait_with_status(
self,
signal_conditions: list[tuple],
timeout: float | None = None,
check_stopped: bool = False,
interval: float = 0.05,
all_signals: bool = False,
exception_on_timeout: Exception = None,
) -> DeviceStatus:
"""Wrapper around wait_for_signals to be started in thread and attach a DeviceStatus object.
This allows BEC to perform actinos in parallel and not be blocked by method calls on a device.
Typically used for on_trigger, on_complete methods or also the kickoff.
Args:
signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check
timeout (float): timeout in seconds
check_stopped (bool): True if stopped flag should be checked
interval (float): interval in seconds
all_signals (bool): True if all signals should be True, False if any signal should be True
exception_on_timeout (Exception): Exception to raise on timeout
Returns:
DeviceStatus: DeviceStatus object that resolves either to set_finished or set_exception
"""
if exception_on_timeout is None:
exception_on_timeout = DeviceTimeoutError(
f"Timeout error for {self.name} while waiting for signals {signal_conditions}"
)
status = DeviceStatus(device=self)
def wait_for_signals_wrapper(
status: DeviceStatus,
signal_conditions: list[tuple],
timeout: float,
check_stopped: bool,
interval: float,
all_signals: bool,
exception_on_timeout: Exception = None,
):
"""Convenient wrapper around wait_for_signals to set status based on the result.
Args:
status (DeviceStatus): DeviceStatus object to be set
signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check
timeout (float): timeout in seconds
check_stopped (bool): True if stopped flag should be checked
interval (float): interval in seconds
all_signals (bool): True if all signals should be True, False if any signal should be True
exception_on_timeout (Exception): Exception to raise on timeout
"""
try:
result = self.wait_for_signals(
signal_conditions, timeout, check_stopped, interval, all_signals
)
if result is True:
# pylint: disable=protected-access
status.set_finished()
else:
if self.stopped:
# INFO This will execute a callback to the parent device.stop() method
status.set_exception(exc=DeviceStopError(f"{self.name} was stopped"))
else:
# INFO This will execute a callback to the parent device.stop() method
status.set_exception(exc=exception_on_timeout)
# pylint: disable=broad-except
except Exception as exc:
content = traceback.format_exc()
logger.warning(f"Error in wait_for_signals in {self.name}; Traceback: {content}")
# INFO This will execute a callback to the parent device.stop() method
status.set_exception(exc=exc)
thread = threading.Thread(
target=wait_for_signals_wrapper,
args=(
status,
signal_conditions,
timeout,
check_stopped,
interval,
all_signals,
exception_on_timeout,
),
daemon=True,
)
thread.start()
return status
+555
View File
@@ -0,0 +1,555 @@
"""Module for the Mo1 Bragg positioner of the Debye beamline.
The softIOC is reachable via the EPICS prefix X01DA-OP-MO1:BRAGG: and connected
to a motor controller via web sockets. The Mo1 Bragg positioner is not only a
positioner, but also a scan controller to setup XAS and XRD scans. A few scan modes
are programmed in the controller, e.g. simple and advanced XAS scans + XRD triggering mode.
Note: For some of the Epics PVs, in particular action buttons, the put_complete=True is
used to ensure that the action is executed completely. This is believed
to allow for a more stable execution of the action."""
import time
from typing import Literal
from bec_lib.devicemanager import ScanInfo
from bec_lib.logger import bec_logger
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from ophyd import Component as Cpt
from ophyd import DeviceStatus, StatusBase
from ophyd.status import WaitTimeoutError
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from pydantic import BaseModel, Field
from typeguard import typechecked
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import Mo1BraggPositioner
# pylint: disable=unused-import
from debye_bec.devices.mo1_bragg.mo1_bragg_enums import (
MoveType,
ScanControlLoadMessage,
ScanControlMode,
ScanControlScanStatus,
TriggerControlMode,
TriggerControlSource,
)
from debye_bec.devices.mo1_bragg.mo1_bragg_utils import compute_spline
from debye_bec.devices.utils.utils import fetch_scan_info
# Initialise logger
logger = bec_logger.logger
########### Exceptions ###########
class Mo1BraggError(Exception):
"""Exception for the Mo1 Bragg positioner"""
########### Mo1 Bragg Motor Class ###########
class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
"""Mo1 Bragg motor for the Debye beamline.
The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:
"""
progress_signal = Cpt(ProgressSignal, name="progress_signal")
USER_ACCESS = ["set_advanced_xas_settings", "set_xtal", "convert_angle_energy"]
def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): # type: ignore
"""
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, prefix=prefix, **kwargs)
self.scan_parameters: ScanServerScanInfo = None
self.timeout_for_pvwait = 7.5
self.valid_scan_names = [
"xas_simple_scan",
"xas_simple_scan_with_xrd",
"xas_advanced_scan",
"xas_advanced_scan_with_xrd",
"nidaq_continuous_scan",
]
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
self.scan_control.scan_progress.subscribe(self._progress_update, run=False)
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.
"""
self.scan_parameters = fetch_scan_info(self.scan_info)
if self.scan_control.scan_msg.get() != ScanControlLoadMessage.PENDING:
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING)
self.cancel_on_stop(status)
self.scan_control.scan_val_reset.put(1)
status.wait(timeout=self.timeout_for_pvwait)
scan_name = self.scan_parameters.scan_name
if self._check_if_scan_name_is_valid(self.scan_parameters):
if self.scan_parameters.positions is not None:
start, stop = (
self.scan_parameters.positions
if len(self.scan_parameters.positions) == 2
else (None, None)
)
else:
start, stop = (None, None)
scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None)
scan_duration = self.scan_parameters.additional_scan_parameters.get(
"scan_duration", None
)
if scan_name == "xas_simple_scan":
if any(param is None for param in [start, stop, scan_time, scan_duration]):
raise Mo1BraggError(
f"Missing scan parameters for xas_simple_scan. Required parameters: start, stop, scan_time, scan_duration in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
)
self.set_xas_settings(low=start, high=stop, scan_time=scan_time)
self.set_trig_settings(
enable_low=False,
enable_high=False,
break_time_low=0,
break_time_high=0,
cycle_low=0,
cycle_high=0,
exp_time=0,
n_of_trigger=0,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=scan_duration
)
elif scan_name == "xas_simple_scan_with_xrd":
break_enable_low = self.scan_parameters.additional_scan_parameters.get(
"break_enable_low", None
)
break_enable_high = self.scan_parameters.additional_scan_parameters.get(
"break_enable_high", None
)
break_time_low = self.scan_parameters.additional_scan_parameters.get(
"break_time_low", None
)
break_time_high = self.scan_parameters.additional_scan_parameters.get(
"break_time_high", None
)
cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None)
cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None)
exp_time = self.scan_parameters.exp_time
n_of_trigger = self.scan_parameters.additional_scan_parameters.get(
"n_of_trigger", None
)
if any(
param is None
for param in [
start,
stop,
scan_time,
scan_duration,
break_enable_low,
break_enable_high,
break_time_low,
break_time_high,
cycle_low,
cycle_high,
exp_time,
n_of_trigger,
]
):
raise Mo1BraggError(
f"Missing scan parameters for xas_simple_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
)
self.set_xas_settings(low=start, high=stop, scan_time=scan_time)
self.set_trig_settings(
enable_low=break_enable_low,
enable_high=break_enable_high,
break_time_low=break_time_low,
break_time_high=break_time_high,
cycle_low=cycle_low,
cycle_high=cycle_high,
exp_time=exp_time,
n_of_trigger=n_of_trigger,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=scan_duration
)
elif scan_name == "xas_advanced_scan":
p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None)
e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None)
if any(
param is None
for param in [start, stop, scan_time, scan_duration, p_kink, e_kink]
):
raise Mo1BraggError(
f"Missing scan parameters for xas_advanced_scan. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
)
self.set_advanced_xas_settings(
low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink
)
self.set_trig_settings(
enable_low=False,
enable_high=False,
break_time_low=0,
break_time_high=0,
cycle_low=0,
cycle_high=0,
exp_time=0,
n_of_trigger=0,
)
self.set_scan_control_settings(
mode=ScanControlMode.ADVANCED, scan_duration=scan_duration
)
elif scan_name == "xas_advanced_scan_with_xrd":
p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None)
e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None)
break_enable_low = self.scan_parameters.additional_scan_parameters.get(
"break_enable_low", None
)
break_enable_high = self.scan_parameters.additional_scan_parameters.get(
"break_enable_high", None
)
break_time_low = self.scan_parameters.additional_scan_parameters.get(
"break_time_low", None
)
break_time_high = self.scan_parameters.additional_scan_parameters.get(
"break_time_high", None
)
cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None)
cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None)
exp_time = self.scan_parameters.exp_time
n_of_trigger = self.scan_parameters.additional_scan_parameters.get(
"n_of_trigger", None
)
if any(
param is None
for param in [
start,
stop,
scan_time,
scan_duration,
p_kink,
e_kink,
break_enable_low,
break_enable_high,
break_time_low,
break_time_high,
cycle_low,
cycle_high,
exp_time,
n_of_trigger,
]
):
raise Mo1BraggError(
f"Missing scan parameters for xas_advanced_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}"
)
self.set_advanced_xas_settings(
low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink
)
self.set_trig_settings(
enable_low=break_enable_low,
enable_high=break_enable_high,
break_time_low=break_time_low,
break_time_high=break_time_high,
cycle_low=cycle_low,
cycle_high=cycle_high,
exp_time=exp_time,
n_of_trigger=n_of_trigger,
)
self.set_scan_control_settings(
mode=ScanControlMode.ADVANCED, scan_duration=scan_duration
)
else:
return # Should never happen.
else:
return
# Setting scan duration seems to lag behind slightly in the backend, include small sleep
logger.info(f"Sleeping for one second")
time.sleep(1)
logger.info(f"Device {self.name}, done sleeping")
# Load the scan parameters to the controller
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.SUCCESS)
self.cancel_on_stop(status)
self.scan_control.scan_load.put(1)
# Wait for params to be checked from controller
status.wait(self.timeout_for_pvwait)
return None
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device."""
if self.stopped is True:
logger.warning(f"Resetting stopped in unstage for device {self.name}.")
self._stopped = False
if self.scan_control.scan_msg.get() in [
ScanControlLoadMessage.STARTED,
ScanControlLoadMessage.SUCCESS,
]:
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING)
self.cancel_on_stop(status)
try:
status.wait(2)
return None
except WaitTimeoutError:
logger.warning(
f"Timeout in on_unstage of {self.name} after {self.timeout_for_pvwait}s, current scan_control_message : {self.scan_control.scan_msg.get()}"
)
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING)
self.cancel_on_stop(status)
self.scan_control.scan_val_reset.put(1)
status.wait(timeout=self.timeout_for_pvwait)
else:
status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING)
self.cancel_on_stop(status)
self.scan_control.scan_val_reset.put(1)
status.wait(timeout=self.timeout_for_pvwait)
return None
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""Called right before the scan starts on all devices automatically."""
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""Called to inquire if a device has completed a scans."""
status = CompareStatus(self.scan_control.scan_done, 1)
self.cancel_on_stop(status)
return status
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
scan_duration = self.scan_control.scan_duration.get()
# TODO implement better logic for infinite scans, at least bring it up with Debye
start_func = (
self.scan_control.scan_start_infinite.put
if scan_duration < 0.1
else self.scan_control.scan_start_timer.put
)
status = TransitionStatus(
self.scan_control.scan_status,
transitions=[ScanControlScanStatus.READY, ScanControlScanStatus.RUNNING],
strict=True,
failure_states=[ScanControlScanStatus.PARAMETER_WRONG],
)
self.cancel_on_stop(status)
start_func(1)
return status
def on_stop(self) -> None:
"""Called when the device is stopped."""
self.stopped = True # Needs to be set to stop motion
######### Utility Methods #########
def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool:
"""Check if the scan is within the list of scans for which the backend is working"""
if scan_parameters.scan_name in self.valid_scan_names:
return True
return False
def _progress_update(self, value, **kwargs) -> None:
"""Callback method to update the scan progress, runs a callback
to SUB_PROGRESS subscribers, i.e. BEC.
Args:
value (int) : current progress value
"""
max_value = 100
self.progress_signal.put(value=value, max_value=max_value, done=bool(max_value == value))
def set_xas_settings(self, low: float, high: float, scan_time: float) -> None:
"""Set XAS parameters for upcoming scan.
Args:
low (float): Low energy/angle value of the scan
high (float): High energy/angle value of the scan
scan_time (float): Time for a half oscillation
"""
status_list = []
status_list.append(self.scan_settings.s_scan_energy_lo.set(low))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.s_scan_energy_hi.set(high))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.s_scan_scantime.set(scan_time))
self.cancel_on_stop(status_list[-1])
for s in status_list:
s.wait(timeout=self.timeout_for_pvwait)
@typechecked
def convert_angle_energy(
self, mode: Literal["AngleToEnergy", "EnergyToAngle"], inp: float
) -> float:
"""Calculate energy to angle or vice versa
Args:
mode (Literal["AngleToEnergy", "EnergyToAngle"]): Mode of calculation
input (float): Either angle or energy
Returns:
output (float): Converted angle or energy
"""
self.calculator.calc_reset.put(0)
self.calculator.calc_reset.put(1)
status = CompareStatus(self.calculator.calc_done, 0)
self.cancel_on_stop(status)
status.wait(self.timeout_for_pvwait)
self.calculator.calc_reset.put(0)
if mode == "AngleToEnergy":
in_signal = self.calculator.calc_angle
out_signal = self.calculator.calc_energy
elif mode == "EnergyToAngle":
in_signal = self.calculator.calc_energy
out_signal = self.calculator.calc_angle
else:
raise Mo1BraggError(f'Unknown mode {mode}')
in_signal.put(inp)
status = CompareStatus(self.calculator.calc_done, 1)
self.cancel_on_stop(status)
status.wait(self.timeout_for_pvwait)
status = CompareStatus(out_signal, 0, operation_success='>')
self.cancel_on_stop(status)
status.wait(self.timeout_for_pvwait)
return out_signal.get()
def set_advanced_xas_settings(
self, low: float, high: float, scan_time: float, p_kink: float, e_kink: float
) -> None:
"""Set Advanced XAS parameters for upcoming scan.
Args:
low (float): Low angle value of the scan in eV
high (float): High angle value of the scan in eV
scan_time (float): Time for a half oscillation in s
p_kink (float): Position of kink in %
e_kink (float): Energy of kink in eV
"""
e_kink_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=e_kink)
# Angle and Energy are inverse proportional!
high_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=low)
low_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=high)
pos, vel, dt = compute_spline(
low_deg=low_deg,
high_deg=high_deg,
p_kink=p_kink,
e_kink_deg=e_kink_deg,
scan_time=scan_time,
)
status_list = []
status_list.append(self.scan_settings.a_scan_pos.set(pos))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.a_scan_vel.set(vel))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.a_scan_time.set(dt))
self.cancel_on_stop(status_list[-1])
for s in status_list:
s.wait(timeout=self.timeout_for_pvwait)
def set_trig_settings(
self,
enable_low: bool,
enable_high: bool,
break_time_low: float,
break_time_high: float,
cycle_low: int,
cycle_high: int,
exp_time: float,
n_of_trigger: int,
) -> None:
"""Set TRIG settings for the upcoming scan.
Args:
enable_low (bool): Enable TRIG for low energy/angle
enable_high (bool): Enable TRIG for high energy/angle
break_time_low (float): Exposure time for low energy/angle
break_time_high (float): Exposure time for high energy/angle
cycle_low (int): Cycle for low energy/angle
cycle_high (int): Cycle for high energy/angle
exp_time (float): Length of 1 trigger period in seconds
n_of_trigger (int): Amount of triggers to be fired during brake
"""
status_list = []
status_list.append(self.scan_settings.trig_ena_hi_enum.set(int(enable_high)))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.trig_ena_lo_enum.set(int(enable_low)))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.trig_time_hi.set(break_time_high))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.trig_time_lo.set(break_time_low))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.trig_every_n_hi.set(cycle_high))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_settings.trig_every_n_lo.set(cycle_low))
self.cancel_on_stop(status_list[-1])
status_list.append(self.trigger_settings.xrd_trig_period.set(exp_time))
self.cancel_on_stop(status_list[-1])
status_list.append(self.trigger_settings.xrd_n_of_trig.set(n_of_trigger))
self.cancel_on_stop(status_list[-1])
for s in status_list:
s.wait(timeout=self.timeout_for_pvwait)
def set_scan_control_settings(self, mode: ScanControlMode, scan_duration: float) -> None:
"""Set the scan control settings for the upcoming scan.
Args:
mode (ScanControlMode): Mode for the scan, either simple or advanced
scan_duration (float): Duration of the scan
"""
val = ScanControlMode(mode).value
status_list = []
status_list.append(self.scan_control.scan_mode_enum.set(val))
self.cancel_on_stop(status_list[-1])
status_list.append(self.scan_control.scan_duration.set(scan_duration))
self.cancel_on_stop(status_list[-1])
for s in status_list:
s.wait(timeout=self.timeout_for_pvwait)
@@ -0,0 +1,20 @@
"""Positioner implementation with readback angle of the MO1 Bragg positioner."""
from ophyd import Component as Cpt
from ophyd import EpicsSignalRO, EpicsSignalWithRBV
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import Mo1BraggPositioner
class Mo1BraggAngle(Mo1BraggPositioner):
"""Positioner implementation with readback angle of the MO1 Bragg positioner."""
readback = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True)
setpoint = Cpt(EpicsSignalWithRBV, suffix="set_abs_pos_angle", kind="normal", auto_monitor=True)
low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_angle_RBV", kind="config", auto_monitor=True)
high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_angle_RBV", kind="config", auto_monitor=True)
@property
def egu(self) -> str:
"""Return the engineering unit of the positioner."""
return "deg"
@@ -0,0 +1,442 @@
"""Module for the Mo1 Bragg positioner"""
import threading
import time
import traceback
from typing import Literal
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import (
Device,
DeviceStatus,
EpicsSignal,
EpicsSignalRO,
EpicsSignalWithRBV,
PositionerBase,
Signal,
)
from ophyd.utils import LimitError
from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType
# Initialise logger
logger = bec_logger.logger
############# Exceptions #############
class Mo1BraggStoppedError(Exception):
"""Exception to raise when the Bragg positioner is stopped."""
############# Signal classes #############
class MoveTypeSignal(Signal):
"""Custom Signal to set the move type of the Bragg positioner"""
# pylint: disable=arguments-differ
def set(self, value: str | MoveType) -> None:
"""Returns currently active move method
Args:
value (str | MoveType) : Can be either 'energy' or 'angle'
"""
value = MoveType(value.lower())
self._readback = value.value
############# Utility devices to separate the namespace #############
class Mo1BraggStatus(Device):
"""Mo1 Bragg PVs for status monitoring"""
error_status = Cpt(EpicsSignalRO, suffix="error_status_RBV", kind="config", auto_monitor=True)
brake_enabled = Cpt(EpicsSignalRO, suffix="brake_enabled_RBV", kind="config", auto_monitor=True)
mot_commutated = Cpt(
EpicsSignalRO, suffix="mot_commutated_RBV", kind="config", auto_monitor=True
)
axis_enabled = Cpt(EpicsSignalRO, suffix="axis_enabled_RBV", kind="config", auto_monitor=True)
enc_initialized = Cpt(
EpicsSignalRO, suffix="enc_initialized_RBV", kind="config", auto_monitor=True
)
heartbeat = Cpt(EpicsSignalRO, suffix="heartbeat_RBV", kind="config", auto_monitor=True)
class Mo1BraggEncoder(Device):
"""Mo1 Bragg PVs to communicate with the encoder"""
enc_reinit = Cpt(EpicsSignal, suffix="enc_reinit", kind="config")
enc_reinit_done = Cpt(EpicsSignalRO, suffix="enc_reinit_done_RBV", kind="config")
class Mo1BraggCrystal(Device):
"""Mo1 Bragg PVs to set the crystal parameters"""
bragg_off_si111 = Cpt(EpicsSignalWithRBV, suffix="bragg_off_si111", kind="config")
bragg_off_si311 = Cpt(EpicsSignalWithRBV, suffix="bragg_off_si311", kind="config")
phi_off_si111 = Cpt(EpicsSignalWithRBV, suffix="phi_off_si111", kind="config")
phi_off_si311 = Cpt(EpicsSignalWithRBV, suffix="phi_off_si311", kind="config")
azm_off_si111 = Cpt(EpicsSignalWithRBV, suffix="azm_off_si111", kind="config")
azm_off_si311 = Cpt(EpicsSignalWithRBV, suffix="azm_off_si311", kind="config")
miscut_si111 = Cpt(EpicsSignalWithRBV, suffix="miscut_si111", kind="config")
miscut_si311 = Cpt(EpicsSignalWithRBV, suffix="miscut_si311", kind="config")
xtal_enum = Cpt(EpicsSignalWithRBV, suffix="xtal_ENUM", kind="config")
d_spacing_si111 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si111", kind="config")
d_spacing_si311 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si311", kind="config")
set_offset = Cpt(EpicsSignal, suffix="set_offset", kind="config", put_complete=True)
current_d_spacing = Cpt(
EpicsSignalRO, suffix="current_d_spacing_RBV", kind="normal", auto_monitor=True
)
current_bragg_off = Cpt(
EpicsSignalRO, suffix="current_bragg_off_RBV", kind="normal", auto_monitor=True
)
current_phi_off = Cpt(
EpicsSignalRO, suffix="current_phi_off_RBV", kind="normal", auto_monitor=True
)
current_azm_off = Cpt(
EpicsSignalRO, suffix="current_azm_off_RBV", kind="normal", auto_monitor=True
)
current_miscut = Cpt(
EpicsSignalRO, suffix="current_miscut_RBV", kind="normal", auto_monitor=True
)
current_xtal = Cpt(
EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True
)
current_xtal_string = Cpt(
EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True
)
class Mo1BraggScanSettings(Device):
"""Mo1 Bragg PVs to set the scan setttings"""
# TRIG settings
trig_select_ref_enum = Cpt(EpicsSignalWithRBV, suffix="trig_select_ref_ENUM", kind="config")
trig_ena_hi_enum = Cpt(EpicsSignalWithRBV, suffix="trig_ena_hi_ENUM", kind="config")
trig_time_hi = Cpt(EpicsSignalWithRBV, suffix="trig_time_hi", kind="config")
trig_every_n_hi = Cpt(EpicsSignalWithRBV, suffix="trig_every_n_hi", kind="config")
trig_ena_lo_enum = Cpt(EpicsSignalWithRBV, suffix="trig_ena_lo_ENUM", kind="config")
trig_time_lo = Cpt(EpicsSignalWithRBV, suffix="trig_time_lo", kind="config")
trig_every_n_lo = Cpt(EpicsSignalWithRBV, suffix="trig_every_n_lo", kind="config")
# XAS simple scan settings
s_scan_angle_hi = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_hi", kind="config")
s_scan_angle_lo = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_lo", kind="config")
s_scan_energy_lo = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_lo", kind="config", auto_monitor=True
)
s_scan_energy_hi = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_hi", kind="config", auto_monitor=True
)
s_scan_scantime = Cpt(
EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True
)
# XAS advanced scan settings
a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=True)
a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=True)
a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=True)
class Mo1TriggerSettings(Device):
"""Mo1 Trigger settings"""
settle_time = Cpt(EpicsSignalWithRBV, suffix="settle_time", kind="config")
max_dev = Cpt(EpicsSignalWithRBV, suffix="max_dev", kind="config")
xrd_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_src_ENUM", kind="config")
xrd_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_mode_ENUM", kind="config")
xrd_trig_len = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_len", kind="config")
xrd_trig_period = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_period", kind="config")
xrd_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="xrd_n_of_trig", kind="config")
xrd_trig_req = Cpt(EpicsSignal, suffix="xrd_trig_req", kind="config")
falcon_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_src_ENUM", kind="config")
falcon_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_mode_ENUM", kind="config")
falcon_trig_len = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_len", kind="config")
falcon_trig_period = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_period", kind="config")
falcon_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="falcon_n_of_trig", kind="config")
falcon_trig_req = Cpt(EpicsSignal, suffix="falcon_trig_req", kind="config")
univ1_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_src_ENUM", kind="config")
univ1_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_mode_ENUM", kind="config")
univ1_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_len", kind="config")
univ1_trig_period = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_period", kind="config")
univ1_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="univ1_n_of_trig", kind="config")
univ1_trig_req = Cpt(EpicsSignal, suffix="univ1_trig_req", kind="config")
univ2_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_src_ENUM", kind="config")
univ2_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_mode_ENUM", kind="config")
univ2_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_len", kind="config")
univ2_trig_period = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_period", kind="config")
univ2_n_of_trig = Cpt(EpicsSignalWithRBV, suffix="univ2_n_of_trig", kind="config")
univ2_trig_req = Cpt(EpicsSignal, suffix="univ2_trig_req", kind="config")
class Mo1BraggCalculator(Device):
"""Mo1 Bragg PVs to convert angle to energy or vice-versa."""
calc_reset = Cpt(EpicsSignalWithRBV, suffix="calc_reset", kind="config", put_complete=True)
calc_done = Cpt(EpicsSignalRO, suffix="calc_done_RBV", kind="config")
calc_energy = Cpt(EpicsSignalWithRBV, suffix="calc_energy", kind="config")
calc_angle = Cpt(EpicsSignalWithRBV, suffix="calc_angle", kind="config")
class Mo1BraggScanControl(Device):
"""Mo1 Bragg PVs to control the scan after setting the parameters."""
scan_mode_enum = Cpt(EpicsSignalWithRBV, suffix="scan_mode_ENUM", kind="config")
scan_duration = Cpt(
EpicsSignalWithRBV, suffix="scan_duration", kind="config", auto_monitor=True
)
scan_load = Cpt(EpicsSignal, suffix="scan_load", kind="config", put_complete=True)
scan_msg = Cpt(EpicsSignalRO, suffix="scan_msg_ENUM_RBV", kind="config", auto_monitor=True)
scan_start_infinite = Cpt(
EpicsSignal, suffix="scan_start_infinite", kind="config", put_complete=True
)
scan_start_timer = Cpt(EpicsSignal, suffix="scan_start_timer", kind="config", put_complete=True)
scan_stop = Cpt(EpicsSignal, suffix="scan_stop", kind="config", put_complete=True)
scan_status = Cpt(
EpicsSignalRO, suffix="scan_status_ENUM_RBV", kind="config", auto_monitor=True
)
scan_time_left = Cpt(
EpicsSignalRO, suffix="scan_time_left_RBV", kind="config", auto_monitor=True
)
scan_done = Cpt(EpicsSignalRO, suffix="scan_done_RBV", kind="config", auto_monitor=True)
scan_val_reset = Cpt(EpicsSignal, suffix="scan_val_reset", kind="config", put_complete=True)
scan_progress = Cpt(EpicsSignalRO, suffix="scan_progress_RBV", kind="config", auto_monitor=True)
scan_spectra_done = Cpt(
EpicsSignalRO, suffix="scan_n_osc_RBV", kind="config", auto_monitor=True
)
scan_spectra_left = Cpt(
EpicsSignalRO, suffix="scan_n_osc_left_RBV", kind="config", auto_monitor=True
)
class Mo1BraggPositioner(Device, PositionerBase):
"""
Positioner implementation with readback energy of the MO1 Bragg positioner.
The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:
This soft IOC connects to the NI motor and its control loop.
"""
USER_ACCESS = ["set_xtal"]
####### Sub-components ########
# Namespace is cleaner and easier to maintain
crystal = Cpt(Mo1BraggCrystal, "")
encoder = Cpt(Mo1BraggEncoder, "")
scan_settings = Cpt(Mo1BraggScanSettings, "")
trigger_settings = Cpt(Mo1TriggerSettings, "")
calculator = Cpt(Mo1BraggCalculator, "")
scan_control = Cpt(Mo1BraggScanControl, "")
status = Cpt(Mo1BraggStatus, "")
############# Energy PVs #############
readback = Cpt(
EpicsSignalRO, suffix="feedback_pos_energy_RBV", kind="hinted", auto_monitor=True
)
setpoint = Cpt(
EpicsSignalWithRBV, suffix="set_abs_pos_energy", kind="normal", auto_monitor=True
)
motor_is_moving = Cpt(
EpicsSignalRO, suffix="move_abs_done_RBV", kind="normal", auto_monitor=True
)
low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_energy_RBV", kind="config", auto_monitor=True)
high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_energy_RBV", kind="config", auto_monitor=True)
velocity = Cpt(EpicsSignalWithRBV, suffix="move_velocity", kind="config", auto_monitor=True)
angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True)
########## Move Command PVs ##########
move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True)
move_stop = Cpt(EpicsSignal, suffix="move_stop", kind="config", put_complete=True)
SUB_READBACK = "readback"
_default_sub = SUB_READBACK
SUB_PROGRESS = "progress"
def __init__(self, prefix="", *, name: str, **kwargs):
"""Initialize the Mo1 Bragg positioner.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, **kwargs)
self._move_thread = None
self._stopped = False
self.readback.name = self.name
def stop(self, *, success=False) -> None:
"""Stop any motion on the positioner
Args:
success (bool) : Flag to indicate if the motion was successful
"""
self.move_stop.put(1)
self._stopped = True
super().stop(success=success)
def stop_scan(self) -> None:
"""Stop the currently running scan gracefully, this finishes the running oscillation."""
self.scan_control.scan_stop.put(1)
@property
def stopped(self) -> bool:
"""Return the status of the positioner"""
return self._stopped
######### Positioner specific methods #########
@property
def limits(self) -> tuple:
"""Return limits of the Bragg positioner"""
return (self.low_lim.get(), self.high_lim.get())
@property
def low_limit(self) -> float:
"""Return low limit of axis"""
return self.limits[0]
@property
def high_limit(self) -> float:
"""Return high limit of axis"""
return self.limits[1]
@property
def egu(self) -> str:
"""Return the engineering units of the positioner"""
return "eV"
@property
def position(self) -> float:
"""Return the current position of Mo1Bragg, considering the move type"""
return self.readback.get()
# pylint: disable=arguments-differ
def check_value(self, value: float) -> None:
"""Method to check if a value is within limits of the positioner.
Called by PositionerBase.move()
Args:
value (float) : value to move axis to.
"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not low_limit <= value <= high_limit:
raise LimitError(f"position={value} not within limits {self.limits}")
def _move_and_finish(
self, target_pos: float, status: DeviceStatus, update_frequency: float = 0.1
) -> None:
"""
Method to be called in the move thread to move the Bragg positioner
to the target position.
Args:
target_pos (float) : target position for the motion
move_cpt (Cpt) : component to set the target position on the IOC,
either setpoint or setpoint_abs_angle depending
on the move type
read_cpt (Cpt) : component to read the current position of the motion,
readback or feedback_pos_angle
status (DeviceStatus) : status object to set the status of the motion
update_frequency (float): Optional, frequency to update the current position of
the motion, defaults to 0.1s
"""
motor_name = None
try:
# Set the target position on IOC
self.setpoint.put(target_pos)
self.move_abs.put(1)
# Currently sleep is needed due to delay in updates on PVs, maybe time can be reduced
time.sleep(0.5)
motor_name = self.name
while self.motor_is_moving.get() == 0:
if self.stopped:
raise Mo1BraggStoppedError(f"Device {self.name} was stopped")
time.sleep(update_frequency)
# pylint: disable=protected-access
status.set_finished()
# pylint: disable=broad-except
except Exception as exc:
content = traceback.format_exc()
logger.error(
f"Error in move thread of device {motor_name if motor_name else ''}: {content}"
)
status.set_exception(exc=exc)
def move(self, value: float, **kwargs) -> DeviceStatus:
"""
Move the Bragg positioner to the specified value, allows to
switch between move types angle and energy.
Args:
value (float) : target value for the motion
move_type (str | MoveType) : Optional, specify the type of move,
either 'energy' or 'angle'
Returns:
DeviceStatus : status object to track the motion
"""
self._stopped = False
self.check_value(value)
status = DeviceStatus(device=self)
self._move_thread = threading.Thread(
target=self._move_and_finish, args=(value, status, 0.1)
)
self._move_thread.start()
return status
# -------------- End of Positioner specific methods -----------------#
# -------------- MO1 Bragg specific methods -----------------#
def set_xtal(
self,
xtal_enum: Literal["111", "311"],
bragg_off_si111: float = None,
bragg_off_si311: float = None,
d_spacing_si111: float = None,
d_spacing_si311: float = None,
) -> None:
"""Method to set the crystal parameters of the Bragg positioner
Args:
xtal_enum (Literal["111", "311"]) : Enum to set the crystal orientation
bragg_off_si111 (float) : Offset for the 111 crystal
bragg_off_si311 (float) : Offset for the 311 crystal
d_spacing_si111 (float) : d-spacing for the 111 crystal
d_spacing_si311 (float) : d-spacing for the 311 crystal
"""
if bragg_off_si111 is not None:
self.crystal.bragg_off_si111.put(bragg_off_si111)
if bragg_off_si311 is not None:
self.crystal.bragg_off_si311.put(bragg_off_si311)
if d_spacing_si111 is not None:
self.crystal.d_spacing_si111.put(d_spacing_si111)
if d_spacing_si311 is not None:
self.crystal.d_spacing_si311.put(d_spacing_si311)
if xtal_enum == "111":
crystal_set = 0
elif xtal_enum == "311":
crystal_set = 1
else:
raise ValueError(
f"Invalid argument for xtal_enum : {xtal_enum}, choose from '111' or '311'"
)
self.crystal.xtal_enum.put(crystal_set)
self.crystal.set_offset.put(1)
@@ -0,0 +1,61 @@
"""Enums for the Bragg positioner and trigger generator"""
import enum
class TriggerControlSource(int, enum.Enum):
"""Enum class for the trigger control source of the trigger generator"""
EPICS = 0
INPOS = 1
class TriggerControlMode(int, enum.Enum):
"""Enum class for the trigger control mode of the trigger generator"""
PULSE = 0
CONDITION = 1
class ScanControlScanStatus(int, enum.Enum):
"""Enum class for the scan status of the Bragg positioner"""
PARAMETER_WRONG = 0
VALIDATION_PENDING = 1
READY = 2
RUNNING = 3
class ScanControlLoadMessage(int, enum.Enum):
"""Enum for validating messages for load message of the Bragg positioner"""
PENDING = 0
STARTED = 1
SUCCESS = 2
ERR_TRIG_MEAS_LEN_LOW = 3
ERR_TRIG_N_TRIGGERS_LOW = 4
ERR_TRIG_TRIGS_EVERY_N_LOW = 5
ERR_TRIG_MEAS_LEN_HI = 6
ERR_TRIG_N_TRIGGERS_HI = 7
ERR_TRIG_TRIGS_EVERY_N_HI = 8
ERR_SCAN_HI_ANGLE_LIMIT = 9
ERR_SCAN_LOW_ANGLE_LIMITS = 10
ERR_SCAN_TIME = 11
ERR_SCAN_VEL_TOO_HI = 12
ERR_SCAN_ANGLE_OUT_OF_LIM = 13
ERR_SCAN_HIGH_VEL_LAR_42 = 14
ERR_SCAN_MODE_INVALID = 15
class MoveType(str, enum.Enum):
"""Enum class to switch between move types energy and angle for the Bragg positioner"""
ENERGY = "energy"
ANGLE = "angle"
class ScanControlMode(int, enum.Enum):
"""Enum class for the scan control mode of the Bragg positioner"""
SIMPLE = 0
ADVANCED = 1
@@ -0,0 +1,93 @@
"""Module for additional utils of the Mo1 Bragg Positioner"""
import numpy as np
from scipy.interpolate import BSpline
################ Define Constants ############
SAFETY_FACTOR = 0.025 # safety factor to limit acceleration -> NEVER SET TO ZERO !
N_SAMPLES = 41 # number of samples to generate -> Always choose uneven number,
# otherwise peak value will not be included
DEGREE_SPLINE = 3 # DEGREE_SPLINE of spline, 3 works good
TIME_COMPENSATE_SPLINE = 0.0062 # time to be compensated each spline in s
POSITION_COMPONSATION = 0.02 # angle to add at both limits, must be same values
# as used on ACS controller for simple scans
class Mo1UtilsSplineError(Exception):
"""Exception for spline computation"""
def compute_spline(
low_deg: float, high_deg: float, p_kink: float, e_kink_deg: float, scan_time: float
) -> tuple[float, float, float]:
"""Spline computation for the advanced scan mode
Args:
low_deg (float): Low angle value of the scan in deg
high_deg (float): High angle value of the scan in deg
scan_time (float): Time for a half oscillation in s
p_kink (float): Position of kink in %
e_kink_deg (float): Position of kink in degree
Returns:
tuple[float,float,float] : Position, Velocity and delta T arrays for the spline
"""
# increase motion range slightly so that xas trigger signals will occur at defined energy limits
low_deg = low_deg - POSITION_COMPONSATION
high_deg = high_deg + POSITION_COMPONSATION
if not (0 <= p_kink <= 100):
raise Mo1UtilsSplineError(
"Kink position not within range of [0..100%]" + f"for p_kink: {p_kink}"
)
if not (low_deg < e_kink_deg < high_deg):
raise Mo1UtilsSplineError(
"Kink energy not within selected energy range of scan,"
+ f"for e_kink_deg {e_kink_deg}, low_deg {low_deg} and"
+ f"high_deg {high_deg}."
)
tc1 = SAFETY_FACTOR / scan_time * TIME_COMPENSATE_SPLINE
t_kink = (scan_time - TIME_COMPENSATE_SPLINE - 2 * (SAFETY_FACTOR - tc1)) * p_kink / 100 + (
SAFETY_FACTOR - tc1
)
t_input = [
0,
SAFETY_FACTOR - tc1,
t_kink,
scan_time - TIME_COMPENSATE_SPLINE - SAFETY_FACTOR + tc1,
scan_time - TIME_COMPENSATE_SPLINE,
]
p_input = [0, 0, e_kink_deg - low_deg, high_deg - low_deg, high_deg - low_deg]
cv = np.stack((t_input, p_input)).T # spline coefficients
max_param = len(cv) - DEGREE_SPLINE
kv = np.clip(np.arange(len(cv) + DEGREE_SPLINE + 1) - DEGREE_SPLINE, 0, max_param) # knots
spl = BSpline(kv, cv, DEGREE_SPLINE) # get spline function
p = spl(np.linspace(0, max_param, N_SAMPLES))
v = spl(np.linspace(0, max_param, N_SAMPLES), 1)
a = spl(np.linspace(0, max_param, N_SAMPLES), 2)
j = spl(np.linspace(0, max_param, N_SAMPLES), 3)
tim, pos = p.T
pos = pos + low_deg
vel = v[:, 1] / v[:, 0]
acc = []
for item in a:
acc.append(0) if item[1] == 0 else acc.append(item[1] / item[0])
jerk = []
for item in j:
jerk.append(0) if item[1] == 0 else jerk.append(item[1] / item[0])
dt = np.zeros(len(tim))
for i in np.arange(len(tim)):
if i == 0:
dt[i] = 0
else:
dt[i] = 1000 * (tim[i] - tim[i - 1])
return pos, vel, dt
View File
+515
View File
@@ -0,0 +1,515 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
from bec_lib.logger import bec_logger
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Kind, StatusBase
from ophyd.status import WaitTimeoutError
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.sim.sim_signals import SetableSignal
from debye_bec.devices.nidaq.nidaq_enums import (
EncoderFactors,
NIDAQCompression,
NidaqState,
ReadoutRange,
ScanRates,
ScanType,
)
from debye_bec.devices.utils.utils import fetch_scan_info
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
logger = bec_logger.logger
class NidaqError(Exception):
"""Nidaq specific error"""
class NidaqControl(Device):
"""Nidaq control class with all PVs"""
# fmt: off
### Readback PVs for EpicsEmitter ###
energy = Cpt(SetableSignal, value=0, kind=Kind.normal)
smpl_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption")
smpl_fluo = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence")
ref_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption")
cisum = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum")
ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN")
ai1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN")
ai2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN")
ai3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN")
ai4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN")
ai5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN")
ai6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN")
ai7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN")
di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX")
di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX")
di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX")
di3_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 3, MAX")
di4_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 4, MAX")
ci0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN")
ci1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN")
ci2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN")
ci3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN")
ci4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN")
ci5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN")
ci6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN")
ci7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN")
ci8_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN")
ci9_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN")
ci10_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN")
ci11_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN")
ci12_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN")
ci13_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN")
ci14_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN")
ci15_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN")
ci16_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN")
ci17_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN")
ai0 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.normal, doc="EPICS analog input 0", auto_monitor=True)
ai1 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.normal, doc="EPICS analog input 1", auto_monitor=True)
ai2 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.normal, doc="EPICS analog input 2", auto_monitor=True)
ai3 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.normal, doc="EPICS analog input 3", auto_monitor=True)
ai4 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.normal, doc="EPICS analog input 4", auto_monitor=True)
ai5 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.normal, doc="EPICS analog input 5", auto_monitor=True)
ai6 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.normal, doc="EPICS analog input 6", auto_monitor=True)
ai7 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.normal, doc="EPICS analog input 7", auto_monitor=True)
ci0 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True)
ci1 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True)
ci2 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True)
ci3 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True)
ci4 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True)
ci5 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True)
ci6 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True)
ci7 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True)
ci8 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.normal, doc="EPICS counter input 8", auto_monitor=True)
ci9 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.normal, doc="EPICS counter input 9", auto_monitor=True)
ci10 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True)
ci11 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True)
ci12 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True)
ci13 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True)
ci14 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True)
ci15 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True)
ci16 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True)
ci17 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True)
di0 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.normal, doc="EPICS digital input 0", auto_monitor=True)
di1 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.normal, doc="EPICS digital input 1", auto_monitor=True)
di2 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.normal, doc="EPICS digital input 2", auto_monitor=True)
di3 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.normal, doc="EPICS digital input 3", auto_monitor=True)
di4 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.normal, doc="EPICS digital input 4", auto_monitor=True)
enc_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENC", kind=Kind.normal, doc="EPICS Encoder reading", auto_monitor=True)
energy_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENERGY", kind=Kind.normal, doc="EPICS Energy reading", auto_monitor=True)
### Readback for BEC emitter ###
ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD")
ai1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, STD")
ai2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, STD")
ai3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, STD")
ai4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, STD")
ai5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, STD")
ai6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, STD")
ai7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD")
ci0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD")
ci1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1. STD")
ci2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2. STD")
ci3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3. STD")
ci4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4. STD")
ci5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5. STD")
ci6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6. STD")
ci7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD")
ci8_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD")
ci9_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD")
ci10_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD")
ci11_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD")
ci12_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD")
ci13_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD")
ci14_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD")
ci15_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD")
ci16_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD")
ci17_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD")
xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp")
xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp")
xrd_angle = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD angle")
xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy")
xrd_ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 mean")
xrd_ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 std dev")
enc = Cpt(SetableSignal, value=0, kind=Kind.normal)
rle = Cpt(SetableSignal, value=0, kind=Kind.normal)
### Control PVs ###
enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config, auto_monitor=True)
enable_dead_time_correction = Cpt(EpicsSignal, suffix="NIDAQ-EnableDTC", kind=Kind.config, auto_monitor=True)
kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config)
stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config)
state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True)
server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config)
compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config)
scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config)
scan_type_string = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config, string=True)
sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, auto_monitor=True)
sampling_rate_string = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, string=True, auto_monitor=True)
scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config)
readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, auto_monitor=True)
readout_range_string = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, string=True, auto_monitor=True)
encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, auto_monitor=True)
encoder_factor_string = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, string=True, auto_monitor=True)
stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config)
power = Cpt(EpicsSignal, suffix="NIDAQ-Power", kind=Kind.config)
heartbeat = Cpt(EpicsSignal, suffix="NIDAQ-Heartbeat", kind=Kind.config, auto_monitor=True)
time_left = Cpt(EpicsSignalRO, suffix="NIDAQ-TimeLeft", kind=Kind.config, auto_monitor=True)
ai_chans = Cpt(EpicsSignal, suffix="NIDAQ-AIChans", kind=Kind.config, auto_monitor=True)
ci_chans = Cpt(EpicsSignal, suffix="NIDAQ-CIChans", kind=Kind.config, auto_monitor=True)
di_chans = Cpt(EpicsSignal, suffix="NIDAQ-DIChans", kind=Kind.config, auto_monitor=True)
add_chans = Cpt(EpicsSignal, suffix="NIDAQ-AddChans", kind=Kind.config, auto_monitor=True)
smpl_abs_ln = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_ln", kind=Kind.config, auto_monitor=True)
ref_abs_ln = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_ln", kind=Kind.config, auto_monitor=True)
smpl_abs_no = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_no", kind=Kind.config, auto_monitor=True)
smpl_abs_no_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_no", kind=Kind.config, string=True, auto_monitor=True)
smpl_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_de", kind=Kind.config, auto_monitor=True)
smpl_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_abs_de", kind=Kind.config, string=True, auto_monitor=True)
smpl_fluo_no = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_no", kind=Kind.config, auto_monitor=True)
smpl_fluo_no_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_no", kind=Kind.config, string=True, auto_monitor=True)
smpl_fluo_de = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_de", kind=Kind.config, auto_monitor=True)
smpl_fluo_de_string = Cpt(EpicsSignal, suffix="NIDAQ-smpl_fluo_de", kind=Kind.config, string=True, auto_monitor=True)
ref_abs_no = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_no", kind=Kind.config, auto_monitor=True)
ref_abs_no_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_no", kind=Kind.config, string=True, auto_monitor=True)
ref_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, auto_monitor=True)
ref_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, string=True, auto_monitor=True)
# fmt: on
class Nidaq(PSIDeviceBase, NidaqControl):
"""NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05
Args:
prefix (str) : Prefix to the NIDAQ soft ioc, currently X01DA-PC-SCANSERVER:
name (str) : Name of the device
scan_info (ScanInfo) : ScanInfo object passed by BEC's devicemanager.
"""
progress_signal = Cpt(ProgressSignal, name="progress_signal")
USER_ACCESS = ["set_config"]
def __init__(self, prefix: str = "", *, name: str, scan_info: ScanInfo = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.scan_parameters: ScanServerScanInfo = None
self.timeout_wait_for_signal = 5 # put 5s firsts
self._timeout_wait_for_pv = (
5 # 5s timeout for pv calls. editted due to timeout issues persisting
)
self.valid_scan_names = [
"xas_simple_scan",
"xas_simple_scan_with_xrd",
"xas_advanced_scan",
"xas_advanced_scan_with_xrd",
"nidaq_continuous_scan",
]
########################################
# Beamline Methods #
########################################
def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool:
"""Check if the scan is within the list of scans for which the backend is working"""
if scan_parameters.scan_name in self.valid_scan_names:
return True
return False
def set_config(
self,
sampling_rate: Literal[
100000, 500000, 1000000, 2000000, 4000000, 5000000, 10000000, 14286000
],
ai: list,
ci: list,
di: list,
scan_type: Literal["continuous", "triggered"] = "triggered",
scan_duration: float = 0,
readout_range: Literal[1, 2, 5, 10] = 10,
encoder_type: Literal["X_1", "X_2", "X_4"] = "X_4",
enable_compression: bool = True,
) -> None:
"""Method to configure the NIDAQ
Args:
sampling_rate(Literal[100000, 500000, 1000000, 2000000, 4000000, 5000000,
10000000, 14286000]): Sampling rate in Hz
ai(list): List of analog input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
ci(list): List of counter input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
di(list): List of digital input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
scan_type(Literal['continuous', 'triggered']): Triggered to use with monochromator,
otherwise continuous, default 'triggered'
scan_duration(float): Scan duration in seconds, use 0 for infinite scan, default 0
readout_range(Literal[1, 2, 5, 10]): Readout range in +- Volts, default +-10V
encoder_type(Literal['X_1', 'X_2', 'X_4']): Encoder readout type, default 'X_4'
enable_compression(bool): Enable or disable compression of data, default True
"""
if sampling_rate == 100000:
self.sampling_rate.put(ScanRates.HUNDRED_KHZ)
elif sampling_rate == 500000:
self.sampling_rate.put(ScanRates.FIVE_HUNDRED_KHZ)
elif sampling_rate == 1000000:
self.sampling_rate.put(ScanRates.ONE_MHZ)
elif sampling_rate == 2000000:
self.sampling_rate.put(ScanRates.TWO_MHZ)
elif sampling_rate == 4000000:
self.sampling_rate.put(ScanRates.FOUR_MHZ)
elif sampling_rate == 5000000:
self.sampling_rate.put(ScanRates.FIVE_MHZ)
elif sampling_rate == 10000000:
self.sampling_rate.put(ScanRates.TEN_MHZ)
elif sampling_rate == 14286000:
self.sampling_rate.put(ScanRates.FOURTEEN_THREE_MHZ)
ai_chans = 0
if isinstance(ai, list):
for ch in ai:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ai_chans = ai_chans | (1 << ch)
self.ai_chans.put(ai_chans)
ci_chans = 0
if isinstance(ci, list):
for ch in ci:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ci_chans = ci_chans | (1 << ch)
self.ci_chans.put(ci_chans)
di_chans = 0
if isinstance(di, list):
for ch in di:
if isinstance(ch, int):
if ch >= 0 and ch <= 4:
di_chans = di_chans | (1 << ch)
self.di_chans.put(di_chans)
if scan_type in "continuous":
self.scan_type.put(ScanType.CONTINUOUS)
elif scan_type in "triggered":
self.scan_type.put(ScanType.TRIGGERED)
if scan_duration >= 0:
self.scan_duration.put(scan_duration)
if readout_range == 1:
self.readout_range.put(ReadoutRange.ONE_V)
elif readout_range == 2:
self.readout_range.put(ReadoutRange.TWO_V)
elif readout_range == 5:
self.readout_range.put(ReadoutRange.FIVE_V)
elif readout_range == 10:
self.readout_range.put(ReadoutRange.TEN_V)
if encoder_type in "1/16":
self.encoder_factor.put(EncoderFactors.X1_16)
elif encoder_type in "1/8":
self.encoder_factor.put(EncoderFactors.X1_8)
elif encoder_type in "1/4":
self.encoder_factor.put(EncoderFactors.X1_4)
elif encoder_type in "1/2":
self.encoder_factor.put(EncoderFactors.X1_2)
elif encoder_type in "1":
self.encoder_factor.put(EncoderFactors.X1)
elif encoder_type in "2":
self.encoder_factor.put(EncoderFactors.X2)
elif encoder_type in "4":
self.encoder_factor.put(EncoderFactors.X4)
if enable_compression is True:
self.enable_compression.put(NIDAQCompression.ON)
elif enable_compression is False:
self.enable_compression.put(NIDAQCompression.OFF)
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
status = TransitionStatus(self.heartbeat, transitions=[0, 1], strict=False)
self.cancel_on_stop(status)
try:
status.wait(timeout=self.timeout_wait_for_signal) # Raises if timeout is reached
except WaitTimeoutError:
logger.warning(f"Device {self.name} was not alive, trying to put power on")
status = TransitionStatus(self.heartbeat, transitions=[0, 1], strict=False)
self.cancel_on_stop(status)
self.power.put(1)
status.wait(timeout=self.timeout_wait_for_signal)
status = CompareStatus(self.state, NidaqState.STANDBY)
self.cancel_on_stop(status)
status.wait(timeout=self.timeout_wait_for_signal)
self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv)
self.time_left.subscribe(self._progress_update, run=False)
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.
If the upcoming scan is not in the list of valid scans, return immediately.
"""
self.scan_parameters = fetch_scan_info(self.scan_info)
if not self._check_if_scan_name_is_valid(self.scan_parameters):
return None
if self.state.get() != NidaqState.STANDBY:
status = CompareStatus(self.state, NidaqState.STANDBY)
self.cancel_on_stop(status)
self.on_stop()
status.wait(timeout=self.timeout_wait_for_signal)
# If scan is not part of the valid_scan_names,
if self.scan_parameters.scan_name != "nidaq_continuous_scan": # what is the new v4 scan
self.scan_type.set(ScanType.TRIGGERED).wait(timeout=self._timeout_wait_for_pv)
self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv)
self.enable_compression.set(1).wait(timeout=self._timeout_wait_for_pv)
else:
self.scan_type.set(ScanType.CONTINUOUS).wait(timeout=self._timeout_wait_for_pv)
self.scan_duration.set(
self.scan_parameters.additional_scan_parameters["scan_duration"]
).wait(timeout=self._timeout_wait_for_pv)
self.enable_compression.set(
self.scan_parameters.additional_scan_parameters["compression"]
).wait(timeout=self._timeout_wait_for_pv)
# Stage call to IOC
status = CompareStatus(self.state, NidaqState.STAGE)
self.cancel_on_stop(status)
# TODO 11.11.25/HS64
# Switched from set to put in the hope to get rid of the rare event where nidaq is stopped at the start of a scan
# Problems consistently persisting, testing changing back to set, unconvinced this is the actual cause 14.11.25/AHC
# self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
self.stage_call.put(1)
status.wait(timeout=self.timeout_wait_for_signal)
if self.scan_parameters.scan_name != "nidaq_continuous_scan":
status = self.on_kickoff()
self.cancel_on_stop(status)
status.wait(timeout=self._timeout_wait_for_pv)
logger.info(f"Device {self.name} was staged: {NidaqState(self.state.get())}")
def on_kickoff(self) -> DeviceStatus | StatusBase:
"""Kickoff the Nidaq"""
status = self.kickoff_call.set(1)
self.cancel_on_stop(status)
return status
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device. Check that the Nidaq goes into Standby"""
status = CompareStatus(self.state, NidaqState.STANDBY)
self.cancel_on_stop(status)
status.wait(timeout=self.timeout_wait_for_signal)
status = self.enable_compression.set(1)
self.cancel_on_stop(status)
status.wait(self._timeout_wait_for_pv)
logger.info(f"Device {self.name} was unstaged: {NidaqState(self.state.get())}")
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""
Called right before the scan starts on all devices automatically.
Here we ensure that the NIDAQ master task is running
before the motor starts its oscillation. This is needed for being properly homed.
The NIDAQ should go into Acquiring mode.
"""
if not self._check_if_scan_name_is_valid(self.scan_parameters):
return None
if self.scan_parameters.scan_name == "nidaq_continuous_scan":
logger.info(f"Device {self.name} ready to be kicked off for nidaq_continuous_scan")
return None
status = CompareStatus(self.state, NidaqState.KICKOFF)
self.cancel_on_stop(status)
status.wait(timeout=self._timeout_wait_for_pv)
logger.info(
f"Device {self.name} ready to take data after pre_scan: {NidaqState(self.state.get())}"
)
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""
Called to inquire if a device has completed a scans.
For the NIDAQ we use this method to stop the backend since it
would not stop by itself in its current implementation since the number of points are not predefined.
"""
if not self._check_if_scan_name_is_valid(self.scan_parameters):
return None
status = CompareStatus(self.state, NidaqState.STANDBY)
self.cancel_on_stop(status)
if self.scan_parameters.scan_name != "nidaq_continuous_scan":
self.on_stop()
return status
def _progress_update(self, value, **kwargs) -> None:
"""Callback method to update the scan progress, runs a callback
to SUB_PROGRESS subscribers, i.e. BEC.
Args:
value (int) : current progress value
"""
if self.scan_parameters is None:
return
scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None)
if not isinstance(scan_duration, (int, float)):
return
value = scan_duration - value
max_value = scan_duration
self.progress_signal.put(value=value, max_value=max_value, done=bool(max_value == value))
def on_stop(self) -> None:
"""Called when the device is stopped."""
self.stop_call.put(1)
+60
View File
@@ -0,0 +1,60 @@
import enum
class NIDAQCompression(str, enum.Enum):
"""Options for Compression"""
OFF = 0
ON = 1
class ScanType(int, enum.Enum):
"""Triggering options of the backend"""
TRIGGERED = 0
CONTINUOUS = 1
class NidaqState(int, enum.Enum):
"""Possible States of the NIDAQ backend"""
DISABLED = 0
STANDBY = 1
STAGE = 2
KICKOFF = 3
ACQUIRE = 4
UNSTAGE = 5
class ScanRates(int, enum.Enum):
"""Sampling Rate options for the backend, in kHZ and MHz"""
HUNDRED_KHZ = 0
FIVE_HUNDRED_KHZ = 1
ONE_MHZ = 2
TWO_MHZ = 3
FOUR_MHZ = 4
FIVE_MHZ = 5
TEN_MHZ = 6
FOURTEEN_THREE_MHZ = 7
class ReadoutRange(int, enum.Enum):
"""ReadoutRange in +-V"""
ONE_V = 0
TWO_V = 1
FIVE_V = 2
TEN_V = 3
class EncoderFactors(int, enum.Enum):
"""Encoder Factors"""
X1_16 = 0
X1_8 = 1
X1_4 = 2
X1_2 = 3
X1 = 4
X2 = 5
X4 = 6
+704
View File
@@ -0,0 +1,704 @@
"""Pilatus AD integration at Debye beamline."""
from __future__ import annotations
import enum
import threading
import time
import traceback
from typing import TYPE_CHECKING, Tuple
import numpy as np
from bec_lib.file_utils import get_full_path
from bec_lib.logger import bec_logger
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO, Kind
from ophyd.areadetector.cam import ADBase, PilatusDetectorCam
from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin
from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin
from ophyd.status import WaitTimeoutError
from ophyd_devices import (
AndStatus,
CompareStatus,
DeviceStatus,
ExceptionStatus,
FileEventSignal,
PreviewSignal,
)
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from pydantic import BaseModel, Field
from debye_bec.devices.utils.utils import fetch_scan_info
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage
from bec_server.device_server.device_server import DeviceManagerDS
PILATUS_READOUT_TIME = 0.1 # in s
# PILATUS_ACQUIRE_TIME = (
# 999999 # This time is the timeout of the detector in operation mode, so it needs to be large.
# )
# pylint: disable=redefined-outer-name
# pylint: disable=raise-missing-from
logger = bec_logger.logger
class DETECTORSTATE(int, enum.Enum):
"""Pilatus Detector States from CamServer"""
UNARMED = 0
ARMED = 1
class ACQUIREMODE(int, enum.Enum):
"""Pilatus Acquisition Modes"""
DONE = 0
ACQUIRING = 1
class FILEWRITEMODE(int, enum.Enum):
"""HDF5 Plugin FileWrite Mode"""
SINGLE = 0
CAPTURE = 1
STREAM = 2
class COMPRESSIONALGORITHM(int, enum.Enum):
"""HDF5 Plugin Compression Algorithm"""
NONE = 0
NBIT = 1 # Don't use that..
SZIP = 2
ZLIB = 3
class TRIGGERMODE(int, enum.Enum):
"""Pilatus Trigger Modes"""
INTERNAL = 0
EXT_ENABLE = 1
EXT_TRIGGER = 2
MULT_TRIGGER = 3
ALIGNMENT = 4
class MONOTRIGGERSOURCE(int, enum.Enum):
""" "Mono XRD trigger source"""
EPICS = 0
INPOS = 1
class MONOTRIGGERMODE(int, enum.Enum):
""" "Mono XRD trigger mode"""
PULSE = 0
CONDITION = 1
def description(self) -> str:
"""Return a description of the trigger mode."""
descriptions = {
TRIGGERMODE.INTERNAL: "Internal trigger mode, images are acquired on internal trigger.",
TRIGGERMODE.EXT_ENABLE: "External Enable trigger mode; check manual as details are currently unknown",
TRIGGERMODE.EXT_TRIGGER: "External Trigger mode, images are acquired on external trigger signal. All images on single trigger.",
TRIGGERMODE.MULT_TRIGGER: "Multiple External Trigger mode, images are acquired on multiple external trigger signals. One image per trigger.",
TRIGGERMODE.ALIGNMENT: "Alignment mode, used for beam alignment.",
}
return descriptions.get(self, "Unknown")
def __str__(self):
return self.description()
class ScanParameter(BaseModel):
"""Dataclass to store the scan parameters for the Pilatus.
This needs to be in sync with the kwargs of the XRD related scans from Debye, to
ensure that the scan parameters are correctly set. Any changes in the scan kwargs,
i.e. renaming or adding new parameters, need to be represented here as well."""
scan_time: float | None = Field(None, description="Scan time for a half oscillation")
scan_duration: float | None = Field(None, description="Duration of the scan")
break_enable_low: bool | None = Field(
None, description="Break enabled for low, should be PV trig_ena_lo_enum"
) # trig_enable_low: bool = None
break_enable_high: bool | None = Field(
None, description="Break enabled for high, should be PV trig_ena_hi_enum"
) # trig_enable_high: bool = None
break_time_low: float | None = Field(None, description="Break time low energy/angle")
break_time_high: float | None = Field(None, description="Break time high energy/angle")
cycle_low: int | None = Field(None, description="Cycle for low energy/angle")
cycle_high: int | None = Field(None, description="Cycle for high energy/angle")
exp_time: float | None = Field(None, description="XRD trigger period")
n_of_trigger: int | None = Field(None, description="Amount of XRD triggers")
start: float | None = Field(None, description="Start value for energy/angle")
stop: float | None = Field(None, description="Stop value for energy/angle")
model_config: dict = {"validate_assignment": True}
class Pilatus(PSIDeviceBase, ADBase):
"""
Pilatus Base integration for Debye.
Prefix of the detector is 'X01DA-ES2-PIL:'
Args:
prefix (str) : Prefix for the IOC
name (str) : Name of the detector
scan_info (ScanInfo | None) : ScanInfo object passed through the device by the device_manager
device_manager (DeviceManager | None) : DeviceManager object passed through the device by the device_manager
"""
# USER_ACCESS = ["start_live_mode", "stop_live_mode"]
cam_gain_menu_string = Cpt(EpicsSignalRO, suffix="cam1:GainMenu", string=True)
_default_configuration_attrs = [
"cam.threshold_energy",
"cam.threshold_auto_apply",
"cam.gain_menu",
"cam_gain_menu_string",
"cam.pixel_cut_off",
"cam.acquire_time",
"cam.num_exposures",
"cam.model",
]
cam = Cpt(PilatusDetectorCam, "cam1:")
hdf = Cpt(HDF5Plugin, "HDF1:")
image1 = Cpt(ImagePlugin, "image1:")
filter_number = Cpt(
EpicsSignal, "cam1:FileNumber", kind=Kind.omitted, doc="File number for ramdisk"
)
trigger_shot = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_req",
write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_req",
add_prefix=("a",),
kind=Kind.omitted,
doc="Trigger PV from MO1 Bragg",
)
trigger_source = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_src_ENUM_RBV",
write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_src_ENUM",
add_prefix=("a",),
kind=Kind.omitted,
doc="Trigger Source; PV, 0 : EPICS, 1 : INPOS",
)
trigger_mode = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_mode_ENUM_RBV",
write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_mode_ENUM",
add_prefix=("a",),
kind=Kind.omitted,
doc="Trigger Mode; 0 : PULSE, 1 : CONDITION",
)
trigger_pulse_length = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_len_RBV",
write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_len",
add_prefix=("a",),
kind=Kind.omitted,
doc="Trigger Period in seconds",
)
trigger_period = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_trig_period_RBV",
write_pv="X01DA-OP-MO1:BRAGG:xrd_trig_period",
add_prefix=("a",),
kind=Kind.omitted,
doc="Trigger Pulse Length in seconds",
)
trigger_n_of = Cpt(
EpicsSignal,
read_pv="X01DA-OP-MO1:BRAGG:xrd_n_of_trig_RBV",
write_pv="X01DA-OP-MO1:BRAGG:xrd_n_of_trig",
add_prefix=("a",),
kind=Kind.omitted,
doc="Number of trigger to generate for each request",
)
preview = Cpt(
PreviewSignal,
name="preview",
ndim=2,
num_rotation_90=3,
doc="Preview signal for the Pilatus Detector",
)
file_event = Cpt(FileEventSignal, name="file_event")
def __init__(
self,
*,
name: str,
prefix: str = "",
scan_info: ScanInfo | None = None,
device_manager: DeviceManagerDS | None = None,
**kwargs,
):
super().__init__(
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
)
self.device_manager = device_manager
self._readout_time = PILATUS_READOUT_TIME
self._full_path = ""
self._poll_thread = threading.Thread(
target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread"
)
self._poll_thread_kill_event = threading.Event()
self._poll_rate = 1 # Poll rate in Hz
self.xas_xrd_scan_names = ["xas_simple_scan_with_xrd", "xas_advanced_scan_with_xrd"]
self.n_images = None
# self._live_mode_thread = threading.Thread(
# target=self._live_mode_loop, daemon=True, name=f"{self.name}_live_mode_thread"
# )
# self._live_mode_kill_event = threading.Event()
# self._live_mode_run_event = threading.Event()
# self._live_mode_stopped_event = threading.Event()
# self._live_mode_stopped_event.set() # Initial state is stopped
self.scan_parameters: ScanServerScanInfo = None
########################################
# Custom Beamline Methods #
########################################
def _poll_array_data(self):
"""Poll the array data for preview updates."""
while not self._poll_thread_kill_event.wait(1 / self._poll_rate):
try:
# logger.info(f"Running poll loop for {self.name}..")
value = self.image1.array_data.get()
if value is None:
continue
width = self.image1.array_size.width.get()
height = self.image1.array_size.height.get()
# Geometry correction for the image
data = np.reshape(value, (height, width))
last_image: DevicePreviewMessage = self.preview.get()
# logger.info(f"Preview image for {self.name} has shape {data.shape}")
if last_image is not None:
if np.array_equal(data, last_image.data):
# No update if image is the same, ~2.5ms on 2400x2400 image (6M)
logger.debug(
f"Pilatus preview image for {self.name} is the same as last one, not updating."
)
continue
logger.debug(f"Setting preview data for {self.name}")
self.preview.put(data)
except Exception: # pylint: disable=broad-except
content = traceback.format_exc()
logger.error(
f"Error while polling array data for preview of {self.name}: {content}"
)
# def start_live_mode(self, exp_time: float, n_images_max: int = 50000):
# """
# Start live mode with given exposure time.
# Args:
# exp_time (float) : Exposure time in seconds
# n_images_max (int): Maximum number of images to capture during live mode.
# Default is 5000. Only reset if needed.
# """
# if (
# self.cam.acquire.get() != ACQUIREMODE.DONE.value
# or self.hdf.capture.get() != ACQUIREMODE.DONE.value
# ):
# logger.warning(f"Can't start live mode, acquisition running on detector {self.name}.")
# return
# if self._live_mode_run_event.is_set():
# logger.warning(f"Live mode is already running on detector {self.name}.")
# return
# # Set relevant PVs
# self.cam.array_counter.set(0).wait(5) # Reset array counter
# self.cam.num_images.set(n_images_max).wait(5)
# logger.info(
# f"Setting exposure time to {exp_time} s for live mode on {self.name} with {n_images_max} images."
# )
# self.cam.acquire_time.set(exp_time - self._readout_time).wait(5)
# self.cam.acquire_period.set(exp_time).wait(5)
# status = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
# # It should suffice to make sure that self.hdf.capture is not set..
# self.cam.acquire.put(1) # Start measurement
# try:
# status.wait(10)
# except WaitTimeoutError:
# content = traceback.format_exc()
# raise RuntimeError(
# f"Live Mode on detector {self.name} did not stop: {content} after 10s."
# )
# self._live_mode_run_event.set()
# def _live_mode_loop(self, exp_time: float):
# while not self._live_mode_kill_event.is_set():
# self._live_mode_run_event.wait()
# self._live_mode_stopped_event.clear() # Clear stopped event
# time.sleep(self._readout_time) # make sure to wait for the readout_time
# n_images = self.cam.array_counter.get()
# status = CompareStatus(self.cam.array_counter, n_images + 1)
# self.trigger_shot.put(1)
# try:
# status.wait(60)
# except WaitTimeoutError:
# logger.warning(
# f"Live mode timeout exceeded for {self.name}. Continuing in live_mode_loop"
# )
# if self._live_mode_run_event.is_set():
# self._live_mode_stopped_event.set() # Set stopped event to indicate that live mode loop is stopped
# def stop_live_mode(self):
# """Stop live mode."""
# if self._live_mode_stopped_event.is_set():
# return
# status = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
# self.cam.acquire.put(0)
# self._live_mode_run_event.clear()
# if not self._live_mode_stopped_event.wait(10): # Wait until live mode loop is stopped
# logger.warning(f"Live mode did not stop in time for {self.name}.")
# try:
# status.wait(10)
# except WaitTimeoutError:
# content = traceback.format_exc()
# raise RuntimeError(
# f"Live Mode on detector {self.name} did not stop: {content} after 10s."
# )
def check_detector_stop_running_acquisition(self) -> AndStatus:
"""Check if the detector is still running an acquisition."""
status_acquire = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_writing = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
status = status_acquire & status_writing & status_cam_server
return status
def _calculate_trigger(self, scan_parameters: ScanServerScanInfo) -> Tuple[float, float]:
total_osc = 0
calc_duration = 0
total_trig_lo = 0
total_trig_hi = 0
# Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy
loc_break_enable_low = scan_parameters.additional_scan_parameters.get(
"break_enable_high", False
)
loc_break_time_low = scan_parameters.additional_scan_parameters.get("break_time_high", 0)
loc_cycle_low = scan_parameters.additional_scan_parameters.get("cycle_high", 1)
loc_break_enable_high = scan_parameters.additional_scan_parameters.get(
"break_enable_low", False
)
loc_break_time_high = scan_parameters.additional_scan_parameters.get("break_time_low", 0)
loc_cycle_high = scan_parameters.additional_scan_parameters.get("cycle_low", 1)
if not loc_break_enable_low:
loc_break_time_low = 0
loc_cycle_low = 1
if not loc_break_enable_high:
loc_break_time_high = 0
loc_cycle_high = 1
total_osc = scan_parameters.additional_scan_parameters.get("scan_duration", 0) / (
scan_parameters.additional_scan_parameters.get("scan_time", 0)
+ loc_break_time_low / (2 * loc_cycle_low)
+ loc_break_time_high / (2 * loc_cycle_high)
)
total_osc = np.ceil(total_osc)
total_osc = total_osc + total_osc % 2 # round up to the next even number
if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = (
total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0)
+ total_trig_lo * loc_break_time_low
+ total_trig_hi * loc_break_time_high
)
if calc_duration < scan_parameters.additional_scan_parameters.get("scan_duration", 0):
# Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers
total_osc = total_osc + 2
if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = (
total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0)
+ total_trig_lo * loc_break_time_low
+ total_trig_hi * loc_break_time_high
)
return total_trig_lo, total_trig_hi
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
try:
status_cam.wait(timeout=5)
status_hdf.wait(timeout=5)
except WaitTimeoutError:
logger.warning(
f"Camera device {self.name} was running an acquisition. Stopping acquisition."
)
self.cam.acquire.put(0)
self.hdf.capture.put(0)
self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5)
self.cam.image_file_tmot.set(60).wait(5)
self.hdf.file_write_mode.set(FILEWRITEMODE.STREAM.value).wait(5)
self.hdf.file_template.set("%s%s").wait(5)
self.hdf.auto_save.set(1).wait(5)
self.hdf.lazy_open.set(1).wait(5)
self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use
# Start polling thread...
self._poll_thread.start()
# Start live mode thread...
# self._live_mode_thread.start()
def on_stage(self) -> DeviceStatus | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info
(self.scan_info.msg) object.
"""
# self.stop_live_mode() # Make sure that live mode is stopped if scan runs
self.scan_parameters = fetch_scan_info(self.scan_info)
# If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition
if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value:
self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5)
if self.cam.acquire.get() == ACQUIREMODE.ACQUIRING.value:
self.cam.acquire.put(0)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam.wait(timeout=5)
if self.scan_parameters.scan_name in self.xas_xrd_scan_names:
# Compute number of triggers
total_trig_lo, total_trig_hi = self._calculate_trigger(self.scan_parameters)
# Set the number of images, we may also set this to a higher values if preferred and stop the acquisition
# TODO This logic is prone to errors, as we rely on the scans to nicely resolve to n_images. We should
# use here instead a way of settings the n_images independently of the scan parameters to avoid running out of sync
# with the complete method. Ideally we comput them in the scan itself.. This is much safer IMO!
self.n_images = (
total_trig_lo + total_trig_hi
) * self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1)
exp_time = self.scan_parameters.exp_time
self.trigger_source.set(MONOTRIGGERSOURCE.INPOS).wait(5)
self.trigger_n_of.set(
self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1)
).wait(5)
# TODO migrate logic to v4 once old scans are deprecated,
# TODO if num_points=None and no logic from scan_name applies, can't measure with this detector..
elif self.scan_parameters.scan_type == "software_triggered":
self.n_images = (
self.scan_parameters.num_monitored_readouts
* self.scan_parameters.frames_per_trigger
)
exp_time = self.scan_parameters.exp_time
self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5)
self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition
else:
# TODO how to deal with fly scans?
return None
# Common settings
self.trigger_mode.set(MONOTRIGGERMODE.PULSE).wait(5)
self.trigger_period.set(exp_time).wait(5)
self.trigger_pulse_length.set(0.005).wait(
5
) # Pulse length of 5 ms enough for Pilatus and NIDAQ
if exp_time - self._readout_time <= 0:
raise ValueError(
(
f"Exposure time {exp_time} is too short ",
f"for Pilatus with readout_time {self._readout_time}.",
)
)
detector_exp_time = exp_time - self._readout_time
self._full_path = get_full_path(self.scan_info.msg, name="pilatus")
file_path = "/".join(self._full_path.split("/")[:-1])
file_name = self._full_path.split("/")[-1]
# Prepare detector and backend
self.cam.array_callbacks.set(1).wait(5) # Enable array callbacks
self.hdf.enable.set(1).wait(5) # Enable HDF5 plugin
# Camera settings
self.cam.num_exposures.set(1, timeout=5).wait()
self.cam.num_images.set(self.n_images, timeout=5).wait()
self.cam.acquire_time.set(detector_exp_time, timeout=5).wait() # let's try this
self.cam.acquire_period.set(exp_time, timeout=5).wait()
self.filter_number.set(0, timeout=5).wait()
# HDF5 settings
logger.debug(
f"Setting HDF5 file path to {file_path} and file name to {file_name}. full_path is {self._full_path}"
)
self.hdf.file_path.set(file_path).wait(5)
self.hdf.file_name.set(file_name).wait(5)
self.hdf.num_capture.set(self.n_images).wait(5)
self.cam.array_counter.set(0).wait(5) # Reset array counter
self.file_event.put(
file_path=self._full_path,
done=False,
successful=False,
hinted_h5_entries={"data": "/entry/data/data"},
)
def on_unstage(self) -> None:
"""Called while unstaging the device."""
def on_pre_scan(self) -> DeviceStatus | None:
"""Called right before the scan starts on all devices automatically."""
if (
self.scan_parameters.scan_name in self.xas_xrd_scan_names
or self.scan_parameters.scan_type == "software_triggered"
): # TODO how to deal with fly scans?
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value)
status = status_hdf & status_cam & status_cam_server
self.cam.acquire.put(1)
self.hdf.capture.put(1)
self.cancel_on_stop(status)
return status
else:
return None
def on_trigger(self) -> DeviceStatus | None:
"""Called when the device is triggered."""
if not self.scan_parameters.scan_type == "software_triggered":
return None
start_time = time.time()
img_counter = self.hdf.num_captured.get()
logger.debug(f"Triggering image with num_captured {img_counter}")
status = CompareStatus(self.hdf.num_captured, img_counter + 1)
logger.debug(f"Triggering took image {time.time() - start_time:.3f} seconds")
self.trigger_shot.put(1)
self.cancel_on_stop(status)
return status
def _complete_callback(self, status: DeviceStatus):
"""Callback for when the device completes a scan."""
if (
self.scan_parameters.scan_name in self.xas_xrd_scan_names
or self.scan_parameters.scan_type == "software_triggered"
): # TODO how to deal with fly scans?
if status.success:
self.file_event.put(
file_path=self._full_path,
done=True,
successful=True,
hinted_h5_entries={"data": "/entry/data/data"},
)
else:
self.file_event.put(
file_path=self._full_path,
done=True,
successful=False,
hinted_h5_entries={"data": "/entry/data/data"},
)
else:
return None
def on_complete(self) -> DeviceStatus | None:
"""Called to inquire if a device has completed a scans."""
if (
self.scan_parameters.scan_name in self.xas_xrd_scan_names
or self.scan_parameters.scan_type == "software_triggered"
): # TODO how to deal with fly scans?
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
# status_write_error = ExceptionStatus(self.hdf.write_status, 0, operation="!=")
if self.scan_parameters.scan_name in self.xas_xrd_scan_names:
# For long scans, it can be that the mono will execute one cycle more,
# meaning a few more XRD triggers will be sent
status_img_written = CompareStatus(
self.hdf.num_captured, self.n_images, operation_success=">="
)
else:
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
status = (
status_hdf & status_cam & status_img_written & status_cam_server
) # & status_write_error
status.add_callback(self._complete_callback) # Callback that writing was successful
self.cancel_on_stop(status)
return status
else:
return None
def on_kickoff(self) -> 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.cam.acquire.put(0)
self.hdf.capture.put(0)
def on_destroy(self) -> None:
"""Called when the device is destroyed. Cleanup resources here."""
self._poll_thread_kill_event.set()
# TODO do we need to clean the poll thread ourselves?
self.on_stop()
if __name__ == "__main__":
try:
pilatus = Pilatus(name="pilatus", prefix="X01DA-ES2-PIL:")
logger.info("Calling wait for connection")
# pilatus.wait_for_connection(all_signals=True, timeout=20)
logger.info("Connecting to pilatus...")
pilatus.on_connected()
for exp_time, scan_number, n_pnts in zip([0.5, 1.0, 2.0], [1, 2, 3], [30, 20, 10]):
logger.info("Sleeping for 5s")
time.sleep(5)
pilatus.scan_info.msg.num_points = n_pnts
pilatus.scan_info.msg.scan_parameters["exp_time"] = exp_time
pilatus.scan_info.msg.scan_parameters["frames_per_trigger"] = 1
pilatus.scan_info.msg.info["file_components"] = (
f"/sls/x01da/data/p22481/raw/data/S00000-00999/S{scan_number:05d}/S{scan_number:05d}",
"h5",
)
pilatus.on_stage()
logger.info("Stage done")
pilatus.on_pre_scan().wait(timeout=5)
logger.info("Pre-scan done")
for ii in range(pilatus.scan_info.msg.num_points):
# if ii == 0:
# time.sleep(1)
logger.info(f"Triggering image {ii+1}/{pilatus.scan_info.msg.num_points}")
pilatus.on_trigger().wait()
p = pilatus.preview.get()
if p is not None:
p: DevicePreviewMessage
logger.warning(
f"Preview shape: {p.data.shape}, max: {np.max(p.data)}, min: {np.min(p.data)}, mean: {np.mean(p.data)}"
)
pilatus.on_complete().wait(timeout=5)
logger.info("Complete done")
pilatus.on_unstage()
logger.info("Unstage done")
finally:
pilatus.on_destroy()
+100
View File
@@ -0,0 +1,100 @@
"""ES2 Pilatus Curtain"""
from __future__ import annotations
import enum
from typing import TYPE_CHECKING
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO
from ophyd_devices import CompareStatus, DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
class PilatusCurtainError(Exception):
"""PilatusCurtain specific exception"""
class COVER(int, enum.Enum):
"""Pilatus Curtain States"""
# TODO What are the proper states here? - Probably enums for the states are better.
OPEN = 0
CLOSED = 0
ERROR = 1
class PilatusCurtain(PSIDeviceBase):
"""Class for the ES2 Pilatus Curtain"""
USER_ACCESS = ["open", "close"]
open_cover = Cpt(EpicsSignal, suffix="OpenCover", kind="config", doc="Open Cover")
close_cover = Cpt(EpicsSignal, suffix="CloseCover", kind="config", doc="Close Cover")
cover_is_closed = Cpt(
EpicsSignalRO, suffix="CoverIsClosed", kind="config", doc="Cover is closed"
)
cover_is_open = Cpt(EpicsSignalRO, suffix="CoverIsOpen", kind="config", doc="Cover is open")
cover_is_moving = Cpt(
EpicsSignalRO, suffix="CoverIsMoving", kind="config", doc="Cover is moving"
)
cover_error = Cpt(EpicsSignalRO, suffix="CoverError", kind="config", doc="Cover error")
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.timeout_for_pvwait = 30
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
if self.cover_error.get() == COVER.ERROR:
raise PilatusCurtainError("Pilatus Curtain is in an error state!")
def on_stage(self) -> DeviceStatus | None:
"""Called while staging the device."""
return self.open()
def on_unstage(self) -> DeviceStatus | None:
"""Called while unstaging the device."""
# return self.close()
def on_stop(self) -> DeviceStatus | None:
"""Called when the device is stopped."""
# return self.close()
def open(self) -> DeviceStatus | None:
"""Open the cover"""
if self.cover_is_closed.get() == COVER.CLOSED:
self.open_cover.put(1)
# TODO timeout ok?
status_open = CompareStatus(self.cover_is_open, COVER.OPEN, timeout=5)
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation_success="!=")
status = status_open & status_error
return status
else:
return None
def close(self) -> DeviceStatus | None:
"""Close the cover"""
if self.cover_is_open.get() == COVER.OPEN:
self.close_cover.put(1)
# TODO timeout ok?
status_close = CompareStatus(self.cover_is_closed, COVER.CLOSED, timeout=5)
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation_success="!=")
status = status_close & status_error
return status
else:
return None
+200
View File
@@ -0,0 +1,200 @@
"""ES2 Reference Foil Changer"""
from __future__ import annotations
import enum
from typing import TYPE_CHECKING
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd.status import DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.utils.errors import DeviceStopError
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
class Status(int, enum.Enum):
"""Enum class for the status field"""
BOOT = 0
RETRACTED = 1
INSERTED = 2
MOVING = 3
ERROR = 4
class OpMode(int, enum.Enum):
"""Enum class for the Operating Mode field"""
USERMODE = 0
MAINTENANCEMODE = 1
DIAGNOSTICMODE = 2
ERRORMODE = 3
class Reffoilchanger(PSIDeviceBase):
"""Class for the ES2 Reference Foil Changer"""
USER_ACCESS = ["insert"]
inserted = Cpt(
EpicsSignalRO, suffix="ES2-REF:TRY-FilterInserted", kind="config", doc="Inserted indicator"
)
retracted = Cpt(
EpicsSignalRO,
suffix="ES2-REF:TRY-FilterRetracted",
kind="config",
doc="Retracted indicator",
)
moving = Cpt(EpicsSignalRO, suffix="ES2-REF:ROTY.MOVN", kind="config", doc="Moving indicator")
status = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status"
)
status_string = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status", string=True
)
op_mode = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status"
)
op_mode_string = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status", string=True
)
ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference")
ref_rb = Cpt(
EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference"
)
foil01 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL01.DESC", kind="config", doc="Foil 01")
foil02 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL02.DESC", kind="config", doc="Foil 02")
foil03 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL03.DESC", kind="config", doc="Foil 03")
foil04 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL04.DESC", kind="config", doc="Foil 04")
foil05 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL05.DESC", kind="config", doc="Foil 05")
foil06 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL06.DESC", kind="config", doc="Foil 06")
foil07 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL07.DESC", kind="config", doc="Foil 07")
foil08 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL08.DESC", kind="config", doc="Foil 08")
foil09 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL09.DESC", kind="config", doc="Foil 09")
foil10 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL10.DESC", kind="config", doc="Foil 10")
foil11 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL11.DESC", kind="config", doc="Foil 11")
foil12 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL12.DESC", kind="config", doc="Foil 12")
foil13 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL13.DESC", kind="config", doc="Foil 13")
foil14 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL14.DESC", kind="config", doc="Foil 14")
foil15 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL15.DESC", kind="config", doc="Foil 15")
foil16 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL16.DESC", kind="config", doc="Foil 16")
foil17 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL17.DESC", kind="config", doc="Foil 17")
foil18 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL18.DESC", kind="config", doc="Foil 18")
foil19 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL19.DESC", kind="config", doc="Foil 19")
foil20 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL20.DESC", kind="config", doc="Foil 20")
foil21 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL21.DESC", kind="config", doc="Foil 21")
foil22 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL22.DESC", kind="config", doc="Foil 22")
foil23 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL23.DESC", kind="config", doc="Foil 23")
foil24 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL24.DESC", kind="config", doc="Foil 24")
foil25 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL25.DESC", kind="config", doc="Foil 25")
foil26 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL26.DESC", kind="config", doc="Foil 26")
foil27 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL27.DESC", kind="config", doc="Foil 27")
foil28 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL28.DESC", kind="config", doc="Foil 28")
foil29 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL29.DESC", kind="config", doc="Foil 29")
foil30 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL30.DESC", kind="config", doc="Foil 30")
foil31 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL31.DESC", kind="config", doc="Foil 31")
foil32 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL32.DESC", kind="config", doc="Foil 32")
foil33 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL33.DESC", kind="config", doc="Foil 33")
foil34 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL34.DESC", kind="config", doc="Foil 34")
foil35 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL35.DESC", kind="config", doc="Foil 35")
foil36 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL36.DESC", kind="config", doc="Foil 36")
foil37 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL37.DESC", kind="config", doc="Foil 37")
foil38 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL38.DESC", kind="config", doc="Foil 38")
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.foils = [
self.foil01,
self.foil02,
self.foil03,
self.foil04,
self.foil05,
self.foil06,
self.foil07,
self.foil08,
self.foil09,
self.foil10,
self.foil11,
self.foil12,
self.foil13,
self.foil14,
self.foil15,
self.foil16,
self.foil17,
self.foil18,
self.foil19,
self.foil20,
self.foil21,
self.foil22,
self.foil23,
self.foil24,
self.foil25,
self.foil26,
self.foil27,
self.foil28,
self.foil29,
self.foil30,
self.foil31,
self.foil32,
self.foil33,
self.foil34,
self.foil35,
self.foil36,
self.foil37,
self.foil38,
]
def insert(self, ref: str, wait: bool = False) -> DeviceStatus:
"""Insert a reference
Args:
ref (str) : Desired reference foil name, e.g. Fe or Pt
wait (bool): If you like to wait for the filling to finish. Default False.
"""
filter_number = -1
for i, foil in enumerate(self.foils):
if foil.get() == ref:
filter_number = i + 1
break
if filter_number == -1:
raise ValueError(f"Requested foil ({ref}) is not in list of available foils")
if self.op_mode.get() == OpMode.USERMODE:
self.ref_set.put(filter_number)
def wait_for_status():
return (
(self.status.get() == Status.RETRACTED)
or (self.status.get() == Status.MOVING)
or (
self.ref_rb.get() < (filter_number + 0.2)
and self.ref_rb.get() > (filter_number - 0.2)
)
)
timeout = 3
if not self.wait_for_condition(wait_for_status, timeout=timeout, check_stopped=True):
raise TimeoutError(
f"Reference foil changer did not retract the current foil within {timeout}s"
)
def wait_for_change_finished():
return self.status.get() == Status.INSERTED and self.op_mode == OpMode.USERMODE
# Wait until new reference foil is inserted
status = self.task_handler.submit_task(
task=self.wait_for_condition, task_args=(wait_for_change_finished, 5, True)
)
if wait:
status.wait()
return status
else:
raise DeviceStopError(
f"Reference foil changer must be in User Mode but is in {self.op_mode.get(as_string=True)}"
)
View File
+26
View File
@@ -0,0 +1,26 @@
"""Utility functions for the devices."""
from copy import deepcopy
import numpy as np
from bec_lib.devicemanager import ScanInfo
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from pydantic import ValidationError
def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo:
"""Fetch the scan parameters from the scan_info object and return them as a ScanServerScanInfo object."""
info = scan_info.msg.info
if isinstance(info["positions"], list):
info["positions"] = np.array(info["positions"])
try:
msg = ScanServerScanInfo.model_validate(info)
except ValidationError: # This means we have an old scan_info object.
info = deepcopy(info)
# We need to convert a few parameters manually.
info["scan_type"] = (
"hardware_triggered" if info["scan_type"] == "fly" else "software_triggered"
)
msg = ScanServerScanInfo.model_validate(info)
return msg
+1
View File
@@ -0,0 +1 @@
from .debye_nexus_structure import DebyeNexusStructure
@@ -0,0 +1,403 @@
from bec_server.file_writer.default_writer import DefaultFormat
import debye_bec.bec_widgets.widgets.digital_twin.x01da_parameters as bl
class DebyeNexusStructure(DefaultFormat):
"""Nexus Structure for Debye"""
def format(self) -> None:
"""Specify the file format for the file writer."""
entry = self.storage.create_group(name="entry")
entry.attrs["NX_class"] = "NXentry"
instrument = entry.create_group(name="instrument")
instrument.attrs["NX_class"] = "NXinstrument"
##################
## source specific information
###################
source = instrument.create_group(name="source")
source.attrs["NX_class"] = "NXsource"
beamline_name = source.create_dataset(name="beamline_name", data="Debye")
beamline_name.attrs["NX_class"] = "NX_CHAR"
facility_name = source.create_dataset(name="facility_name", data="Swiss Light Source")
facility_name.attrs["NX_class"] = "NX_CHAR"
probe = source.create_dataset(name="probe", data="X-ray")
probe.attrs["NX_class"] = "NX_CHAR"
if "curr" in self.device_manager.devices:
ring_current = source.create_soft_link(
name="ring_current", target="/entry/collection/devices/curr/curr/value"
)
ring_current.attrs["NX_class"] = "NX_FLOAT"
ring_current.attrs["units"] = "mA"
###################
## mo1_bragg specific information
###################
## Logic if device exist
if "mo1_bragg" in self.device_manager.devices:
monochromator = instrument.create_group(name="monochromator")
monochromator.attrs["NX_class"] = "NXmonochromator"
crystal = monochromator.create_group(name="crystal")
crystal.attrs["NX_class"] = "NXcrystal"
# Create a dataset
chemical_formular = crystal.create_dataset(name="chemical_formular", data="Si")
chemical_formular.attrs["NX_class"] = "NX_CHAR"
reflection = crystal.create_soft_link(
name="reflection",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value",
)
reflection.attrs["NX_class"] = "NX_CHAR"
# Create a softlink
d_spacing = crystal.create_soft_link(
name="d_spacing",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value",
)
d_spacing.attrs["NX_class"] = "NX_FLOAT"
d_spacing.attrs["units"] = "angstrom"
bragg_offset = crystal.create_soft_link(
name="bragg_offset",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_bragg_off/value",
)
bragg_offset.attrs["NX_class"] = "NX_FLOAT"
bragg_offset.attrs["units"] = "degree"
phi_offset = crystal.create_soft_link(
name="phi_offset",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_phi_off/value",
)
phi_offset.attrs["NX_class"] = "NX_FLOAT"
phi_offset.attrs["units"] = "degree"
## Logic if device exist
if "mo1_roty" in self.device_manager.devices:
# Create a softlink
azimuthal_angle = crystal.create_soft_link(
name="azimuthal_angle",
target="/entry/collection/devices/mo1_roty/mo1_roty/value",
)
azimuthal_angle.attrs["NX_class"] = "NX_FLOAT"
azimuthal_angle.attrs["units"] = "degree"
azm_offset = crystal.create_soft_link(
name="azm_offset",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_azm_off/value",
)
azm_offset.attrs["NX_class"] = "NX_FLOAT"
azm_offset.attrs["units"] = "degree"
miscut = crystal.create_soft_link(
name="miscut",
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_miscut/value",
)
miscut.attrs["NX_class"] = "NX_FLOAT"
miscut.attrs["units"] = "degree"
###################
### cm mirror specific information
####################
collimating_mirror = instrument.create_group(name="collimating_mirror")
collimating_mirror.attrs["NX_class"] = "NXmirror"
cm_substrate_material = collimating_mirror.create_dataset(
name="substrate_material", data="Si"
)
cm_substrate_material.attrs["NX_class"] = "NX_CHAR"
# previous error due to space in name field
if "cm_bnd_radius" in self.device_manager.devices:
cm_bending_radius = collimating_mirror.create_soft_link(
name="sagittal_radius",
target="/entry/collection/devices/cm_bnd_radius/cm_bnd_radius/value",
)
cm_bending_radius.attrs["NX_class"] = "NX_FLOAT"
cm_bending_radius.attrs["units"] = "km"
if "cm_rotx" in self.device_manager.devices:
cm_incidence_angle = collimating_mirror.create_soft_link(
name="incidence_angle", target="/entry/collection/devices/cm_rotx/cm_rotx/value"
)
cm_incidence_angle.attrs["NX_class"] = "NX_FLOAT"
cm_incidence_angle.attrs["units"] = "mrad"
if "cm_roty" in self.device_manager.devices:
cm_yaw_angle = collimating_mirror.create_soft_link(
name="yaw_angle", target="/entry/collection/devices/cm_roty/cm_roty/value"
)
cm_yaw_angle.attrs["NX_class"] = "NX_FLOAT"
cm_yaw_angle.attrs["units"] = "mrad"
if "cm_rotz" in self.device_manager.devices:
cm_roll_angle = collimating_mirror.create_soft_link(
name="roll_angle", target="/entry/collection/devices/cm_rotz/cm_rotz/value"
)
cm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
cm_roll_angle.attrs["units"] = "mrad"
if "cm_trx" in self.device_manager.devices:
cm_trx = (
-self.device_manager.devices.cm_trx.read(cached=True).get("cm_trx").get("value")
)
stripe = "Unknown"
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
if low <= cm_trx <= high:
stripe = name
cm_stripe = collimating_mirror.create_dataset(name="stripe", data=stripe)
cm_stripe.attrs["NX_class"] = "NX_CHAR"
###################
### fm mirror specific information
####################
focusing_mirror = instrument.create_group(name="focusing_mirror")
focusing_mirror.attrs["NX_class"] = "NXmirror"
fm_substrate_material = focusing_mirror.create_dataset(name="substrate_material", data="Si")
fm_substrate_material.attrs["NX_class"] = "NX_CHAR"
if "fm_bnd_radius" in self.device_manager.devices:
fm_bending_radius = focusing_mirror.create_soft_link(
name="sagittal_radius",
target="/entry/collection/devices/fm_bnd_radius/fm_bnd_radius/value",
)
fm_bending_radius.attrs["NX_class"] = "NX_FLOAT"
fm_bending_radius.attrs["units"] = "km"
if "fm_rotx" in self.device_manager.devices:
fm_incidence_angle = focusing_mirror.create_soft_link(
name="incidence_angle", target="/entry/collection/devices/fm_rotx/fm_rotx/value"
)
fm_incidence_angle.attrs["NX_class"] = "NX_FLOAT"
fm_incidence_angle.attrs["units"] = "mrad"
if "fm_roty" in self.device_manager.devices:
fm_yaw_angle = focusing_mirror.create_soft_link(
name="yaw_angle", target="/entry/collection/devices/fm_roty/fm_roty/value"
)
fm_yaw_angle.attrs["NX_class"] = "NX_FLOAT"
fm_yaw_angle.attrs["units"] = "mrad"
if "fm_rotz" in self.device_manager.devices:
fm_roll_angle = focusing_mirror.create_soft_link(
name="roll_angle", target="/entry/collection/devices/fm_rotz/fm_rotz/value"
)
fm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
fm_roll_angle.attrs["units"] = "mrad"
if "fm_trx" in self.device_manager.devices:
fm_trx = (
-self.device_manager.devices.fm_trx.read(cached=True).get("fm_trx").get("value")
)
stripe = "Unknown"
for name, low, high in zip(
bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]
):
if low <= fm_trx <= high:
stripe = name + " (flat)"
for name, low, high in zip(
bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]
):
if low <= fm_trx <= high:
stripe = name + " (toroid)"
fm_stripe = focusing_mirror.create_dataset(name="stripe", data=stripe)
fm_stripe.attrs["NX_class"] = "NX_CHAR"
###################
## nidaq specific information
###################
## Logic if device exist
if "nidaq" in self.device_manager.devices:
# ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value")
ai_chans_bits = (
self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value")
)
ci_chans_bits = (
self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value")
)
# add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value")
add_chans_bits = (
self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
)
rle = self.configuration.get("nidaq", {}).get("nidaq_rle", {}).get("value")
measurement_mode = entry.create_group(name="mode")
measurement_mode.attrs["NX_class"] = "NX_CHAR"
if ci_chans_bits is not None:
if (int(ci_chans_bits) & 0x7F) != 0:
# Create a dataset
rayspec_sdd_active = measurement_mode.create_group(
name="Multi_Element_Partial_Fluorescence_Yield"
)
me_sdd = rayspec_sdd_active.create_dataset(
name="Detector", data="Rayspec 7 element Silicon Drift Detector"
)
me_sdd.attrs["NX_class"] = "NX_CHAR"
if (int(ci_chans_bits) & (1 << 8)) != 0:
# Create a dataset
ketek_sdd_active = measurement_mode.create_group(
name="Single_Element_Partial_Fluorescence_Yield"
)
se_sdd = ketek_sdd_active.create_dataset(
name="Detector", data="Ketex mini single element Silicon Drift Detector"
)
se_sdd.attrs["NX_class"] = "NX_CHAR"
if ai_chans_bits is not None:
if (int(ai_chans_bits) & (1 << 6)) != 0:
# Create a dataset
pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield")
tfy = pips_active.create_dataset(
name="Detector",
data="Mirion Technologies Partially Depeleted PIPS Detector",
)
tfy.attrs["NX_class"] = "NX_CHAR"
if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0):
# Create a dataset
ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission")
sam_trans = ai0ai2_active.create_dataset(
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
)
sam_trans.attrs["NX_class"] = "NX_CHAR"
if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0):
# Create a dataset
ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission")
ref_trans = ai2ai4_active.create_dataset(
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
)
ref_trans.attrs["NX_class"] = "NX_CHAR"
main_data = entry.create_group(name="data")
main_data.attrs["NX_class"] = "NXdata"
##################
## energy, test whether the signal exists. how to check from config?
###################
energy = main_data.create_group(name="energy")
energy.attrs["NX_class"] = "NXdata"
energy.attrs["units"] = "eV"
main_data.create_soft_link(
name="energy",
target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value",
)
##################
## i0
###################
if (int(ai_chans_bits) & (1 << 0)) != 0:
i0 = main_data.create_group(name="i0")
i0.attrs["NX_class"] = "NXdata"
i0.attrs["units"] = "V"
if rle:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value"
else:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0/value"
main_data.create_soft_link(name="i0", target=target)
##################
## i1
###################
if (int(ai_chans_bits) & (1 << 2)) != 0:
i1 = main_data.create_group(name="i1")
i1.attrs["NX_class"] = "NXdata"
i1.attrs["units"] = "V"
if rle:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value"
else:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2/value"
main_data.create_soft_link(name="i1", target=target)
##################
## i2
###################
if (int(ai_chans_bits) & (1 << 4)) != 0:
i2 = main_data.create_group(name="i2")
i2.attrs["NX_class"] = "NXdata"
i2.attrs["units"] = "V"
if rle:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value"
else:
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4/value"
main_data.create_soft_link(name="i2", target=target)
##################
## ci sum
###################
if int(ci_chans_bits) > 0:
ci_sum = main_data.create_group(name="Fluorescence_Sum")
ci_sum.attrs["NX_class"] = "NXdata"
ci_sum.attrs["units"] = "counts"
main_data.create_soft_link(
name="Fluorescence_Sum",
target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value",
)
##################
## mu sample, test whether the signal exists. how to check from config?
###################
if (int(add_chans_bits) & (1 << 0)) != 0:
mu_sample = main_data.create_group(name="mu_sample")
mu_sample.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="mu_sample",
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value",
)
##################
## fluo sample, test whether the signal exists. how to check from config?
###################
if (int(add_chans_bits) & (1 << 1)) != 0:
mu_sample = main_data.create_group(name="fluo_sample")
mu_sample.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="fluo_sample",
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value",
)
##################
## mu reference, test whether the signal exists. how to check from config?
###################
if (int(add_chans_bits) & (1 << 2)) != 0:
mu_reference = main_data.create_group(name="mu_reference")
mu_reference.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="mu_reference",
target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value",
)
+6
View File
@@ -0,0 +1,6 @@
# Macros
This directory is intended to store macros which will be loaded automatically when starting BEC.
Macros are small functions to make repetitive tasks easier. Functions defined in python files in this directory will be accessible from the BEC console.
Please do not put any code outside of function definitions here. If you wish for code to be automatically run when starting BEC, see the startup script at debye_bec/bec_ipython_client/startup/post_startup.py
For a guide on writing macros, please see: https://bec.readthedocs.io/en/latest/user/command_line_interface.html#how-to-write-a-macro
View File
+7 -1
View File
@@ -1 +1,7 @@
from .mono_bragg_scans import XASSimpleScan, XASSimpleScanWithXRD
from .nidaq_continuous_scan import NidaqContinuousScan
from .xas_simple_scan import (
XasAdvancedScan,
XasAdvancedScanWithXrd,
XasSimpleScan,
XasSimpleScanWithXrd,
)
@@ -0,0 +1,12 @@
# from .metadata_schema_xas_simple_scan import xas_simple_scan_schema
METADATA_SCHEMA_REGISTRY = { # "xas_simple_scan": xas_simple_scan_schema
# Add models which should be used to validate scan metadata here.
# Make a model according to the template, and import it as above
# Then associate it with a scan like so:
# "example_scan": ExampleSchema
}
# Define a default schema type which should be used as the fallback for everything:
DEFAULT_SCHEMA = None
@@ -0,0 +1,34 @@
# # By inheriting from BasicScanMetadata you can define a schema by which metadata
# # supplied to a scan must be validated.
# # This schema is a Pydantic model: https://docs.pydantic.dev/latest/concepts/models/
# # but by default it will still allow you to add any arbitrary information to it.
# # That is to say, when you run a scan with which such a model has been associated in the
# # metadata_schema_registry, you can supply any python dictionary with strings as keys
# # and built-in python types (strings, integers, floats) as values, and these will be
# # added to the experiment metadata, but it *must* contain the keys and values of the
# # types defined in the schema class.
# #
# #
# # For example, say that you would like to enforce recording information about sample
# # pretreatment, you could define the following:
# #
#
# from bec_lib.metadata_schema import BasicScanMetadata
#
#
# class ExampleSchema(BasicScanMetadata):
# treatment_description: str
# treatment_temperature_k: int
#
#
# # If this was used according to the example in metadata_schema_registry.py,
# # then when calling the scan, the user would need to write something like:
# >>> scans.example_scan(
# >>> motor,
# >>> 1,
# >>> 2,
# >>> 3,
# >>> metadata={"treatment_description": "oven overnight", "treatment_temperature_k": 575},
# >>> )
#
# # And the additional metadata would be saved in the HDF5 file created for the scan.
@@ -0,0 +1,8 @@
from bec_lib.metadata_schema import BasicScanMetadata
#
#
class xas_simple_scan_schema(BasicScanMetadata):
Edge: str
Element: str
-167
View File
@@ -1,167 +0,0 @@
""" This module contains the scan classes for the mono bragg motor of the Debye beamline."""
import time
import numpy as np
from bec_lib.device import DeviceBase
from bec_server.scan_server.scans import AsyncFlyScanBase
class XASSimpleScan(AsyncFlyScanBase):
scan_name = "xas_simple_scan"
scan_type = "fly"
scan_report_hint = "device_progress"
required_kwargs = []
use_scan_progress_report = False
pre_move = False
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration"],
}
def __init__(
self,
start: float,
stop: float,
scan_time: float,
scan_duration: float,
motor: DeviceBase = "mo1_bragg",
**kwargs,
):
"""The xas_simple_scan is used to start a simple oscillating scan on the mono bragg motor.
Start and Stop define the energy range for the scan, scan_time is the time for one scan cycle and scan_duration
is the duration of the scan. If scan duration is set to 0, the scan will run infinitely.
Args:
start (float): Start energy for the scan.
stop (float): Stop energy for the scan.
scan_time (float): Time for one scan cycle.
scan_duration (float): Duration of the scan.
motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg".
Examples:
>>> scans.xas_simple_scan(start=8000, stop=9000, scan_time=1, scan_duration=10)
"""
super().__init__(**kwargs)
self.motor = motor
self.start = start
self.stop = stop
self.scan_time = scan_time
self.scan_duration = scan_duration
self.primary_readout_cycle = 1
def prepare_positions(self):
"""Prepare the positions for the scan.
Use here only start and end energy defining the range for the scan.
"""
self.positions = np.array([self.start, self.stop], dtype=float)
self.num_pos = None
yield None
def pre_scan(self):
"""Pre Scan action. Ensure the motor movetype is set to energy, then check limits for start/end energy.
#TODO Remove once the motor movetype is removed and ANGLE motion is a pseudo motor.
"""
yield from self.stubs.send_rpc_and_wait(self.motor, "move_type.set", "energy")
self._check_limits()
# Ensure parent class pre_scan actions to be called.
super().pre_scan()
def scan_report_instructions(self):
"""
Return the instructions for the scan report.
"""
yield from self.stubs.scan_report_instruction({"device_progress": [self.motor]})
def scan_core(self):
"""Run the scan core.
Kickoff the oscillation on the Bragg motor and wait for the completion of the motion.
"""
# Start the oscillation on the Bragg motor.
yield from self.stubs.kickoff(device=self.motor)
yield from self.stubs.complete(device=self.motor)
# Get the target DIID (instruction number) for the stubs.complete call
target_diid = self.DIID - 1
while True:
# Readout monitored devices
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
# Check if complete call on Mo1 Bragg has been finished
status = self.stubs.get_req_status(
device=self.motor, RID=self.metadata["RID"], DIID=target_diid
)
if status:
break
time.sleep(self.primary_readout_cycle)
self.point_id += 1
self.num_pos = self.point_id + 1
class XASSimpleScanWithXRD(XASSimpleScan):
scan_name = "xas_simple_scan_with_xrd"
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration"],
"Low Energy Range": ["xrd_enable_low", "num_trigger_low", "exp_time_low", "cycle_low"],
"High Energy Range": ["xrd_enable_high", "num_trigger_high", "exp_time_high", "cycle_high"],
}
def __init__(
self,
start: float,
stop: float,
scan_time: float,
scan_duration: float,
xrd_enable_low: bool,
num_trigger_low: int,
exp_time_low: float,
cycle_low: int,
xrd_enable_high: bool,
num_trigger_high: int,
exp_time_high: float,
cycle_high: float,
motor: DeviceBase = "mo1_bragg",
**kwargs,
):
"""The xas_simple_scan_with_xrd is an oscillation motion on the mono motor
with XRD triggering at low and high energy ranges.
If scan duration is set to 0, the scan will run infinitely.
Args:
start (float): Start energy for the scan.
stop (float): Stop energy for the scan.
scan_time (float): Time for one oscillation .
scan_duration (float): Total duration of the scan.
xrd_enable_low (bool): Enable XRD triggering for the low energy range.
num_trigger_low (int): Number of triggers for the low energy range.
exp_time_low (float): Exposure time for the low energy range.
cycle_low (int): Specify how often the triggers should be considered, every nth cycle for low
xrd_enable_high (bool): Enable XRD triggering for the high energy range.
num_trigger_high (int): Number of triggers for the high energy range.
exp_time_high (float): Exposure time for the high energy range.
cycle_high (int): Specify how often the triggers should be considered, every nth cycle for high
motor (DeviceBase, optional): Motor device to be used for the scan. Defaults to "mo1_bragg".
Examples:
>>> scans.xas_simple_scan_with_xrd(start=8000, stop=9000, scan_time=1, scan_duration=10, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000)
"""
super().__init__(
start=start,
stop=stop,
scan_time=scan_time,
scan_duration=scan_duration,
motor=motor,
**kwargs,
)
self.xrd_enable_low = xrd_enable_low
self.num_trigger_low = num_trigger_low
self.exp_time_low = exp_time_low
self.cycle_low = cycle_low
self.xrd_enable_high = xrd_enable_high
self.num_trigger_high = num_trigger_high
self.exp_time_high = exp_time_high
self.cycle_high = cycle_high
+174
View File
@@ -0,0 +1,174 @@
"""
The NIDAQ continuous scan is used to measure with the NIDAQ without moving the monochromator or any other motor.
Scan procedure:
- prepare_scan
- open_scan
- stage
- pre_scan
- scan_core
- at_each_point (optionally called by scan_core)
- post_scan
- unstage
- close_scan
- on_exception (called if any exception is raised during the scan)
"""
from __future__ import annotations
import time
from typing import Annotated
from bec_lib.device import DeviceBase
from bec_lib.scan_args import ScanArgument, Units
from bec_server.scan_server.scans.scan_base import ScanBase, ScanType
from bec_server.scan_server.scans.scan_modifier import scan_hook
class NidaqContinuousScan(ScanBase):
# Scan Type: Hardware triggered or software triggered?
# If the main trigger and readout logic is done within the at_each_point method in scan_core, choose SOFTWARE_TRIGGERED.
# If the main trigger and readout logic is implemented on a device that is simply kicked off in this scan, choose HARDWARE_TRIGGERED.
# This primarily serves as information for devices: The device may need to react differently if a software trigger is expected
# for every point.
scan_type = ScanType.HARDWARE_TRIGGERED
# Scan name: This is the name of the scan, e.g. "line_scan". This is used for display purposes and to identify the scan type in user interfaces.
# Choose a descriptive name that does not conflict with existing scan names.
# It must be a valid Python identifier, that is, it can only contain letters, numbers, and underscores, and must not start with a number.
scan_name = "nidaq_continuous_scan"
gui_config = {"Scan Parameters": ["scan_duration", "daq", "compression"]}
def __init__(
self,
#fmt: off
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Duration of the scan", units=Units.s)],
daq: Annotated[DeviceBase | None, ScanArgument(display_name="Daq", description="DAQ device to be used for the scan")] = None,
compression: Annotated[bool, ScanArgument(display_name="Compression", description="Whether to compress the data")]= False,
#fmt: on
**kwargs,
):
"""
The NIDAQ continuous scan is used to measure with the NIDAQ without moving the
monochromator or any other motor. The NIDAQ thus runs in continuous mode, with a
set scan_duration.
Args:
scan_duration (float): Duration of the scan
daq (DeviceBase): DAQ device to be used for the scan
compression (bool): Whether to compress the data
Returns:
ScanReport
"""
super().__init__(**kwargs)
self._baseline_readout_status = None
self.scan_duration = scan_duration
self.daq = daq or self.dev["nidaq"]
self.compression = compression
self.monitored_readout_cycle = 1.0 # seconds
self.motors = [self.daq]
self.update_scan_info(scan_duration=scan_duration, compression=compression)
self.actions.set_device_readout_priority([self.daq], priority="async")
@scan_hook
def prepare_scan(self):
"""
Prepare the scan. This can include any steps that need to be executed
before the scan is opened, such as preparing the positions (if not done already)
or setting up the devices.
"""
self.actions.add_scan_report_instruction_device_progress(self.daq)
self._baseline_readout_status = self.actions.read_baseline_devices(wait=False)
@scan_hook
def open_scan(self):
"""
Open the scan.
This step must call self.actions.open_scan() to ensure that a new scan is
opened. Make sure to prepare the scan metadata before, either in
prepare_scan() or in open_scan() itself and call self.update_scan_info(...)
to update the scan metadata if needed.
"""
self.actions.open_scan()
@scan_hook
def stage(self):
"""
Stage the devices for the upcoming scan. The stage logic is typically
implemented on the device itself (i.e. by the device's stage method).
However, if there are any additional steps that need to be executed before
staging the devices, they can be implemented here.
"""
self.actions.stage_all_devices()
@scan_hook
def pre_scan(self):
"""
Pre-scan steps to be executed before the main scan logic.
This is typically the last chance to prepare the devices before the core scan
logic is executed. For example, this is a good place to initialize time-criticial
devices, e.g. devices that have a short timeout.
The pre-scan logic is typically implemented on the device itself.
"""
self.actions.pre_scan_all_devices()
@scan_hook
def scan_core(self):
"""
Core scan logic to be executed during the scan.
This is where the main scan logic should be implemented.
"""
kickoff_status = self.actions.kickoff(device=self.daq, wait=False)
kickoff_status.wait(timeout=5) # wait for proper kickoff of device
complete_status = self.actions.complete(device=self.daq, wait=False)
while not complete_status.done:
self.at_each_point()
time.sleep(self.monitored_readout_cycle)
@scan_hook
def at_each_point(self):
"""
Logic to be executed at each acquisition point during the scan.
"""
self.actions.read_monitored_devices()
@scan_hook
def post_scan(self):
"""
Post-scan steps to be executed after the main scan logic.
"""
self.actions.complete_all_devices()
@scan_hook
def unstage(self):
"""Unstage the scan by executing post-scan steps."""
self.actions.unstage_all_devices()
@scan_hook
def close_scan(self):
"""Close the scan."""
if self._baseline_readout_status is not None:
self._baseline_readout_status.wait()
self.actions.close_scan()
self.actions.check_for_unchecked_statuses()
@scan_hook
def on_exception(self, exception: Exception):
"""
Handle exceptions that occur during the scan.
This is a good place to implement any cleanup logic that needs to be executed in case of an exception,
such as returning the devices to a safe state or moving the motors back to their starting position.
"""
#######################################################
######### Helper methods for the scan logic ###########
#######################################################
# Implement scan-specific helper methods below.
@@ -0,0 +1,12 @@
"""
Scan components for debye_bec.
The scan components module allows you to define custom components that can be used in your scans.
These components can be used to encapsulate reusable logic, interact with devices, or perform specific actions during the scan lifecycle.
"""
from bec_server.scan_server.scans.scan_components import ScanComponents
class DebyeBecScanComponents(ScanComponents):
"""Scan components for debye_bec."""
@@ -0,0 +1,33 @@
"""
Scan modifier plugin for debye_bec.
The scan modifier allows you to modify the scan lifecycle and run custom actions before or after the scan hook or replace the scan hook entirely.
Note that the scan_modifier module must be registered as a plugin in the pyproject.toml file for it to be recognized by the BEC framework and that
there can only be one scan_modifier plugin registered at a time. If you need to run multiple scan modifiers, you can create a single scan
modifier plugin that runs multiple actions in sequence with conditional logic to determine which actions to run based on the scan context.
"""
from bec_server.scan_server.scans.scan_modifier import ScanModifier, scan_hook_impl
class DebyeBecScanModifier(ScanModifier):
"""
Scan modifier for debye_bec.
By inheriting from the ScanModifier base class, you get access to currently running scan (self.scan), the devices (self.dev), the scan info (self.scan_info),
the scan components (self.components) and the scan actions (self.actions).
"""
def __init__(self, **kwargs):
"""Initialize the scan modifier."""
super().__init__(**kwargs)
# Example of running code before the scan stage for a specific scan
# @scan_hook_impl("stage", "before")
# def before_stage(self):
# """Run before the stage hook."""
# self.actions.send_client_info("Custom stage logic executed by ScanModifier.")
# if self.scan_info.scan_name == "example_scan":
# self.dev.samx.set(20)
+326
View File
@@ -0,0 +1,326 @@
"""
V4 implementation of the Debye XAS simple scan.
Scan procedure:
- prepare_scan
- open_scan
- stage
- pre_scan
- scan_core
- at_each_point (optionally called by scan_core)
- post_scan
- unstage
- close_scan
- on_exception (called if any exception is raised during the scan)
"""
from __future__ import annotations
import time
from typing import Annotated
import numpy as np
from bec_lib.device import DeviceBase
from bec_lib.scan_args import ScanArgument, Units
from bec_server.scan_server.scans.scan_base import ScanBase, ScanType
from bec_server.scan_server.scans.scan_modifier import scan_hook
class XasSimpleScan(ScanBase):
scan_type = ScanType.HARDWARE_TRIGGERED
scan_name = "xas_simple_scan"
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
}
def __init__(
self,
# fmt: off
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV, ge=4500, le=64000)],
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV, ge=4500, le=64000)],
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0.05)],
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0.05)],
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0)] = 1,
# fmt: on
**kwargs,
):
"""
Start a simple oscillating scan on the mono bragg motor.
Args:
start (float): Start energy.
stop (float): Stop energy.
scan_time (float): Time for one scan cycle.
scan_duration (float): Total scan duration.
motor (DeviceBase | None): Bragg motor device.
daq (DeviceBase | None): NIDAQ device.
monitored_readout_cycle (float): Delay between monitored readouts.
Returns:
ScanReport
"""
super().__init__(**kwargs)
self.start = start
self.stop = stop
self.scan_time = scan_time
self.scan_duration = scan_duration
self.motor = motor if motor is not None else self.dev["mo1_bragg"]
self.daq = daq if daq is not None else self.dev["nidaq"]
self.monitored_readout_cycle = monitored_readout_cycle
self.positions = np.array([self.start, self.stop], dtype=float)
# We pass on the arguments as "additional_scan_parameters" in the scan info
self.update_scan_info(
positions=self.positions,
scan_time=scan_time,
scan_duration=scan_duration,
monitored_readout_cycle=monitored_readout_cycle,
)
self.actions.set_device_readout_priority([self.daq], priority="async")
@scan_hook
def prepare_scan(self):
"""
Prepare the scan. This can include any steps that need to be executed
before the scan is opened, such as preparing the positions (if not done already)
or setting up the devices.
"""
self.actions.add_scan_report_instruction_device_progress(self.motor)
self._baseline_readout_status = self.actions.read_baseline_devices(wait=False)
@scan_hook
def open_scan(self):
"""
Open the scan.
This step must call self.actions.open_scan() to ensure that a new scan is
opened. Make sure to prepare the scan metadata before, either in
prepare_scan() or in open_scan() itself and call self.update_scan_info(...)
to update the scan metadata if needed.
"""
self.actions.open_scan()
@scan_hook
def stage(self):
"""
Stage the devices for the upcoming scan. The stage logic is typically
implemented on the device itself (i.e. by the device's stage method).
However, if there are any additional steps that need to be executed before
staging the devices, they can be implemented here.
"""
self.actions.stage_all_devices()
@scan_hook
def pre_scan(self):
"""
Pre-scan steps to be executed before the main scan logic.
This is typically the last chance to prepare the devices before the core scan
logic is executed. For example, this is a good place to initialize time-criticial
devices, e.g. devices that have a short timeout.
The pre-scan logic is typically implemented on the device itself.
"""
self.actions.pre_scan_all_devices()
@scan_hook
def scan_core(self):
"""
Core scan logic to be executed during the scan.
This is where the main scan logic should be implemented.
"""
self.actions.kickoff(self.motor)
completion_status = self.actions.complete(self.motor, wait=False)
while not completion_status.done:
self.at_each_point()
@scan_hook
def at_each_point(self):
"""
Logic to be executed at each acquisition point during the scan.
"""
self.actions.read_monitored_devices()
time.sleep(self.monitored_readout_cycle)
@scan_hook
def post_scan(self):
"""
Post-scan steps to be executed after the main scan logic.
"""
self.actions.complete_all_devices()
@scan_hook
def unstage(self):
"""Unstage the scan by executing post-scan steps."""
self.actions.unstage_all_devices()
@scan_hook
def close_scan(self):
"""Close the scan."""
if self._baseline_readout_status is not None:
self._baseline_readout_status.wait()
self.actions.close_scan()
self.actions.check_for_unchecked_statuses()
@scan_hook
def on_exception(self, exception: Exception):
"""
Handle exceptions that occur during the scan.
This is a good place to implement any cleanup logic that needs to be executed in case of an exception,
such as returning the devices to a safe state or moving the motors back to their starting position.
"""
self.actions.complete_all_devices(wait=False)
class XasSimpleScanWithXrd(XasSimpleScan):
scan_name = "xas_simple_scan_with_xrd"
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
"XRD Triggers": ["exp_time", "n_of_trigger"],
}
def __init__(
self,
# fmt: off
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")],
break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)],
cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)],
break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")],
break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)],
cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)],
exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)],
n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)],
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
**kwargs,
# fmt: on
):
super().__init__(
start=start,
stop=stop,
scan_time=scan_time,
scan_duration=scan_duration,
motor=motor,
daq=daq,
monitored_readout_cycle=monitored_readout_cycle,
**kwargs,
)
# We pass on the arguments as "additional_scan_parameters" in the scan info
self.update_scan_info(
break_enable_low=break_enable_low,
break_time_low=break_time_low,
cycle_low=cycle_low,
break_enable_high=break_enable_high,
break_time_high=break_time_high,
cycle_high=cycle_high,
exp_time=exp_time,
n_of_trigger=n_of_trigger,
)
class XasAdvancedScan(XasSimpleScan):
scan_name = "xas_advanced_scan"
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
"Spline Parameters": ["p_kink", "e_kink"],
}
def __init__(
self,
# fmt: off
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)],
e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)],
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
**kwargs,
# fmt: on
):
super().__init__(
start=start,
stop=stop,
scan_time=scan_time,
scan_duration=scan_duration,
motor=motor,
daq=daq,
monitored_readout_cycle=monitored_readout_cycle,
**kwargs,
)
# We pass on the arguments as "additional_scan_parameters" in the scan info
self.update_scan_info(p_kink=p_kink, e_kink=e_kink)
class XasAdvancedScanWithXrd(XasAdvancedScan):
scan_name = "xas_advanced_scan_with_xrd"
gui_config = {
"Movement Parameters": ["start", "stop"],
"Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"],
"Spline Parameters": ["p_kink", "e_kink"],
"Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"],
"High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"],
"XRD Triggers": ["exp_time", "n_of_trigger"],
}
def __init__(
self,
# fmt: off
start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)],
stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)],
scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)],
scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)],
p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)],
e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)],
break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")],
break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)],
cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)],
break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")],
break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)],
cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)],
exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)],
n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)],
motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None,
daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None,
monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1,
**kwargs,
# fmt: on
):
super().__init__(
start=start,
stop=stop,
scan_time=scan_time,
scan_duration=scan_duration,
p_kink=p_kink,
e_kink=e_kink,
motor=motor,
daq=daq,
monitored_readout_cycle=monitored_readout_cycle,
**kwargs,
)
# We pass on the arguments as "additional_scan_parameters" in the scan info
self.update_scan_info(
break_enable_low=break_enable_low,
break_time_low=break_time_low,
cycle_low=cycle_low,
break_enable_high=break_enable_high,
break_time_high=break_time_high,
cycle_high=cycle_high,
exp_time=exp_time,
n_of_trigger=n_of_trigger,
)
@@ -28,7 +28,7 @@ class NIDAQWriterService(BECService):
def __init__(self, config: ServiceConfig, connector_cls: RedisConnector) -> None:
super().__init__(config=config, connector_cls=connector_cls, unique_service=True)
self.queue = queue.Queue()
config = self._service_config.service_config.get("file_writer")
config = self._service_config.config.get("file_writer")
self.writer_mixin = FileWriter(service_config=config)
self._scan_status_consumer = None
self._ni_data_consumer = None
+24 -6
View File
@@ -5,24 +5,33 @@ build-backend = "hatchling.build"
[project]
name = "debye_bec"
version = "0.0.0"
description = "Custom device implementations based on the ophyd hardware abstraction layer"
requires-python = ">=3.10"
description = "A plugin repository for BEC"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering",
]
dependencies = ["numpy", "bec_lib", "h5py", "ophyd_devices"]
dependencies = [
"numpy",
"scipy",
"bec_lib",
"h5py",
"ophyd_devices",
"opencv-python==4.11.0.86",
"xrt",
]
[project.optional-dependencies]
dev = [
"bec_server",
"black",
"copier",
"isort",
"coverage",
"pylint",
"pytest",
"pytest-random-order",
"bec_server",
]
[project.entry-points."bec"]
@@ -37,12 +46,21 @@ plugin_file_writer = "debye_bec.file_writer"
[project.entry-points."bec.scans"]
plugin_scans = "debye_bec.scans"
[project.entry-points."bec.scans.scan_modifier"]
plugin_scan_modifier = "debye_bec.scans.scan_customization.scan_modifier"
[project.entry-points."bec.scans.metadata_schema"]
plugin_metadata_schema = "debye_bec.scans.metadata_schema"
[project.entry-points."bec.ipython_client_startup"]
plugin_ipython_client_pre = "debye_bec.bec_ipython_client.startup.pre_startup"
plugin_ipython_client_post = "debye_bec.bec_ipython_client.startup"
[project.entry-points."bec.widgets"]
plugin_widgets = "debye_bec.bec_widgets"
[project.entry-points."bec.widgets.auto_updates"]
plugin_widgets_update = "debye_bec.bec_widgets.auto_updates"
[project.entry-points."bec.widgets.user_widgets"]
plugin_widgets = "debye_bec.bec_widgets.widgets"
[tool.hatch.build.targets.wheel]
include = ["*"]
View File
View File
View File
View File
+12 -9
View File
@@ -1,31 +1,34 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
pip install pytest
```
in your *python environment*.
in your _python environment_.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
``` bash
```bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+12 -9
View File
@@ -1,31 +1,34 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
pip install pytest
```
in your *python environment*.
in your _python environment_.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
``` bash
```bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+34
View File
@@ -0,0 +1,34 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
pip install pytest
```
in your _python environment_.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+12 -9
View File
@@ -1,31 +1,34 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
pip install pytest
```
in your *python environment*.
in your _python environment_.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
``` bash
```bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+70
View File
@@ -0,0 +1,70 @@
"""Module to test prosilica and Basler cam integrations."""
import threading
from unittest import mock
import ophyd
import pytest
from ophyd_devices.devices.areadetector.cam import AravisDetectorCam, ProsilicaDetectorCam
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from debye_bec.devices.cameras.basler_cam import BaslerCam
from debye_bec.devices.cameras.prosilica_cam import ProsilicaCam
# pylint: disable=protected-access
# pylint: disable=redefined-outer-name
@pytest.fixture(scope="function")
def mock_basler():
"""Fixture to mock the camera device."""
name = "cam"
prefix = "test:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = BaslerCam(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
def test_basler_init(mock_basler):
"""Test the initialization of the Basler camera device."""
assert mock_basler.name == "cam"
assert mock_basler.prefix == "test:"
assert isinstance(mock_basler.cam1, AravisDetectorCam)
assert isinstance(mock_basler.image1, ImagePlugin_V35)
assert mock_basler._update_frequency == 1
assert mock_basler._live_mode is False
assert mock_basler._live_mode_event is None
assert mock_basler._task_status is None
assert mock_basler.preview.ndim == 2
assert mock_basler.preview.num_rotation_90 == 3
@pytest.fixture(scope="function")
def mock_prosilica():
"""Fixture to mock the camera device."""
name = "cam"
prefix = "test:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = ProsilicaCam(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
def test_prosilica_init(mock_prosilica):
"""Test the initialization of the Prosilica camera device."""
assert mock_prosilica.name == "cam"
assert mock_prosilica.prefix == "test:"
assert isinstance(mock_prosilica.cam1, ProsilicaDetectorCam)
assert isinstance(mock_prosilica.image1, ImagePlugin_V35)
assert mock_prosilica._update_frequency == 1
assert mock_prosilica._live_mode is False
assert mock_prosilica._live_mode_event is None
assert mock_prosilica._task_status is None
assert mock_prosilica.preview.ndim == 2
assert mock_prosilica.preview.num_rotation_90 == 3
@@ -0,0 +1,86 @@
"""Module to test camera base integration class for Debye."""
import threading
from unittest import mock
import ophyd
import pytest
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from debye_bec.devices.cameras.debye_base_cam import DebyeBaseCamera
# pylint: disable=protected-access
# pylint: disable=redefined-outer-name
@pytest.fixture(scope="function")
def mock_cam():
"""Fixture to mock the camera device."""
name = "cam"
prefix = "test:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = DebyeBaseCamera(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
def test_init(mock_cam):
"""Test the initialization of the camera device."""
assert mock_cam.name == "cam"
assert mock_cam.prefix == "test:"
assert mock_cam._update_frequency == 1
assert mock_cam._live_mode is False
assert mock_cam._live_mode_event is None
assert mock_cam._task_status is None
assert mock_cam.preview.ndim == 2
assert mock_cam.preview.num_rotation_90 == -1
def test_start_live_mode(mock_cam):
"""Test starting live mode."""
def mock_emit_to_bec(*args, **kwargs):
"""Mock emit_to_bec method."""
while not mock_cam._live_mode_event.wait(1 / mock_cam._update_frequency):
pass
with mock.patch.object(mock_cam, "emit_to_bec", side_effect=mock_emit_to_bec):
mock_cam._start_live_mode()
assert mock_cam._live_mode_event is not None
assert mock_cam._task_status is not None
assert mock_cam._task_status.state == "running"
mock_cam._live_mode_event.set()
# Wait for the task to resolve
mock_cam._task_status.wait(timeout=5)
assert mock_cam._task_status.done is True
def test_stop_live_mode(mock_cam):
"""Test stopping live mode."""
with mock.patch.object(mock_cam, "_live_mode_event") as mock_live_mode_event:
mock_cam._stop_live_mode()
assert mock_live_mode_event.set.called
assert mock_cam._live_mode_event is None
def test_live_mode_property(mock_cam):
"""Test the live_mode property."""
assert mock_cam.live_mode is False
with mock.patch.object(mock_cam, "_start_live_mode") as mock_start_live_mode:
with mock.patch.object(mock_cam, "_stop_live_mode") as mock_stop_live_mode:
# Set to true
mock_cam.live_mode = True
assert mock_start_live_mode.called
assert mock_cam._live_mode is True
assert mock_start_live_mode.call_count == 1
# Second call should call _start_live_mode
mock_cam.live_mode = True
assert mock_start_live_mode.call_count == 1
# Set to false
mock_cam.live_mode = False
assert mock_stop_live_mode.called
assert mock_cam._live_mode is False
assert mock_stop_live_mode.call_count == 1
+356 -283
View File
@@ -16,14 +16,14 @@ from ophyd.utils import LimitError
from ophyd_devices.tests.utils import MockPV
# from bec_server.device_server.tests.utils import DMMock
from debye_bec.devices.mo1_bragg import (
from debye_bec.devices.mo1_bragg.mo1_bragg import (
Mo1Bragg,
Mo1BraggError,
MoveType,
ScanControlLoadMessage,
ScanControlMode,
ScanControlScanStatus,
)
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import MoveType
# TODO move this function to ophyd_devices, it is duplicated in csaxs_bec and needed for other pluging repositories
from debye_bec.devices.test_utils.utils import patch_dual_pvs
@@ -40,10 +40,7 @@ def scan_worker_mock(scan_server_mock):
def mock_bragg():
name = "bragg"
prefix = "X01DA-OP-MO1:BRAGG:"
with (
mock.patch.object(ophyd, "cl") as mock_cl,
mock.patch("debye_bec.devices.mo1_bragg.Mo1Bragg", "_on_init"),
):
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = Mo1Bragg(name=name, prefix=prefix)
@@ -55,52 +52,38 @@ def test_init(mock_bragg):
dev = mock_bragg
assert dev.name == "bragg"
assert dev.prefix == "X01DA-OP-MO1:BRAGG:"
assert dev.move_type.get() == MoveType.ENERGY
assert dev.crystal.offset_si111._read_pvname == "X01DA-OP-MO1:BRAGG:offset_si111_RBV"
assert dev.move_abs._read_pvname == "X01DA-OP-MO1:BRAGG:move_abs.PROC"
assert dev.crystal.bragg_off_si111._read_pvname == "X01DA-OP-MO1:BRAGG:bragg_off_si111_RBV"
assert dev.move_abs._read_pvname == "X01DA-OP-MO1:BRAGG:move_abs"
def test_check_value(mock_bragg):
dev = mock_bragg
dev.low_lim._read_pv.mock_data = 0
dev.high_lim._read_pv.mock_data = 1
dev.low_limit_angle._read_pv.mock_data = 10
dev.high_limit_angle._read_pv.mock_data = 20
# Check that limits are taken correctly from angle or energy
# Energy first
move_type = MoveType.ENERGY
dev.move_type.set(move_type)
# nothing happens
dev.check_value(0.5)
with pytest.raises(LimitError):
dev.check_value(15)
# Angle next
move_type = MoveType.ANGLE
dev.move_type.set(move_type)
dev.check_value(15)
with pytest.raises(LimitError):
dev.check_value(0.5)
def test_egu(mock_bragg):
dev = mock_bragg
assert dev.egu == "eV"
dev.move_type.set(MoveType.ANGLE)
assert dev.egu == "deg"
def test_move_succeeds(mock_bragg):
dev = mock_bragg
dev.move_abs._read_pv.mock_data = 0
# Move succeeds
with mock.patch.object(dev.motor_is_moving._read_pv, "mock_data", side_effect=[0, 1]):
status = dev.move(0.5)
# Sleep needed to allow thread to resolive in _move_and_finish, i.e. and the 0.25s sleep inside the function
time.sleep(1)
assert status.done is True
assert status.success is True
assert dev.setpoint.get() == 0.5
assert dev.move_abs.get() == 1
dev.motor_is_moving._read_pv.mock_data = 0
status = dev.move(0.5)
assert status.done is False
dev.motor_is_moving._read_pv.mock_data = 1
status.wait(timeout=3) # Callback should within that time
assert status.done is True
assert status.success is True
assert dev.setpoint.get() == 0.5
assert dev.move_abs.get() == 1
def test_stop_move(mock_bragg):
@@ -123,14 +106,14 @@ def test_set_xtal(mock_bragg):
dev = mock_bragg
dev.set_xtal("111")
# Default values for mock
assert dev.crystal.offset_si111.get() == 0
assert dev.crystal.offset_si311.get() == 0
assert dev.crystal.bragg_off_si111.get() == 0
assert dev.crystal.bragg_off_si311.get() == 0
assert dev.crystal.d_spacing_si111.get() == 0
assert dev.crystal.d_spacing_si311.get() == 0
assert dev.crystal.xtal_enum.get() == 0
dev.set_xtal("311", offset_si111=1, offset_si311=2, d_spacing_si111=3, d_spacing_si311=4)
assert dev.crystal.offset_si111.get() == 1
assert dev.crystal.offset_si311.get() == 2
dev.set_xtal("311", bragg_off_si111=1, bragg_off_si311=2, d_spacing_si111=3, d_spacing_si311=4)
assert dev.crystal.bragg_off_si111.get() == 1
assert dev.crystal.bragg_off_si311.get() == 2
assert dev.crystal.d_spacing_si111.get() == 3
assert dev.crystal.d_spacing_si311.get() == 4
assert dev.crystal.xtal_enum.get() == 1
@@ -138,38 +121,32 @@ def test_set_xtal(mock_bragg):
def test_set_xas_settings(mock_bragg):
dev = mock_bragg
dev.move_type.set(MoveType.ENERGY)
dev.set_xas_settings(low=0.5, high=1, scan_time=0.1)
assert dev.scan_settings.s_scan_energy_lo.get() == 0.5
assert dev.scan_settings.s_scan_energy_hi.get() == 1
assert dev.scan_settings.s_scan_scantime.get() == 0.1
dev.move_type.set(MoveType.ANGLE)
dev.set_xas_settings(low=10, high=20, scan_time=1)
assert dev.scan_settings.s_scan_angle_lo.get() == 10
assert dev.scan_settings.s_scan_angle_hi.get() == 20
assert dev.scan_settings.s_scan_scantime.get() == 1
def test_set_xrd_settings(mock_bragg):
def test_set_trig_settings(mock_bragg):
dev = mock_bragg
dev.set_xrd_settings(
dev.set_trig_settings(
enable_low=True,
enable_high=False,
num_trigger_low=1,
num_trigger_high=7,
exp_time_low=1,
exp_time_high=3,
break_time_high=0.1,
break_time_low=0.01,
cycle_low=1,
cycle_high=5,
cycle_high=3,
exp_time=0.5,
n_of_trigger=7,
)
assert dev.scan_settings.xrd_enable_lo_enum.get() == True
assert dev.scan_settings.xrd_enable_hi_enum.get() == False
assert dev.scan_settings.xrd_n_trigger_lo.get() == 1
assert dev.scan_settings.xrd_n_trigger_hi.get() == 7
assert dev.scan_settings.xrd_time_lo.get() == 1
assert dev.scan_settings.xrd_time_hi.get() == 3
assert dev.scan_settings.xrd_every_n_lo.get() == 1
assert dev.scan_settings.xrd_every_n_hi.get() == 5
assert dev.scan_settings.trig_ena_lo_enum.get() == True
assert dev.scan_settings.trig_ena_hi_enum.get() == False
assert dev.scan_settings.trig_every_n_lo.get() == 1
assert dev.scan_settings.trig_every_n_hi.get() == 3
assert dev.scan_settings.trig_time_lo.get() == 0.01
assert dev.scan_settings.trig_time_hi.get() == 0.1
assert dev.trigger_settings.xrd_trig_period.get() == 0.5
assert dev.trigger_settings.xrd_n_of_trig.get() == 7
def test_set_control_settings(mock_bragg):
@@ -182,39 +159,6 @@ def test_set_control_settings(mock_bragg):
assert dev.scan_control.scan_duration.get() == 5
def test_update_scan_parameters(mock_bragg):
dev = mock_bragg
msg = ScanStatusMessage(
scan_id="my_scan_id",
status="closed",
info={
"kwargs": {
"start": 0,
"stop": 5,
"scan_time": 1,
"scan_duration": 10,
"xrd_enable_low": True,
"xrd_enable_high": False,
"num_trigger_low": 1,
"num_trigger_high": 7,
"exp_time_low": 1,
"exp_time_high": 3,
"cycle_low": 1,
"cycle_high": 5,
}
},
metadata={},
)
mock_bragg.scaninfo.scan_msg = msg
for field in fields(dev.scan_parameter):
assert getattr(dev.scan_parameter, field.name) == None
dev._update_scan_parameter()
for field in fields(dev.scan_parameter):
assert getattr(dev.scan_parameter, field.name) == msg.content["info"]["kwargs"].get(
field.name, None
)
def test_kickoff_scan(mock_bragg):
dev = mock_bragg
dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.READY
@@ -223,9 +167,14 @@ def test_kickoff_scan(mock_bragg):
dev.scan_control.scan_start_infinite._read_pv.mock_data = 0
status = dev.kickoff()
assert status.done is False
dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
time.sleep(0.2)
assert status.done is True
# TODO MockPV does not support callbacks yet, so we need to improve here #16
# dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
# dev.scan_control.scan_status._read_pv.
# status.wait(timeout=3) # Callback should resolve now
# assert status.done is True
# # dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
# time.sleep(0.2)
# assert status.done is True
assert dev.scan_control.scan_start_timer.get() == 1
dev.scan_control.scan_duration._read_pv.mock_data = 0
@@ -237,200 +186,324 @@ def test_kickoff_scan(mock_bragg):
assert dev.scan_control.scan_start_infinite.get() == 1
def test_complete(mock_bragg):
dev = mock_bragg
dev.scan_control.scan_done._read_pv.mock_data = 0
# Normal case
status = dev.complete()
assert status.done is False
assert status.success is False
dev.scan_control.scan_done._read_pv.mock_data = 1
time.sleep(0.2)
assert status.done is True
assert status.success is True
# FIXME #22 once mock_pv supports callbacks, high priority!
# def test_complete(mock_bragg):
# dev = mock_bragg
# dev.scan_control.scan_done._read_pv.mock_data = 0
# # Normal case
# status = dev.complete()
# assert status.done is False
# assert status.success is False
# dev.scan_control.scan_done._read_pv.mock_data = 1
# status.wait()
# # time.sleep(0.2)
# assert status.done is True
# assert status.success is True
# Stop called case
dev.scan_control.scan_done._read_pv.mock_data = 0
status = dev.complete()
assert status.done is False
assert status.success is False
dev.stop()
time.sleep(0.2)
assert status.done is True
assert status.success is False
# # Stop called case
# dev.scan_control.scan_done._read_pv.mock_data = 0
# status = dev.complete()
# assert status.done is False
# assert status.success is False
# dev.stop()
# time.sleep(0.2)
# assert status.done is True
# assert status.success is False
def test_unstage(mock_bragg):
mock_bragg.timeout_for_pvwait = 0.5
mock_bragg.scan_control.scan_val_reset._read_pv.mock_data = 0
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.PENDING
# FIXME #22 once mock_pv supports callbacks, high priority!
# def test_unstage(mock_bragg):
# mock_bragg.timeout_for_pvwait = 0.5
# mock_bragg.scan_control.scan_val_reset._read_pv.mock_data = 0
# mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.PENDING
with mock.patch.object(mock_bragg.scan_control.scan_val_reset, "put") as mock_put:
mock_bragg.unstage()
assert mock_put.call_count == 0
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
with pytest.raises(TimeoutError):
mock_bragg.unstage()
assert mock_put.call_count == 1
# with mock.patch.object(mock_bragg.scan_control.scan_val_reset, "put") as mock_put:
# status = mock_bragg.unstage()
# assert mock_put.call_count == 0
# mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
# with pytest.raises(TimeoutError):
# mock_bragg.unstage()
# assert mock_put.call_count == 1
@pytest.mark.parametrize(
"msg",
[
ScanQueueMessage(
scan_type="monitor_scan",
parameter={
"args": {},
"kwargs": {"device": "mo1_bragg", "start": 0, "stop": 10, "relative": True},
"num_points": 100,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_simple_scan",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 0,
"stop": 10,
"scan_time": 1,
"scan_duration": 10,
},
"num_points": 100,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_simple_scan_with_xrd",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 0,
"stop": 10,
"scan_time": 1,
"scan_duration": 10,
"xrd_enable_low": True,
"xrd_enable_high": False,
"num_trigger_low": 1,
"num_trigger_high": 7,
"exp_time_low": 1,
"exp_time_high": 3,
"cycle_low": 1,
"cycle_high": 5,
},
"num_points": 10,
},
queue="primary",
metadata={"RID": "test1234"},
),
],
)
def test_stage(mock_bragg, scan_worker_mock, msg):
"""This test is important to check that the stage method of the device is working correctly.
Changing the kwargs names in the scans is tightly linked to the logic on the device, thus
it is important to check that the stage method is working correctly for the current implementation.
# TODO reimplement the test for stage method
# @pytest.mark.parametrize(
# "msg",
# [
# ScanQueueMessage(
# scan_type="monitor_scan",
# parameter={
# "args": {},
# "kwargs": {
# "device": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "relative": True,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_simple_scan",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "scan_time": 1,
# "scan_duration": 10,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_simple_scan_with_xrd",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "scan_time": 1,
# "scan_duration": 10,
# "xrd_enable_low": True,
# "xrd_enable_high": False,
# "num_trigger_low": 1,
# "num_trigger_high": 7,
# "exp_time_low": 1,
# "exp_time_high": 3,
# "cycle_low": 1,
# "cycle_high": 5,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 10,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_advanced_scan",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 8000,
# "stop": 9000,
# "scan_time": 1,
# "scan_duration": 10,
# "p_kink": 50,
# "e_kink": 8500,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_advanced_scan_with_xrd",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 8000,
# "stop": 9000,
# "scan_time": 1,
# "scan_duration": 10,
# "p_kink": 50,
# "e_kink": 8500,
# "xrd_enable_low": True,
# "xrd_enable_high": False,
# "num_trigger_low": 1,
# "num_trigger_high": 7,
# "exp_time_low": 1,
# "exp_time_high": 3,
# "cycle_low": 1,
# "cycle_high": 5,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 10,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ],
# )
# def test_stage(mock_bragg, scan_worker_mock, msg):
# """This test is important to check that the stage method of the device is working correctly.
# Changing the kwargs names in the scans is tightly linked to the logic on the device, thus
# it is important to check that the stage method is working correctly for the current implementation.
Therefor, this test creates a scaninfo message using the scan.open_scan() method to always check
agains the currently implemented scans vs. the logic on the device"""
# Create a scaninfo message using scans the ScanQueueMessages above, 3 cases of fly scan; for the general case the procedure is not defined yet
worker = scan_worker_mock
scan_server = worker.parent
rb = RequestBlock(msg, assembler=ScanAssembler(parent=scan_server))
with mock.patch.object(worker, "current_instruction_queue_item"):
worker.scan_motors = []
worker.readout_priority = {
"monitored": [],
"baseline": [],
"async": [],
"continuous": [],
"on_request": [],
}
open_scan_msg = list(rb.scan.open_scan())[0]
worker._initialize_scan_info(rb, open_scan_msg, msg.content["parameter"].get("num_points"))
scan_status_msg = ScanStatusMessage(
scan_id="test1234", status="closed", info=worker.current_scan_info, metadata={}
)
mock_bragg.scaninfo.scan_msg = scan_status_msg
# Therefor, this test creates a scaninfo message using the scan.open_scan() method to always check
# agains the currently implemented scans vs. the logic on the device"""
# # Create a scaninfo message using scans the ScanQueueMessages above, 3 cases of fly scan; for the general case the procedure is not defined yet
# worker = scan_worker_mock
# scan_server = worker.parent
# rb = RequestBlock(msg, assembler=ScanAssembler(parent=scan_server))
# with mock.patch.object(worker, "current_instruction_queue_item"):
# worker.scan_motors = []
# worker.readout_priority = {
# "monitored": [],
# "baseline": [],
# "async": [],
# "continuous": [],
# "on_request": [],
# }
# open_scan_msg = list(rb.scan.open_scan())[0]
# worker._initialize_scan_info(
# rb, open_scan_msg, msg.content["parameter"].get("num_points", 1)
# )
# # TODO find a better solution to this...
# scan_status_msg = ScanStatusMessage(
# scan_id=worker.current_scan_id,
# status="open",
# scan_name=worker.current_scan_info.get("scan_name"),
# scan_number=worker.current_scan_info.get("scan_number"),
# session_id=worker.current_scan_info.get("session_id"),
# dataset_number=worker.current_scan_info.get("dataset_number"),
# num_points=worker.current_scan_info.get("num_points"),
# scan_type=worker.current_scan_info.get("scan_type"),
# scan_report_devices=worker.current_scan_info.get("scan_report_devices"),
# user_metadata=worker.current_scan_info.get("user_metadata"),
# readout_priority=worker.current_scan_info.get("readout_priority"),
# scan_parameters=worker.current_scan_info.get("scan_parameters"),
# request_inputs=worker.current_scan_info.get("request_inputs"),
# info=worker.current_scan_info,
# )
# mock_bragg.scan_info.msg = scan_status_msg
# Ensure that ScanControlLoadMessage is set to SUCCESS
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
with (
mock.patch.object(mock_bragg.scaninfo, "load_scan_metadata") as mock_load_scan_metadata,
mock.patch.object(mock_bragg, "_check_scan_msg") as mock_check_scan_msg,
mock.patch.object(mock_bragg, "on_unstage"),
):
scan_name = scan_status_msg.content["info"].get("scan_name", "")
# Chek the not implemented fly scan first, should raise Mo1BraggError
if scan_name not in ["xas_simple_scan", "xas_simple_scan_with_xrd"]:
with pytest.raises(Mo1BraggError):
mock_bragg.stage()
assert mock_check_scan_msg.call_count == 1
assert mock_load_scan_metadata.call_count == 1
else:
with (
mock.patch.object(mock_bragg, "set_xas_settings") as mock_xas_settings,
mock.patch.object(mock_bragg, "set_xrd_settings") as mock_xrd_settings,
mock.patch.object(
mock_bragg, "set_scan_control_settings"
) as mock_set_scan_control_settings,
):
# Check xas_simple_scan
if scan_name == "xas_simple_scan":
mock_bragg.stage()
assert mock_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
)
assert mock_xrd_settings.call_args == mock.call(
enable_low=False,
enable_high=False,
num_trigger_low=0,
num_trigger_high=0,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.SIMPLE,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# Check xas_simple_scan_with_xrd
elif scan_name == "xas_simple_scan_with_xrd":
mock_bragg.stage()
assert mock_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
)
assert mock_xrd_settings.call_args == mock.call(
enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
enable_high=scan_status_msg.content["info"]["kwargs"][
"xrd_enable_high"
],
num_trigger_low=scan_status_msg.content["info"]["kwargs"][
"num_trigger_low"
],
num_trigger_high=scan_status_msg.content["info"]["kwargs"][
"num_trigger_high"
],
exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
exp_time_high=scan_status_msg.content["info"]["kwargs"][
"exp_time_high"
],
cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.SIMPLE,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# # Ensure that ScanControlLoadMessage is set to SUCCESS
# mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
# with (
# mock.patch.object(mock_bragg, "_check_scan_msg") as mock_check_scan_msg,
# mock.patch.object(mock_bragg, "on_unstage"),
# ):
# scan_name = scan_status_msg.content["info"].get("scan_name", "")
# # Chek the not implemented fly scan first, should raise Mo1BraggError
# if scan_name not in [
# "xas_simple_scan",
# "xas_simple_scan_with_xrd",
# "xas_advanced_scan",
# "xas_advanced_scan_with_xrd",
# ]:
# with pytest.raises(Mo1BraggError):
# mock_bragg.stage()
# assert mock_check_scan_msg.call_count == 1
# else:
# with (
# mock.patch.object(mock_bragg, "set_xas_settings") as mock_xas_settings,
# mock.patch.object(
# mock_bragg, "set_advanced_xas_settings"
# ) as mock_advanced_xas_settings,
# mock.patch.object(mock_bragg, "set_trig_settings") as mock_trig_settings,
# mock.patch.object(
# mock_bragg, "set_scan_control_settings"
# ) as mock_set_scan_control_settings,
# ):
# # Check xas_simple_scan
# if scan_name == "xas_simple_scan":
# mock_bragg.stage()
# assert mock_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=False,
# enable_high=False,
# exp_time_low=0,
# exp_time_high=0,
# cycle_low=0,
# cycle_high=0,
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.SIMPLE,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_simple_scan_with_xrd
# elif scan_name == "xas_simple_scan_with_xrd":
# mock_bragg.stage()
# assert mock_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
# enable_high=scan_status_msg.content["info"]["kwargs"][
# "xrd_enable_high"
# ],
# exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
# exp_time_high=scan_status_msg.content["info"]["kwargs"][
# "exp_time_high"
# ],
# cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
# cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.SIMPLE,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_advanced_scan
# elif scan_name == "xas_advanced_scan":
# mock_bragg.stage()
# assert mock_advanced_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
# e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=False,
# enable_high=False,
# exp_time_low=0,
# exp_time_high=0,
# cycle_low=0,
# cycle_high=0,
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.ADVANCED,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_advanced_scan_with_xrd
# elif scan_name == "xas_advanced_scan_with_xrd":
# mock_bragg.stage()
# assert mock_advanced_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
# e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
# enable_high=scan_status_msg.content["info"]["kwargs"][
# "xrd_enable_high"
# ],
# exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
# exp_time_high=scan_status_msg.content["info"]["kwargs"][
# "exp_time_high"
# ],
# cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
# cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.ADVANCED,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
@@ -0,0 +1,88 @@
"""Tests for the Mo1BraggAngle class."""
import threading
from unittest import mock
import ophyd
import pytest
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from debye_bec.devices.mo1_bragg.mo1_bragg_angle import Mo1BraggAngle
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import Mo1BraggStoppedError
# pylint: disable=protected-access
@pytest.fixture(scope="function")
def mock_bragg() -> Mo1BraggAngle:
"""Fixture for the Mo1BraggAngle device."""
name = "bragg"
prefix = "X01DA-OP-MO1:BRAGG:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = Mo1BraggAngle(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
def test_mo1_bragg_angle_init(mock_bragg):
"""Test the initialization of the Mo1BraggAngle device."""
assert mock_bragg.name == "bragg"
assert mock_bragg.prefix == "X01DA-OP-MO1:BRAGG:"
assert isinstance(mock_bragg.readback, ophyd.EpicsSignalRO)
assert isinstance(mock_bragg.setpoint, ophyd.EpicsSignalWithRBV)
assert isinstance(mock_bragg.low_lim, ophyd.EpicsSignalRO)
assert isinstance(mock_bragg.high_lim, ophyd.EpicsSignalRO)
def test_mo1_bragg_angle_egu(mock_bragg):
"""Test the engineering unit of the Mo1BraggAngle device."""
assert mock_bragg.egu == "deg"
def test_mo1_bragg_angle_limits(mock_bragg):
"""Test the limits of the Mo1BraggAngle device."""
mock_bragg.low_lim._read_pv.mock_data = -10
mock_bragg.high_lim._read_pv.mock_data = 10
assert mock_bragg.limits == (-10, 10)
def test_mo1_bragg_angle_move(mock_bragg):
"""Test the move method of the Mo1BraggAngle device."""
mock_bragg.setpoint.put(0)
mock_bragg.readback._read_pv.mock_data = 0 # EpicsSignalRO
# Change PV for motor is moving before starting the move
mock_bragg.motor_is_moving._read_pv.mock_data = 0 # EpicsSignalRO
status = mock_bragg.move(5)
assert status.done is False
# Check setpoint is set correctly
assert mock_bragg.setpoint.get() == 5
# Update the motor is moving PV to simulate that the move is done
mock_bragg.motor_is_moving._read_pv.mock_data = 1
assert mock_bragg.motor_is_moving.get() == 1
status.wait(timeout=5) # If the status does not resolve after 5 seconds, something is wrong
assert status.done is True
def test_mo1_bragg_angle_stop(mock_bragg):
"""Test the stop method of the Mo1BraggAngle device."""
assert mock_bragg.stopped is False
mock_bragg.stop()
assert mock_bragg.stopped is True
status = mock_bragg.move(5)
assert status.done is False
# stopped should be resetted
assert mock_bragg.stopped is False
with pytest.raises(Mo1BraggStoppedError):
mock_bragg.stop()
status.wait(timeout=5) # This should raise before due to stop() call
+152
View File
@@ -0,0 +1,152 @@
# pylint: skip-file
import numpy as np
import debye_bec.devices.mo1_bragg.mo1_bragg_utils as utils
def test_compute_spline():
p, v, dt = utils.compute_spline(
low_deg=10, high_deg=12, p_kink=50, e_kink_deg=11, scan_time=0.5
)
rtol = 1e-6
atol = 1e-3
p_desired = [
9.98,
9.98376125,
9.99479,
10.01270375,
10.03712,
10.06765625,
10.10393,
10.14555875,
10.19216,
10.24335125,
10.29875,
10.35797375,
10.42064,
10.48636625,
10.55477,
10.62546875,
10.69808,
10.77222125,
10.84751,
10.92356375,
11.0,
11.07643625,
11.15249,
11.22777875,
11.30192,
11.37453125,
11.44523,
11.51363375,
11.57936,
11.64202625,
11.70125,
11.75664875,
11.80784,
11.85444125,
11.89607,
11.93234375,
11.96288,
11.98729625,
12.00521,
12.01623875,
12.02,
]
v_desired = [
0.0,
1.50156441,
2.35715667,
2.90783907,
3.29035796,
3.57019636,
3.78263174,
3.9483388,
4.08022441,
4.18675043,
4.27368333,
4.34507577,
4.40384627,
4.45213618,
4.49153736,
4.52324148,
4.54814006,
4.5668924,
4.57997194,
4.58769736,
4.59025246,
4.58769736,
4.57997194,
4.5668924,
4.54814006,
4.52324148,
4.49153736,
4.45213618,
4.40384627,
4.34507577,
4.27368333,
4.18675043,
4.08022441,
3.9483388,
3.78263174,
3.57019636,
3.29035796,
2.90783907,
2.35715667,
1.50156441,
0.0,
]
dt_desired = [
0.0,
4.34081063,
5.57222438,
6.73882688,
7.84061813,
8.87759812,
9.84976688,
10.75712437,
11.59967063,
12.37740563,
13.09032937,
13.73844188,
14.32174313,
14.84023312,
15.29391188,
15.68277937,
16.00683562,
16.26608063,
16.46051438,
16.59013687,
16.65494813,
16.65494813,
16.59013687,
16.46051438,
16.26608063,
16.00683562,
15.68277938,
15.29391188,
14.84023312,
14.32174313,
13.73844187,
13.09032938,
12.37740562,
11.59967063,
10.75712437,
9.84976687,
8.87759813,
7.84061812,
6.73882688,
5.57222437,
4.34081063,
]
np.testing.assert_allclose(p, p_desired, rtol, atol)
np.testing.assert_allclose(v, v_desired, rtol, atol)
np.testing.assert_allclose(dt, dt_desired, rtol, atol)
assert utils.SAFETY_FACTOR == 0.025
assert utils.N_SAMPLES == 41
assert utils.DEGREE_SPLINE == 3
assert utils.TIME_COMPENSATE_SPLINE == 0.0062
assert utils.POSITION_COMPONSATION == 0.02

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